From bcb4abc9d9ed8b87eb8173ac471e0cb4f6c8f3f8 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Wed, 29 May 2024 11:20:22 -0700 Subject: [PATCH 1/7] add SemanticsAction.focus --- lib/ui/semantics.dart | 4 ++++ lib/ui/semantics/semantics_node.h | 1 + lib/web_ui/lib/semantics.dart | 3 +++ shell/platform/embedder/embedder.h | 1 + shell/platform/fuchsia/flutter/accessibility_bridge.cc | 3 +++ shell/platform/linux/fl_accessible_node.cc | 1 + testing/dart/semantics_test.dart | 2 +- 7 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index e0da396d8ec58..4e6e23a95f044 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -44,6 +44,7 @@ class SemanticsAction { static const int _kMoveCursorForwardByWordIndex = 1 << 19; static const int _kMoveCursorBackwardByWordIndex = 1 << 20; static const int _kSetTextIndex = 1 << 21; + static const int _kFocus = 1 << 22; // READ THIS: if you add an action here, you MUST update the // numSemanticsActions value in testing/dart/semantics_test.dart, or tests // will fail. @@ -201,6 +202,8 @@ class SemanticsAction { /// movement should extend (or start) a selection. static const SemanticsAction moveCursorBackwardByWord = SemanticsAction._(_kMoveCursorBackwardByWordIndex, 'moveCursorBackwardByWord'); + static const SemanticsAction focus = SemanticsAction._(_kFocus, 'focus'); + /// The possible semantics actions. /// /// The map's key is the [index] of the action and the value is the action @@ -228,6 +231,7 @@ class SemanticsAction { _kMoveCursorForwardByWordIndex: moveCursorForwardByWord, _kMoveCursorBackwardByWordIndex: moveCursorBackwardByWord, _kSetTextIndex: setText, + _kFocus: focus, }; static List get values => _kActionById.values.toList(growable: false); diff --git a/lib/ui/semantics/semantics_node.h b/lib/ui/semantics/semantics_node.h index 993c1dc80eb40..e3c620fc1aa27 100644 --- a/lib/ui/semantics/semantics_node.h +++ b/lib/ui/semantics/semantics_node.h @@ -42,6 +42,7 @@ enum class SemanticsAction : int32_t { kMoveCursorForwardByWord = 1 << 19, kMoveCursorBackwardByWord = 1 << 20, kSetText = 1 << 21, + kFocus = 1 << 22, }; const int kVerticalScrollSemanticsActions = diff --git a/lib/web_ui/lib/semantics.dart b/lib/web_ui/lib/semantics.dart index 4ede9f248f485..02168d197176f 100644 --- a/lib/web_ui/lib/semantics.dart +++ b/lib/web_ui/lib/semantics.dart @@ -32,6 +32,7 @@ class SemanticsAction { static const int _kMoveCursorForwardByWordIndex = 1 << 19; static const int _kMoveCursorBackwardByWordIndex = 1 << 20; static const int _kSetTextIndex = 1 << 21; + static const int _kFocus = 1 << 22; static const SemanticsAction tap = SemanticsAction._(_kTapIndex, 'tap'); static const SemanticsAction longPress = SemanticsAction._(_kLongPressIndex, 'longPress'); @@ -55,6 +56,7 @@ class SemanticsAction { static const SemanticsAction dismiss = SemanticsAction._(_kDismissIndex, 'dismiss'); static const SemanticsAction moveCursorForwardByWord = SemanticsAction._(_kMoveCursorForwardByWordIndex, 'moveCursorForwardByWord'); static const SemanticsAction moveCursorBackwardByWord = SemanticsAction._(_kMoveCursorBackwardByWordIndex, 'moveCursorBackwardByWord'); + static const SemanticsAction focus = SemanticsAction._(_kFocus, 'focus'); static const Map _kActionById = { _kTapIndex: tap, @@ -79,6 +81,7 @@ class SemanticsAction { _kMoveCursorForwardByWordIndex: moveCursorForwardByWord, _kMoveCursorBackwardByWordIndex: moveCursorBackwardByWord, _kSetTextIndex: setText, + _kFocus: focus, }; static List get values => _kActionById.values.toList(growable: false); diff --git a/shell/platform/embedder/embedder.h b/shell/platform/embedder/embedder.h index 41b455ce8df75..4ba55543b3d31 100644 --- a/shell/platform/embedder/embedder.h +++ b/shell/platform/embedder/embedder.h @@ -162,6 +162,7 @@ typedef enum { kFlutterSemanticsActionMoveCursorBackwardByWord = 1 << 20, /// Replace the current text in the text field. kFlutterSemanticsActionSetText = 1 << 21, + kFlutterSemanticsActionFocus = 1 << 22, } FlutterSemanticsAction; /// The set of properties that may be associated with a semantics node. diff --git a/shell/platform/fuchsia/flutter/accessibility_bridge.cc b/shell/platform/fuchsia/flutter/accessibility_bridge.cc index c317a17b5497b..b9f443035a37f 100644 --- a/shell/platform/fuchsia/flutter/accessibility_bridge.cc +++ b/shell/platform/fuchsia/flutter/accessibility_bridge.cc @@ -173,6 +173,9 @@ std::string NodeActionsToString(const flutter::SemanticsNode& node) { if (node.HasAction(flutter::SemanticsAction::kTap)) { output += "kTap|"; } + if (node.HasAction(flutter::SemanticsAction::kFocus)) { + output += "kFocus|"; + } return output; } diff --git a/shell/platform/linux/fl_accessible_node.cc b/shell/platform/linux/fl_accessible_node.cc index f8316755ecb87..1c937c8e43e7c 100644 --- a/shell/platform/linux/fl_accessible_node.cc +++ b/shell/platform/linux/fl_accessible_node.cc @@ -59,6 +59,7 @@ static ActionData action_mapping[] = { {kFlutterSemanticsActionMoveCursorForwardByWord, "MoveCursorForwardByWord"}, {kFlutterSemanticsActionMoveCursorBackwardByWord, "MoveCursorBackwardByWord"}, + {kFocus, "Focus"}, {static_cast(0), nullptr}}; struct FlAccessibleNodePrivate { diff --git a/testing/dart/semantics_test.dart b/testing/dart/semantics_test.dart index a995b4037fb87..85200ebef95c1 100644 --- a/testing/dart/semantics_test.dart +++ b/testing/dart/semantics_test.dart @@ -11,7 +11,7 @@ import 'package:litetest/litetest.dart'; void main() { // This must match the number of flags in lib/ui/semantics.dart - const int numSemanticsFlags = 28; + const int numSemanticsFlags = 29; test('SemanticsFlag.values refers to all flags.', () async { expect(SemanticsFlag.values.length, equals(numSemanticsFlags)); for (int index = 0; index < numSemanticsFlags; ++index) { From 34a0dd182701b141ec6e4d127bafa59c878e0c96 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Wed, 29 May 2024 12:10:32 -0700 Subject: [PATCH 2/7] fix typo --- shell/platform/linux/fl_accessible_node.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/linux/fl_accessible_node.cc b/shell/platform/linux/fl_accessible_node.cc index 1c937c8e43e7c..962ff0cc354fa 100644 --- a/shell/platform/linux/fl_accessible_node.cc +++ b/shell/platform/linux/fl_accessible_node.cc @@ -59,7 +59,7 @@ static ActionData action_mapping[] = { {kFlutterSemanticsActionMoveCursorForwardByWord, "MoveCursorForwardByWord"}, {kFlutterSemanticsActionMoveCursorBackwardByWord, "MoveCursorBackwardByWord"}, - {kFocus, "Focus"}, + {kFlutterSemanticsActionFocus, "Focus"}, {static_cast(0), nullptr}}; struct FlAccessibleNodePrivate { From 9f83a891ef8de8f17d5e24134b5b1335bdbaf1ee Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Wed, 29 May 2024 12:55:39 -0700 Subject: [PATCH 3/7] numSemanticsActions, not numSemanticsFlags! --- lib/ui/semantics.dart | 8 +++++--- lib/web_ui/test/engine/semantics/semantics_api_test.dart | 2 +- testing/dart/semantics_test.dart | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index 4e6e23a95f044..fa6bc6945be99 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -46,7 +46,8 @@ class SemanticsAction { static const int _kSetTextIndex = 1 << 21; static const int _kFocus = 1 << 22; // READ THIS: if you add an action here, you MUST update the - // numSemanticsActions value in testing/dart/semantics_test.dart, or tests + // numSemanticsActions value in testing/dart/semantics_test.dart and + // lib/web_ui/test/engine/semantics/semantics_api_test.dart, or tests // will fail. /// The equivalent of a user briefly tapping the screen with the finger @@ -289,8 +290,9 @@ class SemanticsFlag { static const int _kHasExpandedStateIndex = 1 << 26; static const int _kIsExpandedIndex = 1 << 27; // READ THIS: if you add a flag here, you MUST update the numSemanticsFlags - // value in testing/dart/semantics_test.dart, or tests will fail. Also, - // please update the Flag enum in + // value in testing/dart/semantics_test.dart and + // lib/web_ui/test/engine/semantics/semantics_api_test.dart, or tests will + // fail. Also, please update the Flag enum in // flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java, // and the SemanticsFlag class in lib/web_ui/lib/semantics.dart. If the new flag // affects the visibility of a [SemanticsNode] to accessibility services, diff --git a/lib/web_ui/test/engine/semantics/semantics_api_test.dart b/lib/web_ui/test/engine/semantics/semantics_api_test.dart index db1cda282e1c5..1b558d403737e 100644 --- a/lib/web_ui/test/engine/semantics/semantics_api_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_api_test.dart @@ -29,7 +29,7 @@ void testMain() { }); // This must match the number of actions in lib/ui/semantics.dart - const int numSemanticsActions = 22; + const int numSemanticsActions = 23; test('SemanticsAction.values refers to all actions.', () async { expect(SemanticsAction.values.length, equals(numSemanticsActions)); for (int index = 0; index < numSemanticsActions; ++index) { diff --git a/testing/dart/semantics_test.dart b/testing/dart/semantics_test.dart index 85200ebef95c1..24c5bd10e088f 100644 --- a/testing/dart/semantics_test.dart +++ b/testing/dart/semantics_test.dart @@ -11,7 +11,7 @@ import 'package:litetest/litetest.dart'; void main() { // This must match the number of flags in lib/ui/semantics.dart - const int numSemanticsFlags = 29; + const int numSemanticsFlags = 28; test('SemanticsFlag.values refers to all flags.', () async { expect(SemanticsFlag.values.length, equals(numSemanticsFlags)); for (int index = 0; index < numSemanticsFlags; ++index) { @@ -22,7 +22,7 @@ void main() { }); // This must match the number of actions in lib/ui/semantics.dart - const int numSemanticsActions = 22; + const int numSemanticsActions = 23; test('SemanticsAction.values refers to all actions.', () async { expect(SemanticsAction.values.length, equals(numSemanticsActions)); for (int index = 0; index < numSemanticsActions; ++index) { From 72444344831d7d58e15b697f6f38cd6484156822 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Wed, 29 May 2024 14:26:23 -0700 Subject: [PATCH 4/7] update Java enum --- shell/platform/android/io/flutter/view/AccessibilityBridge.java | 1 + 1 file changed, 1 insertion(+) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index a0280322c06f9..0c52c91227e00 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -2103,6 +2103,7 @@ public enum Action { MOVE_CURSOR_FORWARD_BY_WORD(1 << 19), MOVE_CURSOR_BACKWARD_BY_WORD(1 << 20), SET_TEXT(1 << 21); + FOCUS(1 << 22); public final int value; From 07d3f0863d8527a7d6bb90af67439448031c7179 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Wed, 29 May 2024 14:48:09 -0700 Subject: [PATCH 5/7] fix typo --- shell/platform/android/io/flutter/view/AccessibilityBridge.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 0c52c91227e00..e6014f4329ca5 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -2102,7 +2102,7 @@ public enum Action { DISMISS(1 << 18), MOVE_CURSOR_FORWARD_BY_WORD(1 << 19), MOVE_CURSOR_BACKWARD_BY_WORD(1 << 20), - SET_TEXT(1 << 21); + SET_TEXT(1 << 21), FOCUS(1 << 22); public final int value; From d806b6693f769c6e28507aac87428de013da1169 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Wed, 29 May 2024 15:46:41 -0700 Subject: [PATCH 6/7] more typos /facepalm --- lib/ui/semantics.dart | 6 +++--- lib/web_ui/lib/semantics.dart | 6 +++--- shell/platform/embedder/embedder.h | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index fa6bc6945be99..7ab26af4daff7 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -44,7 +44,7 @@ class SemanticsAction { static const int _kMoveCursorForwardByWordIndex = 1 << 19; static const int _kMoveCursorBackwardByWordIndex = 1 << 20; static const int _kSetTextIndex = 1 << 21; - static const int _kFocus = 1 << 22; + static const int _kFocusIndex = 1 << 22; // READ THIS: if you add an action here, you MUST update the // numSemanticsActions value in testing/dart/semantics_test.dart and // lib/web_ui/test/engine/semantics/semantics_api_test.dart, or tests @@ -203,7 +203,7 @@ class SemanticsAction { /// movement should extend (or start) a selection. static const SemanticsAction moveCursorBackwardByWord = SemanticsAction._(_kMoveCursorBackwardByWordIndex, 'moveCursorBackwardByWord'); - static const SemanticsAction focus = SemanticsAction._(_kFocus, 'focus'); + static const SemanticsAction focus = SemanticsAction._(_kFocusIndex, 'focus'); /// The possible semantics actions. /// @@ -232,7 +232,7 @@ class SemanticsAction { _kMoveCursorForwardByWordIndex: moveCursorForwardByWord, _kMoveCursorBackwardByWordIndex: moveCursorBackwardByWord, _kSetTextIndex: setText, - _kFocus: focus, + _kFocusIndex: focus, }; static List get values => _kActionById.values.toList(growable: false); diff --git a/lib/web_ui/lib/semantics.dart b/lib/web_ui/lib/semantics.dart index 02168d197176f..3656abd92375e 100644 --- a/lib/web_ui/lib/semantics.dart +++ b/lib/web_ui/lib/semantics.dart @@ -32,7 +32,7 @@ class SemanticsAction { static const int _kMoveCursorForwardByWordIndex = 1 << 19; static const int _kMoveCursorBackwardByWordIndex = 1 << 20; static const int _kSetTextIndex = 1 << 21; - static const int _kFocus = 1 << 22; + static const int _kFocusIndex = 1 << 22; static const SemanticsAction tap = SemanticsAction._(_kTapIndex, 'tap'); static const SemanticsAction longPress = SemanticsAction._(_kLongPressIndex, 'longPress'); @@ -56,7 +56,7 @@ class SemanticsAction { static const SemanticsAction dismiss = SemanticsAction._(_kDismissIndex, 'dismiss'); static const SemanticsAction moveCursorForwardByWord = SemanticsAction._(_kMoveCursorForwardByWordIndex, 'moveCursorForwardByWord'); static const SemanticsAction moveCursorBackwardByWord = SemanticsAction._(_kMoveCursorBackwardByWordIndex, 'moveCursorBackwardByWord'); - static const SemanticsAction focus = SemanticsAction._(_kFocus, 'focus'); + static const SemanticsAction focus = SemanticsAction._(_kFocusIndex, 'focus'); static const Map _kActionById = { _kTapIndex: tap, @@ -81,7 +81,7 @@ class SemanticsAction { _kMoveCursorForwardByWordIndex: moveCursorForwardByWord, _kMoveCursorBackwardByWordIndex: moveCursorBackwardByWord, _kSetTextIndex: setText, - _kFocus: focus, + _kFocusIndex: focus, }; static List get values => _kActionById.values.toList(growable: false); diff --git a/shell/platform/embedder/embedder.h b/shell/platform/embedder/embedder.h index 4ba55543b3d31..81a45a0dda9f1 100644 --- a/shell/platform/embedder/embedder.h +++ b/shell/platform/embedder/embedder.h @@ -162,6 +162,7 @@ typedef enum { kFlutterSemanticsActionMoveCursorBackwardByWord = 1 << 20, /// Replace the current text in the text field. kFlutterSemanticsActionSetText = 1 << 21, + /// Request that the respective focusable widget gain keyboard input focus. kFlutterSemanticsActionFocus = 1 << 22, } FlutterSemanticsAction; From ac1da72275d1e3058042465e9ad29f0972c8cef8 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Wed, 29 May 2024 16:41:46 -0700 Subject: [PATCH 7/7] docs --- lib/ui/semantics.dart | 46 ++++++++++++++++++++++++++++++ shell/platform/embedder/embedder.h | 2 +- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index 7ab26af4daff7..18e17c49e72d8 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -157,6 +157,10 @@ class SemanticsAction { /// The accessibility focus is different from the input focus. The input focus /// is usually held by the element that currently responds to keyboard inputs. /// Accessibility focus and input focus can be held by two different nodes! + /// + /// See also: + /// + /// * [focus], which controls the input focus. static const SemanticsAction didGainAccessibilityFocus = SemanticsAction._(_kDidGainAccessibilityFocusIndex, 'didGainAccessibilityFocus'); /// Indicates that the node has lost accessibility focus. @@ -203,6 +207,48 @@ class SemanticsAction { /// movement should extend (or start) a selection. static const SemanticsAction moveCursorBackwardByWord = SemanticsAction._(_kMoveCursorBackwardByWordIndex, 'moveCursorBackwardByWord'); + /// Move the input focus to the respective widget. + /// + /// Most commonly, the input focus determines what widget will receive + /// keyboard input. Semantics nodes that can receive this action are expected + /// to have [SemanticsFlag.isFocusable] set. Examples of such focusable + /// widgets include buttons, checkboxes, switches, and text fields. + /// + /// Upon receiving this action, the corresponding widget must move input focus + /// to itself. Doing otherwise is likely to lead to a poor user experience, + /// such as user input routed to a wrong widget. Text fields in particular, + /// must immediately become editable, opening a virtual keyboard, if needed. + /// Buttons must respond to tap/click events from the keyboard. + /// + /// Focus behavior is specific to the platform and to the assistive technology + /// used. Typically on desktop operating systems, such as Windows, macOS, and + /// Linux, moving accessibility focus will also move the input focus. On + /// mobile it is more common for the accessibility focus to be detached from + /// the input focus. In order to synchronize the two, a user takes an explicit + /// action (e.g. double-tap to activate). Sometimes this behavior is + /// configurable. For example, VoiceOver on macOS can be configured in the + /// global OS user settings to either move the input focus together with the + /// VoiceOver focus, or to keep the two detached. For this reason, widgets + /// should not expect to receive [didGainAccessibilityFocus] and [focus] + /// actions to be reported in any particular combination or order. + /// + /// On the web, the DOM "focus" event is equivalent to + /// [SemanticsAction.focus]. Accessibility focus is not observable from within + /// the browser. Instead, the browser, based on the platform features and user + /// preferences, makes the determination on whether input focus should be + /// moved to an element and, if so, fires a DOM "focus" event. This event is + /// forwarded to the framework as [SemanticsAction.focus]. For this reason, on + /// the web, the engine never sends [didGainAccessibilityFocus]. + /// + /// On Android input focus is observable as `AccessibilityAction#ACTION_FOCUS` + /// and is separate from accessibility focus, which is observed as + /// `AccessibilityAction#ACTION_ACCESSIBILITY_FOCUS`. + /// + /// See also: + /// + /// * [didGainAccessibilityFocus], which informs the framework about + /// accessibility focus ring, such as the TalkBack (Android) and + /// VoiceOver (iOS), moving which does not move the input focus. static const SemanticsAction focus = SemanticsAction._(_kFocusIndex, 'focus'); /// The possible semantics actions. diff --git a/shell/platform/embedder/embedder.h b/shell/platform/embedder/embedder.h index 81a45a0dda9f1..069c813a8d2ed 100644 --- a/shell/platform/embedder/embedder.h +++ b/shell/platform/embedder/embedder.h @@ -162,7 +162,7 @@ typedef enum { kFlutterSemanticsActionMoveCursorBackwardByWord = 1 << 20, /// Replace the current text in the text field. kFlutterSemanticsActionSetText = 1 << 21, - /// Request that the respective focusable widget gain keyboard input focus. + /// Request that the respective focusable widget gain input focus. kFlutterSemanticsActionFocus = 1 << 22, } FlutterSemanticsAction;