Skip to content

Commit

Permalink
add SemanticsAction.focus (#53094)
Browse files Browse the repository at this point in the history
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 flutter/flutter#83809
  • Loading branch information
yjbanov authored May 30, 2024
1 parent 4af4cbd commit 0a3423a
Show file tree
Hide file tree
Showing 9 changed files with 69 additions and 6 deletions.
58 changes: 55 additions & 3 deletions lib/ui/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -228,6 +278,7 @@ class SemanticsAction {
_kMoveCursorForwardByWordIndex: moveCursorForwardByWord,
_kMoveCursorBackwardByWordIndex: moveCursorBackwardByWord,
_kSetTextIndex: setText,
_kFocusIndex: focus,
};

static List<SemanticsAction> get values => _kActionById.values.toList(growable: false);
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions lib/ui/semantics/semantics_node.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ enum class SemanticsAction : int32_t {
kMoveCursorForwardByWord = 1 << 19,
kMoveCursorBackwardByWord = 1 << 20,
kSetText = 1 << 21,
kFocus = 1 << 22,
};

const int kVerticalScrollSemanticsActions =
Expand Down
3 changes: 3 additions & 0 deletions lib/web_ui/lib/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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<int, SemanticsAction> _kActionById = <int, SemanticsAction>{
_kTapIndex: tap,
Expand All @@ -79,6 +81,7 @@ class SemanticsAction {
_kMoveCursorForwardByWordIndex: moveCursorForwardByWord,
_kMoveCursorBackwardByWordIndex: moveCursorBackwardByWord,
_kSetTextIndex: setText,
_kFocusIndex: focus,
};

static List<SemanticsAction> get values => _kActionById.values.toList(growable: false);
Expand Down
2 changes: 1 addition & 1 deletion lib/web_ui/test/engine/semantics/semantics_api_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 2 additions & 0 deletions shell/platform/embedder/embedder.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions shell/platform/fuchsia/flutter/accessibility_bridge.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions shell/platform/linux/fl_accessible_node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ static ActionData action_mapping[] = {
{kFlutterSemanticsActionMoveCursorForwardByWord, "MoveCursorForwardByWord"},
{kFlutterSemanticsActionMoveCursorBackwardByWord,
"MoveCursorBackwardByWord"},
{kFlutterSemanticsActionFocus, "Focus"},
{static_cast<FlutterSemanticsAction>(0), nullptr}};

struct FlAccessibleNodePrivate {
Expand Down
2 changes: 1 addition & 1 deletion testing/dart/semantics_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit 0a3423a

Please sign in to comment.