Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/ui/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ class SemanticsFlag {
static const int _kHasImplicitScrollingIndex = 1 << 18;
static const int _kIsMultilineIndex = 1 << 19;
static const int _kIsReadOnlyIndex = 1 << 20;
static const int _kIsFocusableIndex = 1 << 21;

const SemanticsFlag._(this.index);

Expand Down Expand Up @@ -348,6 +349,11 @@ class SemanticsFlag {
/// Only applicable when [isTextField] is true.
static const SemanticsFlag isReadOnly = SemanticsFlag._(_kIsReadOnlyIndex);

/// Whether the semantic node is able to hold the user's focus.
///
/// The focused element is usually the current receiver of keyboard inputs.
static const SemanticsFlag isFocusable = SemanticsFlag._(_kIsFocusableIndex);

/// Whether the semantic node currently holds the user's focus.
///
/// The focused element is usually the current receiver of keyboard inputs.
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 @@ -72,6 +72,7 @@ enum class SemanticsFlags : int32_t {
// The Dart API defines the following flag but it isn't used in iOS.
// kIsMultiline = 1 << 19,
kIsReadOnly = 1 << 20,
kIsFocusable = 1 << 21,
};

const int kScrollableSemanticsFlags =
Expand Down
9 changes: 9 additions & 0 deletions lib/web_ui/lib/src/ui/semantics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ class SemanticsFlag {
static const int _kHasImplicitScrollingIndex = 1 << 18;
static const int _kIsMultilineIndex = 1 << 19;
static const int _kIsReadOnlyIndex = 1 << 20;
static const int _kIsFocusableIndex = 1 << 21;

const SemanticsFlag._(this.index);

Expand Down Expand Up @@ -351,6 +352,11 @@ class SemanticsFlag {
/// Only applicable when [isTextField] is true.
static const SemanticsFlag isReadOnly = SemanticsFlag._(_kIsReadOnlyIndex);

/// Whether the semantic node is able to hold the user's focus.
///
/// The focused element is usually the current receiver of keyboard inputs.
static const SemanticsFlag isFocusable = SemanticsFlag._(_kIsFocusableIndex);

/// Whether the semantic node currently holds the user's focus.
///
/// The focused element is usually the current receiver of keyboard inputs.
Expand Down Expand Up @@ -517,6 +523,7 @@ class SemanticsFlag {
_kIsSelectedIndex: isSelected,
_kIsButtonIndex: isButton,
_kIsTextFieldIndex: isTextField,
_kIsFocusableIndex: isFocusable,
_kIsFocusedIndex: isFocused,
_kHasEnabledStateIndex: hasEnabledState,
_kIsEnabledIndex: isEnabled,
Expand Down Expand Up @@ -548,6 +555,8 @@ class SemanticsFlag {
return 'SemanticsFlag.isButton';
case _kIsTextFieldIndex:
return 'SemanticsFlag.isTextField';
case _kIsFocusableIndex:
return 'SemanticsFlag.isFocusable';
case _kIsFocusedIndex:
return 'SemanticsFlag.isFocused';
case _kHasEnabledStateIndex:
Expand Down
46 changes: 37 additions & 9 deletions shell/platform/android/io/flutter/view/AccessibilityBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -1271,7 +1271,7 @@ void updateSemantics(@NonNull ByteBuffer buffer, @NonNull String[] strings) {
}
if (lastAdded != null && lastAdded.id != previousRouteId) {
previousRouteId = lastAdded.id;
createAndSendWindowChangeEvent(lastAdded);
sendWindowChangeEvent(lastAdded);
}
flutterNavigationStack.clear();
for (SemanticsNode semanticsNode : newRoutes) {
Expand All @@ -1290,7 +1290,7 @@ void updateSemantics(@NonNull ByteBuffer buffer, @NonNull String[] strings) {

// TODO(goderbauer): Send this event only once (!) for changed subtrees,
// see https://github.com/flutter/flutter/issues/14534
sendAccessibilityEvent(0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
sendWindowContentChangeEvent(0);

for (SemanticsNode object : updated) {
if (object.didScroll()) {
Expand Down Expand Up @@ -1362,13 +1362,13 @@ void updateSemantics(@NonNull ByteBuffer buffer, @NonNull String[] strings) {
String label = object.label == null ? "" : object.label;
String previousLabel = object.previousLabel == null ? "" : object.label;
if (!label.equals(previousLabel) || !object.hadFlag(Flag.IS_LIVE_REGION)) {
sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
sendWindowContentChangeEvent(object.id);
}
} else if (object.hasFlag(Flag.IS_TEXT_FIELD) && object.didChangeLabel()
&& inputFocusedSemanticsNode != null && inputFocusedSemanticsNode.id == object.id) {
// Text fields should announce when their label changes while focused. We use a live
// region tag to do so, and this event triggers that update.
sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
sendWindowContentChangeEvent(object.id);
}
if (accessibilityFocusedSemanticsNode != null && accessibilityFocusedSemanticsNode.id == object.id
&& !object.hadFlag(Flag.IS_SELECTED) && object.hasFlag(Flag.IS_SELECTED)) {
Expand Down Expand Up @@ -1472,13 +1472,13 @@ private void sendAccessibilityEvent(@NonNull AccessibilityEvent event) {
}

/**
* Factory method that creates a {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED} and sends
* the event to Android's accessibility system.
* Creates a {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED} and sends the event to
* Android's accessibility system.
*
* The given {@code route} should be a {@link SemanticsNode} that represents a navigation route
* in the Flutter app.
*/
private void createAndSendWindowChangeEvent(@NonNull SemanticsNode route) {
private void sendWindowChangeEvent(@NonNull SemanticsNode route) {
AccessibilityEvent event = obtainAccessibilityEvent(
route.id,
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
Expand All @@ -1488,6 +1488,27 @@ private void createAndSendWindowChangeEvent(@NonNull SemanticsNode route) {
sendAccessibilityEvent(event);
}

/**
* Creates a {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} and sends the event to
* Android's accessibility system.
*
* It sets the content change types to {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE}
* when supported by the API level.
*
* The given {@code virtualViewId} should be a {@link SemanticsNode} below which the content has
* changed.
*/
private void sendWindowContentChangeEvent(int virtualViewId) {
AccessibilityEvent event = obtainAccessibilityEvent(
virtualViewId,
AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
}
sendAccessibilityEvent(event);
}

/**
* Factory method that creates a new {@link AccessibilityEvent} that is configured to represent
* the Flutter {@link SemanticsNode} represented by the given {@code virtualViewId}, categorized
Expand Down Expand Up @@ -1559,7 +1580,7 @@ public void reset() {
}
accessibilityFocusedSemanticsNode = null;
hoveredObject = null;
sendAccessibilityEvent(0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
sendWindowContentChangeEvent(0);
}

/**
Expand Down Expand Up @@ -1626,7 +1647,8 @@ private enum Flag {
HAS_IMPLICIT_SCROLLING(1 << 18),
// The Dart API defines the following flag but it isn't used in Android.
// IS_MULTILINE(1 << 19);
IS_READ_ONLY(1 << 20);
IS_READ_ONLY(1 << 20),
IS_FOCUSABLE(1 << 21);

final int value;

Expand Down Expand Up @@ -2000,6 +2022,12 @@ private boolean isFocusable() {
if (hasFlag(Flag.SCOPES_ROUTE)) {
return false;
}
if (hasFlag(Flag.IS_FOCUSABLE)) {
return true;
}
// If not explicitly set as focusable, then use our legacy
// algorithm. Once all focusable widgets have a Focus widget, then
// this won't be needed.
int scrollableActions = Action.SCROLL_RIGHT.value | Action.SCROLL_LEFT.value
| Action.SCROLL_UP.value | Action.SCROLL_DOWN.value;
return (actions & ~scrollableActions) != 0 || flags != 0
Expand Down