Skip to content

Commit

Permalink
add onFocus to text fields (#150648)
Browse files Browse the repository at this point in the history
Adds `onFocus` support to Cupertino and Material text field widgets (similar to flutter/flutter#142942).
  • Loading branch information
yjbanov authored Jun 27, 2024
1 parent 9a67317 commit ef34436
Show file tree
Hide file tree
Showing 4 changed files with 395 additions and 0 deletions.
29 changes: 29 additions & 0 deletions packages/flutter/lib/src/cupertino/text_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
29 changes: 29 additions & 0 deletions packages/flutter/lib/src/material/text_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
},
Expand Down
158 changes: 158 additions & 0 deletions packages/flutter/test/cupertino/text_field_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Loading

0 comments on commit ef34436

Please sign in to comment.