diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 4a3a1e54186c..9b71adbdc9c6 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -1290,6 +1290,7 @@ class _TextFieldState extends State 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 with RestorationMixin implements _effectiveFocusNode.requestFocus(); } }; + handleDidLoseAccessibilityFocus = () { + _effectiveFocusNode.unfocus(); + }; case TargetPlatform.android: case TargetPlatform.fuchsia: @@ -1337,6 +1341,15 @@ class _TextFieldState extends State 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 with RestorationMixin implements _effectiveFocusNode.requestFocus(); } }; + handleDidLoseAccessibilityFocus = () { + _effectiveFocusNode.unfocus(); + }; } Widget child = RepaintBoundary( @@ -1478,6 +1494,7 @@ class _TextFieldState extends State with RestorationMixin implements _requestKeyboard(); }, onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus, + onDidLoseAccessibilityFocus: handleDidLoseAccessibilityFocus, child: child, ); }, diff --git a/packages/flutter/test/material/search_test.dart b/packages/flutter/test/material/search_test.dart index be9a1d6a2498..e1623b664fe8 100644 --- a/packages/flutter/test/material/search_test.dart +++ b/packages/flutter/test/material/search_test.dart @@ -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( @@ -651,9 +654,10 @@ void main() { debugDefaultTargetPlatformOverride != TargetPlatform.macOS) SemanticsFlag.namesRoute, ], actions: [ - 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( @@ -791,9 +798,10 @@ void main() { debugDefaultTargetPlatformOverride != TargetPlatform.macOS) SemanticsFlag.namesRoute, ], actions: [ - if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS || - debugDefaultTargetPlatformOverride == TargetPlatform.windows) + if (isDesktop) SemanticsAction.didGainAccessibilityFocus, + if (isDesktop) + SemanticsAction.didLoseAccessibilityFocus, SemanticsAction.tap, SemanticsAction.setSelection, SemanticsAction.setText, diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index b7a226e989f1..09fb36bce0f0 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -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.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.macOS, TargetPlatform.windows })); + }, variant: const TargetPlatformVariant({ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux })); testWidgets('TextField passes onEditingComplete to EditableText', (WidgetTester tester) async { void onEditingComplete() { }