Unifies text field focus management in desktops (#129652)
related https://github.com/flutter/flutter/issues/128709 engine PR: https://github.com/flutter/engine/pull/43279 The web engine requires a way to unfocus textfield, It comes to nature to me that we should leverage didGain/didLose a11y focus action. I also unifies the action handler of all desktop platforms ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [Features we expect every widget to implement]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat
This commit is contained in:
parent
ab86c79c3a
commit
2e8af2c35e
@ -1290,6 +1290,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
||||
Color? autocorrectionTextRectColor;
|
||||
Radius? cursorRadius = widget.cursorRadius;
|
||||
VoidCallback? handleDidGainAccessibilityFocus;
|
||||
VoidCallback? handleDidLoseAccessibilityFocus;
|
||||
|
||||
switch (theme.platform) {
|
||||
case TargetPlatform.iOS:
|
||||
@ -1320,6 +1321,9 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
||||
_effectiveFocusNode.requestFocus();
|
||||
}
|
||||
};
|
||||
handleDidLoseAccessibilityFocus = () {
|
||||
_effectiveFocusNode.unfocus();
|
||||
};
|
||||
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
@ -1337,6 +1341,15 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
||||
cursorOpacityAnimates ??= false;
|
||||
cursorColor = _hasError ? _errorColor : widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
|
||||
selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
|
||||
handleDidGainAccessibilityFocus = () {
|
||||
// Automatically activate the TextField when it receives accessibility focus.
|
||||
if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) {
|
||||
_effectiveFocusNode.requestFocus();
|
||||
}
|
||||
};
|
||||
handleDidLoseAccessibilityFocus = () {
|
||||
_effectiveFocusNode.unfocus();
|
||||
};
|
||||
|
||||
case TargetPlatform.windows:
|
||||
forcePressEnabled = false;
|
||||
@ -1351,6 +1364,9 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
||||
_effectiveFocusNode.requestFocus();
|
||||
}
|
||||
};
|
||||
handleDidLoseAccessibilityFocus = () {
|
||||
_effectiveFocusNode.unfocus();
|
||||
};
|
||||
}
|
||||
|
||||
Widget child = RepaintBoundary(
|
||||
@ -1478,6 +1494,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
||||
_requestKeyboard();
|
||||
},
|
||||
onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus,
|
||||
onDidLoseAccessibilityFocus: handleDidLoseAccessibilityFocus,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
|
@ -605,6 +605,9 @@ void main() {
|
||||
const Widget flexibleSpace = Text('FlexibleSpace');
|
||||
|
||||
TestSemantics buildExpected({ required String routeName }) {
|
||||
final bool isDesktop = debugDefaultTargetPlatformOverride == TargetPlatform.macOS ||
|
||||
debugDefaultTargetPlatformOverride == TargetPlatform.windows ||
|
||||
debugDefaultTargetPlatformOverride == TargetPlatform.linux;
|
||||
return TestSemantics.root(
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
@ -651,9 +654,10 @@ void main() {
|
||||
debugDefaultTargetPlatformOverride != TargetPlatform.macOS) SemanticsFlag.namesRoute,
|
||||
],
|
||||
actions: <SemanticsAction>[
|
||||
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS ||
|
||||
debugDefaultTargetPlatformOverride == TargetPlatform.windows)
|
||||
if (isDesktop)
|
||||
SemanticsAction.didGainAccessibilityFocus,
|
||||
if (isDesktop)
|
||||
SemanticsAction.didLoseAccessibilityFocus,
|
||||
SemanticsAction.tap,
|
||||
SemanticsAction.setSelection,
|
||||
SemanticsAction.setText,
|
||||
@ -748,6 +752,9 @@ void main() {
|
||||
|
||||
group('contributes semantics', () {
|
||||
TestSemantics buildExpected({ required String routeName }) {
|
||||
final bool isDesktop = debugDefaultTargetPlatformOverride == TargetPlatform.macOS ||
|
||||
debugDefaultTargetPlatformOverride == TargetPlatform.windows ||
|
||||
debugDefaultTargetPlatformOverride == TargetPlatform.linux;
|
||||
return TestSemantics.root(
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
@ -791,9 +798,10 @@ void main() {
|
||||
debugDefaultTargetPlatformOverride != TargetPlatform.macOS) SemanticsFlag.namesRoute,
|
||||
],
|
||||
actions: <SemanticsAction>[
|
||||
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS ||
|
||||
debugDefaultTargetPlatformOverride == TargetPlatform.windows)
|
||||
if (isDesktop)
|
||||
SemanticsAction.didGainAccessibilityFocus,
|
||||
if (isDesktop)
|
||||
SemanticsAction.didLoseAccessibilityFocus,
|
||||
SemanticsAction.tap,
|
||||
SemanticsAction.setSelection,
|
||||
SemanticsAction.setText,
|
||||
|
@ -655,7 +655,7 @@ void main() {
|
||||
expect(editableText.cursorOpacityAnimates, false);
|
||||
});
|
||||
|
||||
testWidgets('Activates the text field when receives semantics focus on Mac, Windows', (WidgetTester tester) async {
|
||||
testWidgets('Activates the text field when receives semantics focus on desktops', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
|
||||
final FocusNode focusNode = FocusNode();
|
||||
@ -686,6 +686,7 @@ void main() {
|
||||
actions: <SemanticsAction>[
|
||||
SemanticsAction.tap,
|
||||
SemanticsAction.didGainAccessibilityFocus,
|
||||
SemanticsAction.didLoseAccessibilityFocus,
|
||||
],
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
@ -705,8 +706,12 @@ void main() {
|
||||
semanticsOwner.performAction(4, SemanticsAction.didGainAccessibilityFocus);
|
||||
await tester.pumpAndSettle();
|
||||
expect(focusNode.hasFocus, isTrue);
|
||||
|
||||
semanticsOwner.performAction(4, SemanticsAction.didLoseAccessibilityFocus);
|
||||
await tester.pumpAndSettle();
|
||||
expect(focusNode.hasFocus, isFalse);
|
||||
semantics.dispose();
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows }));
|
||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux }));
|
||||
|
||||
testWidgets('TextField passes onEditingComplete to EditableText', (WidgetTester tester) async {
|
||||
void onEditingComplete() { }
|
||||
|
Loading…
x
Reference in New Issue
Block a user