From 0a3423abd79c282c8a5e84a24e03733c98b31144 Mon Sep 17 00:00:00 2001 From: Yegor Date: Thu, 30 May 2024 10:15:53 -0700 Subject: [PATCH] add SemanticsAction.focus (#53094) Add `SemanticsAction.focus`. This PR just adds the new enum value without any logic. Adding the enum value first to unblock work that needs to be done on both the engine and framework side that will actually implement all the necessary logic. This is PR 1 out of ~3 for https://github.com/flutter/flutter/issues/83809 --- lib/ui/semantics.dart | 58 ++++++++++++++++++- lib/ui/semantics/semantics_node.h | 1 + lib/web_ui/lib/semantics.dart | 3 + .../engine/semantics/semantics_api_test.dart | 2 +- .../io/flutter/view/AccessibilityBridge.java | 3 +- shell/platform/embedder/embedder.h | 2 + .../fuchsia/flutter/accessibility_bridge.cc | 3 + shell/platform/linux/fl_accessible_node.cc | 1 + testing/dart/semantics_test.dart | 2 +- 9 files changed, 69 insertions(+), 6 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index e0da396d8ec58..18e17c49e72d8 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -44,8 +44,10 @@ class SemanticsAction { static const int _kMoveCursorForwardByWordIndex = 1 << 19; static const int _kMoveCursorBackwardByWordIndex = 1 << 20; static const int _kSetTextIndex = 1 << 21; + 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, 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 @@ -155,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. @@ -201,6 +207,50 @@ 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. /// /// The map's key is the [index] of the action and the value is the action @@ -228,6 +278,7 @@ class SemanticsAction { _kMoveCursorForwardByWordIndex: moveCursorForwardByWord, _kMoveCursorBackwardByWordIndex: moveCursorBackwardByWord, _kSetTextIndex: setText, + _kFocusIndex: focus, }; static List get values => _kActionById.values.toList(growable: false); @@ -285,8 +336,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/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..3656abd92375e 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 _kFocusIndex = 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._(_kFocusIndex, 'focus'); static const Map _kActionById = { _kTapIndex: tap, @@ -79,6 +81,7 @@ class SemanticsAction { _kMoveCursorForwardByWordIndex: moveCursorForwardByWord, _kMoveCursorBackwardByWordIndex: moveCursorBackwardByWord, _kSetTextIndex: setText, + _kFocusIndex: focus, }; static List get values => _kActionById.values.toList(growable: false); 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/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index a0280322c06f9..e6014f4329ca5 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -2102,7 +2102,8 @@ 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; diff --git a/shell/platform/embedder/embedder.h b/shell/platform/embedder/embedder.h index 41b455ce8df75..069c813a8d2ed 100644 --- a/shell/platform/embedder/embedder.h +++ b/shell/platform/embedder/embedder.h @@ -162,6 +162,8 @@ typedef enum { kFlutterSemanticsActionMoveCursorBackwardByWord = 1 << 20, /// Replace the current text in the text field. kFlutterSemanticsActionSetText = 1 << 21, + /// Request that the respective focusable widget gain input focus. + 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..962ff0cc354fa 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"}, + {kFlutterSemanticsActionFocus, "Focus"}, {static_cast(0), nullptr}}; struct FlAccessibleNodePrivate { diff --git a/testing/dart/semantics_test.dart b/testing/dart/semantics_test.dart index a995b4037fb87..24c5bd10e088f 100644 --- a/testing/dart/semantics_test.dart +++ b/testing/dart/semantics_test.dart @@ -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) {