Feat: TextField can scroll when disabled (#140922)

This PR is adding a flag parameter to the `TextField` widget. This flag controls whether the TextField ignores pointers. The flag takes priority over other TextField behaviors such as enabled, so it can be useful when trying to have a disabled TextField that can be scrolled (behavior observed using TextArea on the web).

Adding a flag parameter to `TextField` helps with more customization and flexibility to the widget which can improve user experience. I am open to other ideas.   

Fixes issue #140147 

Before: 

https://github.com/flutter/flutter/assets/66151079/293e5b4e-3126-4a00-824d-1530aeaa494b

After:

https://github.com/flutter/flutter/assets/66151079/08c1af09-3bf9-4b49-b684-dda4dd920503

Usage:
```dart
child: TextField(
  ignorePointer: false,
  enabled: false,
),
```
This commit is contained in:
Bryan Olivares 2024-01-29 12:22:19 -08:00 committed by GitHub
parent 21ca59e72c
commit 1d5c2c5118
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 59 additions and 2 deletions

View file

@ -285,6 +285,7 @@ class TextField extends StatefulWidget {
this.onAppPrivateCommand,
this.inputFormatters,
this.enabled,
this.ignorePointers,
this.cursorWidth = 2.0,
this.cursorHeight,
this.cursorRadius,
@ -598,6 +599,11 @@ class TextField extends StatefulWidget {
/// [InputDecoration.enabled] property.
final bool? enabled;
/// Determines whether this widget ignores pointer events.
///
/// Defaults to null, and when null, does nothing.
final bool? ignorePointers;
/// {@macro flutter.widgets.editableText.cursorWidth}
final double cursorWidth;
@ -979,7 +985,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
@override
bool get selectionEnabled => widget.selectionEnabled;
bool get selectionEnabled => widget.selectionEnabled && _isEnabled;
// End of API for TextSelectionGestureDetectorBuilderDelegate.
bool get _isEnabled => widget.enabled ?? widget.decoration?.enabled ?? true;
@ -1559,7 +1565,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
onExit: (PointerExitEvent event) => _handleHover(false),
child: TextFieldTapRegion(
child: IgnorePointer(
ignoring: !_isEnabled,
ignoring: widget.ignorePointers ?? !_isEnabled,
child: AnimatedBuilder(
animation: controller, // changes the _currentLength
builder: (BuildContext context, Widget? child) {

View file

@ -142,6 +142,7 @@ class TextFormField extends FormField<String> {
super.validator,
List<TextInputFormatter>? inputFormatters,
bool? enabled,
bool? ignorePointers,
double cursorWidth = 2.0,
double? cursorHeight,
Radius? cursorRadius,
@ -238,6 +239,7 @@ class TextFormField extends FormField<String> {
onSubmitted: onFieldSubmitted,
inputFormatters: inputFormatters,
enabled: enabled ?? decoration?.enabled ?? true,
ignorePointers: ignorePointers,
cursorWidth: cursorWidth,
cursorHeight: cursorHeight,
cursorRadius: cursorRadius,

View file

@ -6535,6 +6535,55 @@ void main() {
semantics.dispose();
});
testWidgets('Can scroll multiline input when disabled', (WidgetTester tester) async {
final Key textFieldKey = UniqueKey();
final TextEditingController controller = _textEditingController(
text: kMoreThanFourLines,
);
await tester.pumpWidget(
overlay(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
key: textFieldKey,
controller: controller,
ignorePointers: false,
enabled: false,
style: const TextStyle(color: Colors.black, fontSize: 34.0),
maxLines: 2,
),
),
);
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
final RenderBox inputBox = findInputBox();
// Check that the last line of text is not displayed.
final Offset firstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
final Offset fourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
expect(firstPos.dx, fourthPos.dx);
expect(firstPos.dy, lessThan(fourthPos.dy));
expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(firstPos)), isTrue);
expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(fourthPos)), isFalse);
final TestGesture gesture = await tester.startGesture(firstPos, pointer: 7);
await tester.pump();
await gesture.moveBy(const Offset(0.0, -1000.0));
await tester.pumpAndSettle();
await gesture.moveBy(const Offset(0.0, -1000.0));
await tester.pumpAndSettle();
await gesture.up();
await tester.pumpAndSettle();
// Now the first line is scrolled up, and the fourth line is visible.
final Offset finalFirstPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('First'));
final Offset finalFourthPos = textOffsetToPosition(tester, kMoreThanFourLines.indexOf('Fourth'));
expect(finalFirstPos.dy, lessThan(firstPos.dy));
expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(finalFirstPos)), isFalse);
expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(finalFourthPos)), isTrue);
});
testWidgets('Disabled text field does not have tap action', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(