Unverified Commit 8daa165d authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Make disabled buttons/chips/text fields not be focusable. (#38726)

This changes the behavior of text fields, Material buttons, and Chips so that if they are disabled they lose focus. Before this change, it was possible to disable a control and then use focus traversal to reach it anyhow, and in the case of text fields, enter text into a disabled field.

Fixes #33985
parent 0ebcfe10
......@@ -332,6 +332,7 @@ class _RawMaterialButtonState extends State<RawMaterialButton> {
final Widget result = Focus(
focusNode: widget.focusNode,
canRequestFocus: widget.enabled,
onFocusChange: _handleFocusedChanged,
autofocus: widget.autofocus,
child: ConstrainedBox(
......
......@@ -1728,6 +1728,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
onFocusChange: _handleFocus,
focusNode: widget.focusNode,
autofocus: widget.autofocus,
canRequestFocus: widget.isEnabled,
child: Material(
elevation: isTapping ? pressElevation : elevation,
shadowColor: widget.selected ? selectedShadowColor : shadowColor,
......
......@@ -313,6 +313,7 @@ class IconButton extends StatelessWidget {
child: Focus(
focusNode: focusNode,
autofocus: autofocus,
canRequestFocus: onPressed != null,
child: InkResponse(
onTap: onPressed,
child: result,
......
......@@ -686,6 +686,8 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
bool get selectionEnabled => widget.selectionEnabled;
// End of API for TextSelectionGestureDetectorBuilderDelegate.
bool get _isEnabled => widget.enabled ?? widget.decoration?.enabled ?? true;
InputDecoration _getEffectiveDecoration() {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final ThemeData themeData = Theme.of(context);
......@@ -755,8 +757,10 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
void initState() {
super.initState();
_selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this);
if (widget.controller == null)
if (widget.controller == null) {
_controller = TextEditingController();
}
_effectiveFocusNode.canRequestFocus = _isEnabled;
}
@override
......@@ -766,11 +770,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
_controller = TextEditingController.fromValue(oldWidget.controller.value);
else if (widget.controller != null && oldWidget.controller == null)
_controller = null;
final bool isEnabled = widget.enabled ?? widget.decoration?.enabled ?? true;
final bool wasEnabled = oldWidget.enabled ?? oldWidget.decoration?.enabled ?? true;
if (wasEnabled && !isEnabled) {
_effectiveFocusNode.unfocus();
}
_effectiveFocusNode.canRequestFocus = _isEnabled;
if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly) {
if(_effectiveController.selection.isCollapsed) {
_showSelectionHandles = !widget.readOnly;
......@@ -1045,7 +1045,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
onEnter: _handleMouseEnter,
onExit: _handleMouseExit,
child: IgnorePointer(
ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true),
ignoring: !_isEnabled,
child: _selectionGestureDetectorBuilder.buildGestureDetector(
behavior: HitTestBehavior.translucent,
child: child,
......
......@@ -391,7 +391,8 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
set skipTraversal(bool value) {
if (value != _skipTraversal) {
_skipTraversal = value;
_notify();
_manager?._dirtyNodes?.add(this);
_manager?._markNeedsUpdate();
}
}
......@@ -423,7 +424,8 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
if (!_canRequestFocus) {
unfocus();
}
_notify();
_manager?._dirtyNodes?.add(this);
_manager?._markNeedsUpdate();
}
}
......
......@@ -345,7 +345,7 @@ class _ShortcutsState extends State<Shortcuts> {
@override
Widget build(BuildContext context) {
return Focus(
skipTraversal: true,
canRequestFocus: false,
onKey: _handleOnKey,
child: _ShortcutsMarker(
manager: manager,
......
......@@ -1837,4 +1837,71 @@ void main() {
// Teardown.
await gesture.removePointer();
});
testWidgets('loses focus when disabled', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'InputChip');
await tester.pumpWidget(
_wrapForChip(
child: InputChip(
focusNode: focusNode,
autofocus: true,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(0.0))),
avatar: const CircleAvatar(child: Text('A')),
label: const Text('Chip A'),
onPressed: () { },
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
await tester.pumpWidget(
_wrapForChip(
child: InputChip(
focusNode: focusNode,
autofocus: true,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(0.0))),
avatar: const CircleAvatar(child: Text('A')),
label: const Text('Chip A'),
onPressed: null,
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isFalse);
});
testWidgets('cannot be traversed to when disabled', (WidgetTester tester) async {
final FocusNode focusNode1 = FocusNode(debugLabel: 'InputChip 1');
final FocusNode focusNode2 = FocusNode(debugLabel: 'InputChip 2');
await tester.pumpWidget(
_wrapForChip(
child: Column(
children: <Widget>[
InputChip(
focusNode: focusNode1,
autofocus: true,
label: const Text('Chip A'),
onPressed: () { },
),
InputChip(
focusNode: focusNode2,
autofocus: true,
label: const Text('Chip B'),
onPressed: null,
),
],
),
),
);
await tester.pump();
expect(focusNode1.hasPrimaryFocus, isTrue);
expect(focusNode2.hasPrimaryFocus, isFalse);
expect(focusNode1.nextFocus(), isTrue);
await tester.pump();
expect(focusNode1.hasPrimaryFocus, isTrue);
expect(focusNode2.hasPrimaryFocus, isFalse);
});
}
......@@ -332,13 +332,81 @@ void main() {
semantics.dispose();
});
testWidgets('IconButton loses focus when disabled.', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'IconButton');
await tester.pumpWidget(
wrap(
child: IconButton(
focusNode: focusNode,
autofocus: true,
onPressed: () {},
icon: const Icon(Icons.link),
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
await tester.pumpWidget(
wrap(
child: IconButton(
focusNode: focusNode,
autofocus: true,
onPressed: null,
icon: const Icon(Icons.link),
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isFalse);
});
testWidgets("Disabled IconButton can't be traversed to when disabled.", (WidgetTester tester) async {
final FocusNode focusNode1 = FocusNode(debugLabel: 'IconButton 1');
final FocusNode focusNode2 = FocusNode(debugLabel: 'IconButton 2');
await tester.pumpWidget(
wrap(
child: Column(
children: <Widget>[
IconButton(
focusNode: focusNode1,
autofocus: true,
onPressed: () {},
icon: const Icon(Icons.link),
),
IconButton(
focusNode: focusNode2,
onPressed: null,
icon: const Icon(Icons.link),
),
],
),
),
);
await tester.pump();
expect(focusNode1.hasPrimaryFocus, isTrue);
expect(focusNode2.hasPrimaryFocus, isFalse);
expect(focusNode1.nextFocus(), isTrue);
await tester.pump();
expect(focusNode1.hasPrimaryFocus, isTrue);
expect(focusNode2.hasPrimaryFocus, isFalse);
});
}
Widget wrap({ Widget child }) {
return Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(child: child),
return DefaultFocusTraversal(
policy: ReadingOrderTraversalPolicy(),
child: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(child: child),
),
),
);
}
......@@ -237,6 +237,78 @@ void main() {
expect(box, paints..rect(color: focusColor));
});
testWidgets('$RawMaterialButton loses focus when disabled.', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'RawMaterialButton');
await tester.pumpWidget(
MaterialApp(
home: Center(
child: RawMaterialButton(
autofocus: true,
focusNode: focusNode,
onPressed: () {},
child: Container(width: 100, height: 100, color: const Color(0xffff0000)),
),
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: RawMaterialButton(
focusNode: focusNode,
onPressed: null,
child: Container(width: 100, height: 100, color: const Color(0xffff0000)),
),
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isFalse);
});
testWidgets("Disabled $RawMaterialButton can't be traversed to when disabled.", (WidgetTester tester) async {
final FocusNode focusNode1 = FocusNode(debugLabel: '$RawMaterialButton 1');
final FocusNode focusNode2 = FocusNode(debugLabel: '$RawMaterialButton 2');
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Column(
children: <Widget>[
RawMaterialButton(
autofocus: true,
focusNode: focusNode1,
onPressed: () {},
child: Container(width: 100, height: 100, color: const Color(0xffff0000)),
),
RawMaterialButton(
autofocus: true,
focusNode: focusNode2,
onPressed: null,
child: Container(width: 100, height: 100, color: const Color(0xffff0000)),
),
],
),
),
),
);
await tester.pump();
expect(focusNode1.hasPrimaryFocus, isTrue);
expect(focusNode2.hasPrimaryFocus, isFalse);
expect(focusNode1.nextFocus(), isTrue);
await tester.pump();
expect(focusNode1.hasPrimaryFocus, isTrue);
expect(focusNode2.hasPrimaryFocus, isFalse);
});
testWidgets('$RawMaterialButton handles hover', (WidgetTester tester) async {
const Key key = Key('test');
const Color hoverColor = Color(0xff00ff00);
......
......@@ -3391,6 +3391,83 @@ void main() {
semantics.dispose();
});
testWidgets('TextField loses focus when disabled', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'TextField');
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
focusNode: focusNode,
autofocus: true,
maxLength: 10,
enabled: true,
),
),
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
focusNode: focusNode,
autofocus: true,
maxLength: 10,
enabled: false,
),
),
),
),
);
await tester.pump();
expect(focusNode.hasPrimaryFocus, isFalse);
});
testWidgets("Disabled TextField can't be traversed to when disabled.", (WidgetTester tester) async {
final FocusNode focusNode1 = FocusNode(debugLabel: 'TextField 1');
final FocusNode focusNode2 = FocusNode(debugLabel: 'TextField 2');
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: Column(
children: <Widget>[
TextField(
focusNode: focusNode1,
autofocus: true,
maxLength: 10,
enabled: true,
),
TextField(
focusNode: focusNode2,
maxLength: 10,
enabled: false,
),
],
),
),
),
),
);
await tester.pump();
expect(focusNode1.hasPrimaryFocus, isTrue);
expect(focusNode2.hasPrimaryFocus, isFalse);
expect(focusNode1.nextFocus(), isTrue);
await tester.pump();
expect(focusNode1.hasPrimaryFocus, isTrue);
expect(focusNode2.hasPrimaryFocus, isFalse);
});
void sendFakeKeyEvent(Map<String, dynamic> data) {
ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name,
......
......@@ -184,36 +184,11 @@ void main() {
);
expect(_validateCalled, 1);
await tester.showKeyboard(find.byType(TextField));
await tester.enterText(find.byType(TextField), 'a');
await tester.pump();
expect(_validateCalled, 2);
});
testWidgets('validate is not called if widget is disabled', (WidgetTester tester) async {
int _validateCalled = 0;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextFormField(
enabled: false,
autovalidate: true,
validator: (String value) { _validateCalled += 1; return null; },
),
),
),
),
);
expect(_validateCalled, 0);
await tester.showKeyboard(find.byType(TextField));
await tester.enterText(find.byType(TextField), 'a');
await tester.pump();
expect(_validateCalled, 0);
});
testWidgets('validate is called if widget is enabled', (WidgetTester tester) async {
int _validateCalled = 0;
......@@ -232,7 +207,6 @@ void main() {
);
expect(_validateCalled, 1);
await tester.showKeyboard(find.byType(TextField));
await tester.enterText(find.byType(TextField), 'a');
await tester.pump();
expect(_validateCalled, 2);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment