diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart index c3af40acfd..9b58ac40c9 100644 --- a/packages/flutter/lib/src/cupertino/text_field.dart +++ b/packages/flutter/lib/src/cupertino/text_field.dart @@ -1071,6 +1071,7 @@ class _CupertinoTextFieldState extends State with Restoratio final TextEditingController controller = _effectiveController; TextSelectionControls? textSelectionControls = widget.selectionControls; + VoidCallback? handleDidGainAccessibilityFocus; switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.android: @@ -1082,6 +1083,13 @@ class _CupertinoTextFieldState extends State with Restoratio case TargetPlatform.macOS: textSelectionControls ??= cupertinoDesktopTextSelectionControls; + handleDidGainAccessibilityFocus = () { + // macOS automatically activated the TextField when it receives + // accessibility focus. + if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) { + _effectiveFocusNode.requestFocus(); + } + }; break; } @@ -1212,6 +1220,7 @@ class _CupertinoTextFieldState extends State with Restoratio } _requestKeyboard(); }, + onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus, child: IgnorePointer( ignoring: !enabled, child: Container( diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index e4061cb338..8b908e8f17 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -1144,6 +1144,7 @@ class _TextFieldState extends State with RestorationMixin implements final Color selectionColor; Color? autocorrectionTextRectColor; Radius? cursorRadius = widget.cursorRadius; + VoidCallback? handleDidGainAccessibilityFocus; switch (theme.platform) { case TargetPlatform.iOS: @@ -1169,6 +1170,13 @@ class _TextFieldState extends State with RestorationMixin implements selectionColor = selectionTheme.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); cursorRadius ??= const Radius.circular(2.0); cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); + handleDidGainAccessibilityFocus = () { + // macOS automatically activated the TextField when it receives + // accessibility focus. + if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) { + _effectiveFocusNode.requestFocus(); + } + }; break; case TargetPlatform.android: @@ -1310,6 +1318,7 @@ class _TextFieldState extends State with RestorationMixin implements _effectiveController.selection = TextSelection.collapsed(offset: _effectiveController.text.length); _requestKeyboard(); }, + onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus, child: child, ); }, diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index 017841a83a..bd23599aec 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -284,6 +284,56 @@ void main() { expect(find.byType(CupertinoButton), findsNothing); }, variant: const TargetPlatformVariant({ TargetPlatform.macOS }), skip: kIsWeb); + testWidgets('Activates the text field when receives semantics focus on Mac', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; + final FocusNode focusNode = FocusNode(); + await tester.pumpWidget( + CupertinoApp( + home: CupertinoTextField(focusNode: focusNode) + ), + ); + expect(semantics, hasSemantics( + TestSemantics.root( + children: [ + TestSemantics( + id: 1, + textDirection: TextDirection.ltr, + children: [ + TestSemantics( + id: 2, + children: [ + TestSemantics( + id: 3, + flags: [SemanticsFlag.scopesRoute], + children: [ + TestSemantics( + id: 4, + flags: [SemanticsFlag.isTextField, + SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled], + actions: [SemanticsAction.tap, + SemanticsAction.didGainAccessibilityFocus], + textDirection: TextDirection.ltr, + ), + ], + ), + ], + ), + ], + ), + ], + ), + ignoreRect: true, + ignoreTransform: true, + )); + + expect(focusNode.hasFocus, isFalse); + semanticsOwner.performAction(4, SemanticsAction.didGainAccessibilityFocus); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, isTrue); + semantics.dispose(); + }, variant: const TargetPlatformVariant({ TargetPlatform.macOS }), skip: kIsWeb); + testWidgets( 'takes available space horizontally and takes intrinsic space vertically no-strut', (WidgetTester tester) async { diff --git a/packages/flutter/test/material/search_test.dart b/packages/flutter/test/material/search_test.dart index 36636a22d8..a38450a2eb 100644 --- a/packages/flutter/test/material/search_test.dart +++ b/packages/flutter/test/material/search_test.dart @@ -626,6 +626,8 @@ void main() { debugDefaultTargetPlatformOverride != TargetPlatform.macOS) SemanticsFlag.namesRoute, ], actions: [ + if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) + SemanticsAction.didGainAccessibilityFocus, 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 823848c3ca..ee46a93a36 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -255,6 +255,57 @@ void main() { expect(find.byType(CupertinoButton), findsNothing); }, variant: const TargetPlatformVariant({ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux }), skip: kIsWeb); + testWidgets('Activates the text field when receives semantics focus on Mac', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; + final FocusNode focusNode = FocusNode(); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TextField(focusNode: focusNode), + ), + ), + ); + expect(semantics, hasSemantics( + TestSemantics.root( + children: [ + TestSemantics( + id: 1, + textDirection: TextDirection.ltr, + children: [ + TestSemantics( + id: 2, + children: [ + TestSemantics( + id: 3, + flags: [SemanticsFlag.scopesRoute], + children: [ + TestSemantics( + id: 4, + flags: [SemanticsFlag.isTextField], + actions: [SemanticsAction.tap, + SemanticsAction.didGainAccessibilityFocus], + textDirection: TextDirection.ltr, + ), + ], + ), + ], + ), + ], + ), + ], + ), + ignoreRect: true, + ignoreTransform: true, + )); + + expect(focusNode.hasFocus, isFalse); + semanticsOwner.performAction(4, SemanticsAction.didGainAccessibilityFocus); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, isTrue); + semantics.dispose(); + }, variant: const TargetPlatformVariant({ TargetPlatform.macOS }), skip: kIsWeb); + testWidgets('TextField passes onEditingComplete to EditableText', (WidgetTester tester) async { void onEditingComplete() { }