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) {