add onFocus to text fields (#150648)

Adds `onFocus` support to Cupertino and Material text field widgets (similar to https://github.com/flutter/flutter/pull/142942).
This commit is contained in:
Yegor 2024-06-27 07:38:32 -07:00 committed by GitHub
parent 9a673175ab
commit ef34436402
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 395 additions and 0 deletions

View File

@ -1445,6 +1445,35 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
_requestKeyboard();
},
onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus,
onFocus: enabled
? () {
assert(
_effectiveFocusNode.canRequestFocus,
'Received SemanticsAction.focus from the engine. However, the FocusNode '
'of this text field cannot gain focus. This likely indicates a bug. '
'If this text field cannot be focused (e.g. because it is not '
'enabled), then its corresponding semantics node must be configured '
'such that the assistive technology cannot request focus on it.'
);
if (_effectiveFocusNode.canRequestFocus && !_effectiveFocusNode.hasFocus) {
_effectiveFocusNode.requestFocus();
} else if (!widget.readOnly) {
// If the platform requested focus, that means that previously the
// platform believed that the text field did not have focus (even
// though Flutter's widget system believed otherwise). This likely
// means that the on-screen keyboard is hidden, or more generally,
// there is no current editing session in this field. To correct
// that, keyboard must be requested.
//
// A concrete scenario where this can happen is when the user
// dismisses the keyboard on the web. The editing session is
// closed by the engine, but the text field widget stays focused
// in the framework.
_requestKeyboard();
}
}
: null,
child: TextFieldTapRegion(
child: IgnorePointer(
ignoring: !enabled,

View File

@ -1594,6 +1594,35 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
},
onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus,
onDidLoseAccessibilityFocus: handleDidLoseAccessibilityFocus,
onFocus: _isEnabled
? () {
assert(
_effectiveFocusNode.canRequestFocus,
'Received SemanticsAction.focus from the engine. However, the FocusNode '
'of this text field cannot gain focus. This likely indicates a bug. '
'If this text field cannot be focused (e.g. because it is not '
'enabled), then its corresponding semantics node must be configured '
'such that the assistive technology cannot request focus on it.'
);
if (_effectiveFocusNode.canRequestFocus && !_effectiveFocusNode.hasFocus) {
_effectiveFocusNode.requestFocus();
} else if (!widget.readOnly) {
// If the platform requested focus, that means that previously the
// platform believed that the text field did not have focus (even
// though Flutter's widget system believed otherwise). This likely
// means that the on-screen keyboard is hidden, or more generally,
// there is no current editing session in this field. To correct
// that, keyboard must be requested.
//
// A concrete scenario where this can happen is when the user
// dismisses the keyboard on the web. The editing session is
// closed by the engine, but the text field widget stays focused
// in the framework.
_requestKeyboard();
}
}
: null,
child: child,
);
},

View File

@ -10280,4 +10280,162 @@ void main() {
},
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
testWidgets('when enabled listens to onFocus events and gains focus', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoTextField(focusNode: focusNode),
),
);
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
if (defaultTargetPlatform == TargetPlatform.windows || defaultTargetPlatform == TargetPlatform.macOS)
SemanticsAction.didGainAccessibilityFocus,
// TODO(gspencergoog): also test for the presence of SemanticsAction.focus when
// this iOS issue is addressed: https://github.com/flutter/flutter/issues/150030
],
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
));
expect(focusNode.hasFocus, isFalse);
semanticsOwner.performAction(4, SemanticsAction.focus);
await tester.pumpAndSettle();
expect(focusNode.hasFocus, isTrue);
semantics.dispose();
}, variant: TargetPlatformVariant.all());
testWidgets('when disabled does not listen to onFocus events or gain focus', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoTextField(focusNode: focusNode, enabled: false),
),
);
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isReadOnly,
],
actions: <SemanticsAction>[
if (defaultTargetPlatform == TargetPlatform.windows || defaultTargetPlatform == TargetPlatform.macOS)
SemanticsAction.didGainAccessibilityFocus,
],
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
));
expect(focusNode.hasFocus, isFalse);
semanticsOwner.performAction(4, SemanticsAction.focus);
await tester.pumpAndSettle();
expect(focusNode.hasFocus, isFalse);
semantics.dispose();
}, variant: TargetPlatformVariant.all());
testWidgets('when receives SemanticsAction.focus while already focused, shows keyboard', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoTextField(focusNode: focusNode),
),
);
focusNode.requestFocus();
await tester.pumpAndSettle();
tester.testTextInput.log.clear();
expect(focusNode.hasFocus, isTrue);
semanticsOwner.performAction(4, SemanticsAction.focus);
await tester.pumpAndSettle();
expect(focusNode.hasFocus, isTrue);
expect(tester.testTextInput.log.single.method, 'TextInput.show');
semantics.dispose();
}, variant: TargetPlatformVariant.all());
testWidgets('when receives SemanticsAction.focus while focused but read-only, does not show keyboard', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
CupertinoApp(
home: CupertinoTextField(focusNode: focusNode, readOnly: true),
),
);
focusNode.requestFocus();
await tester.pumpAndSettle();
tester.testTextInput.log.clear();
expect(focusNode.hasFocus, isTrue);
semanticsOwner.performAction(4, SemanticsAction.focus);
await tester.pumpAndSettle();
expect(focusNode.hasFocus, isTrue);
expect(tester.testTextInput.log, isEmpty);
semantics.dispose();
}, variant: TargetPlatformVariant.all());
}

View File

@ -18350,6 +18350,185 @@ void main() {
},
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
testWidgets('when enabled listens to onFocus events and gains focus', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(focusNode: focusNode),
),
),
),
);
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
if (defaultTargetPlatform == TargetPlatform.windows || defaultTargetPlatform == TargetPlatform.macOS || defaultTargetPlatform == TargetPlatform.linux)
SemanticsAction.didGainAccessibilityFocus,
if (defaultTargetPlatform == TargetPlatform.windows || defaultTargetPlatform == TargetPlatform.macOS || defaultTargetPlatform == TargetPlatform.linux)
SemanticsAction.didLoseAccessibilityFocus,
// TODO(gspencergoog): also test for the presence of SemanticsAction.focus when
// this iOS issue is addressed: https://github.com/flutter/flutter/issues/150030
],
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
));
expect(focusNode.hasFocus, isFalse);
semanticsOwner.performAction(4, SemanticsAction.focus);
await tester.pumpAndSettle();
expect(focusNode.hasFocus, isTrue);
semantics.dispose();
}, variant: TargetPlatformVariant.all());
testWidgets('when disabled does not listen to onFocus events or gain focus', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(focusNode: focusNode, enabled: false),
),
),
),
);
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isReadOnly,
],
actions: <SemanticsAction>[
if (defaultTargetPlatform == TargetPlatform.windows || defaultTargetPlatform == TargetPlatform.macOS || defaultTargetPlatform == TargetPlatform.linux)
SemanticsAction.didGainAccessibilityFocus,
if (defaultTargetPlatform == TargetPlatform.windows || defaultTargetPlatform == TargetPlatform.macOS || defaultTargetPlatform == TargetPlatform.linux)
SemanticsAction.didLoseAccessibilityFocus,
],
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
));
expect(focusNode.hasFocus, isFalse);
semanticsOwner.performAction(4, SemanticsAction.focus);
await tester.pumpAndSettle();
expect(focusNode.hasFocus, isFalse);
semantics.dispose();
}, variant: TargetPlatformVariant.all());
testWidgets('when receives SemanticsAction.focus while already focused, shows keyboard', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(focusNode: focusNode),
),
),
),
);
focusNode.requestFocus();
await tester.pumpAndSettle();
tester.testTextInput.log.clear();
expect(focusNode.hasFocus, isTrue);
semanticsOwner.performAction(4, SemanticsAction.focus);
await tester.pumpAndSettle();
expect(focusNode.hasFocus, isTrue);
expect(tester.testTextInput.log.single.method, 'TextInput.show');
semantics.dispose();
}, variant: TargetPlatformVariant.all());
testWidgets('when receives SemanticsAction.focus while focused but read-only, does not show keyboard', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(focusNode: focusNode, readOnly: true),
),
),
),
);
focusNode.requestFocus();
await tester.pumpAndSettle();
tester.testTextInput.log.clear();
expect(focusNode.hasFocus, isTrue);
semanticsOwner.performAction(4, SemanticsAction.focus);
await tester.pumpAndSettle();
expect(focusNode.hasFocus, isTrue);
expect(tester.testTextInput.log, isEmpty);
semantics.dispose();
}, variant: TargetPlatformVariant.all());
}
/// A Simple widget for testing the obscure text.