mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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:
parent
9a673175ab
commit
ef34436402
@ -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,
|
||||
|
||||
@ -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,
|
||||
);
|
||||
},
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user