Skip to content

Commit

Permalink
Add accessibility identifier to SemanticsProperties (flutter#138331)
Browse files Browse the repository at this point in the history
This PR adds `String? identifier` to `Semantics` and `SemanticsProperties`. The `identifier` will be exposed on Android as `resource-id` and on iOS as `accessibilityIdentifier`.

Mainly targeted at flutter#17988

Initial Engine PR with Android support: flutter/engine#47961
iOS Engine PR: flutter/engine#48858

### Migration

This change breaks the SemanticsUpdateBuilder API which is on the Framework<-->Engine border. For more details see [engine PR](flutter/engine#47961).

Steps:
part 1: [engine] add `SemanticsUpdateBuilderNew` flutter/engine#47961
**part 2: [flutter] use `SemanticsUpdateBuilderNew`**  <-- we are here
part 3: [engine] update `SemanticsUpdateBuilder` to be the same as `SemanticsUpdateBuilderNew`*
part 4: [flutter] use (now updated) `SemanticsUpdateBuilder` again.
part 5: [engine] remove `SemanticsBuilderNew`
  • Loading branch information
bartekpacia authored and Michal-MK committed Jan 8, 2024
1 parent ee8b0ce commit 5936783
Show file tree
Hide file tree
Showing 9 changed files with 93 additions and 12 deletions.
3 changes: 3 additions & 0 deletions packages/flutter/lib/src/rendering/proxy_box.dart
Expand Up @@ -4356,6 +4356,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
if (_properties.image != null) {
config.isImage = _properties.image!;
}
if (_properties.identifier != null) {
config.identifier = _properties.identifier!;
}
if (_attributedLabel != null) {
config.attributedLabel = _attributedLabel!;
}
Expand Down
12 changes: 8 additions & 4 deletions packages/flutter/lib/src/semantics/binding.dart
Expand Up @@ -2,14 +2,16 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:ui' as ui show AccessibilityFeatures, SemanticsActionEvent, SemanticsUpdateBuilder;
// ignore: deprecated_member_use
import 'dart:ui' as ui show AccessibilityFeatures, SemanticsActionEvent, SemanticsUpdateBuilderNew;

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

import 'debug.dart';

export 'dart:ui' show AccessibilityFeatures, SemanticsActionEvent, SemanticsUpdateBuilder;
// ignore: deprecated_member_use
export 'dart:ui' show AccessibilityFeatures, SemanticsActionEvent, SemanticsUpdateBuilderNew;

/// The glue between the semantics layer and the Flutter engine.
mixin SemanticsBinding on BindingBase {
Expand Down Expand Up @@ -160,8 +162,10 @@ mixin SemanticsBinding on BindingBase {
///
/// This method is used by the [SemanticsOwner] to create builder for all its
/// semantics updates.
ui.SemanticsUpdateBuilder createSemanticsUpdateBuilder() {
return ui.SemanticsUpdateBuilder();
// ignore: deprecated_member_use
ui.SemanticsUpdateBuilderNew createSemanticsUpdateBuilder() {
// ignore: deprecated_member_use
return ui.SemanticsUpdateBuilderNew();
}

/// The platform is requesting that animations be disabled or simplified.
Expand Down
59 changes: 55 additions & 4 deletions packages/flutter/lib/src/semantics/semantics.dart
Expand Up @@ -3,7 +3,8 @@
// found in the LICENSE file.

import 'dart:math' as math;
import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag, SemanticsUpdate, SemanticsUpdateBuilder, StringAttribute, TextDirection;
// ignore: deprecated_member_use
import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag, SemanticsUpdate, SemanticsUpdateBuilderNew, StringAttribute, TextDirection;

import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
Expand Down Expand Up @@ -426,6 +427,7 @@ class SemanticsData with Diagnosticable {
SemanticsData({
required this.flags,
required this.actions,
required this.identifier,
required this.attributedLabel,
required this.attributedValue,
required this.attributedIncreasedValue,
Expand Down Expand Up @@ -461,6 +463,9 @@ class SemanticsData with Diagnosticable {
/// A bit field of [SemanticsAction]s that apply to this node.
final int actions;

/// {@macro flutter.semantics.SemanticsProperties.identifier}
final String identifier;

/// A textual description for the current label of the node.
///
/// The reading direction is given by [textDirection].
Expand Down Expand Up @@ -696,6 +701,7 @@ class SemanticsData with Diagnosticable {
flag.name,
];
properties.add(IterableProperty<String>('flags', flagSummary, ifEmpty: null));
properties.add(StringProperty('identifier', identifier, defaultValue: ''));
properties.add(AttributedStringProperty('label', attributedLabel));
properties.add(AttributedStringProperty('value', attributedValue));
properties.add(AttributedStringProperty('increasedValue', attributedIncreasedValue));
Expand All @@ -721,6 +727,7 @@ class SemanticsData with Diagnosticable {
return other is SemanticsData
&& other.flags == flags
&& other.actions == actions
&& other.identifier == identifier
&& other.attributedLabel == attributedLabel
&& other.attributedValue == attributedValue
&& other.attributedIncreasedValue == attributedIncreasedValue
Expand Down Expand Up @@ -749,6 +756,7 @@ class SemanticsData with Diagnosticable {
int get hashCode => Object.hash(
flags,
actions,
identifier,
attributedLabel,
attributedValue,
attributedIncreasedValue,
Expand All @@ -765,8 +773,8 @@ class SemanticsData with Diagnosticable {
scrollExtentMax,
scrollExtentMin,
platformViewId,
maxValueLength,
Object.hash(
maxValueLength,
currentValueLength,
transform,
elevation,
Expand Down Expand Up @@ -901,6 +909,7 @@ class SemanticsProperties extends DiagnosticableTree {
this.liveRegion,
this.maxValueLength,
this.currentValueLength,
this.identifier,
this.label,
this.attributedLabel,
this.value,
Expand Down Expand Up @@ -1165,6 +1174,21 @@ class SemanticsProperties extends DiagnosticableTree {
/// [maxValueLength] is set.
final int? currentValueLength;

/// {@template flutter.semantics.SemanticsProperties.identifier}
/// Provides an identifier for the semantics node in native accessibility hierarchy.
///
/// This value is not exposed to the users of the app.
///
/// It's usually used for UI testing with tools that work by querying the
/// native accessibility, like UIAutomator, XCUITest, or Appium.
///
/// On Android, this is used for `AccessibilityNodeInfo.setViewIdResourceName`.
/// It'll be appear in accessibility hierarchy as `resource-id`.
///
/// On iOS, this will set `UIAccessibilityElement.accessibilityIdentifier`.
/// {@endtemplate}
final String? identifier;

/// Provides a textual description of the widget.
///
/// If a label is provided, there must either by an ambient [Directionality]
Expand Down Expand Up @@ -1632,6 +1656,7 @@ class SemanticsProperties extends DiagnosticableTree {
properties.add(DiagnosticsProperty<bool>('mixed', mixed, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('expanded', expanded, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('selected', selected, defaultValue: null));
properties.add(StringProperty('identifier', identifier));
properties.add(StringProperty('label', label, defaultValue: null));
properties.add(AttributedStringProperty('attributedLabel', attributedLabel, defaultValue: null));
properties.add(StringProperty('value', value, defaultValue: null));
Expand Down Expand Up @@ -2210,6 +2235,10 @@ class SemanticsNode with DiagnosticableTreeMixin {
/// Whether this node currently has a given [SemanticsFlag].
bool hasFlag(SemanticsFlag flag) => _flags & flag.index != 0;

/// {@macro flutter.semantics.SemanticsProperties.identifier}
String get identifier => _identifier;
String _identifier = _kEmptyConfig.identifier;

/// A textual description of this node.
///
/// The reading direction is given by [textDirection].
Expand Down Expand Up @@ -2514,6 +2543,7 @@ class SemanticsNode with DiagnosticableTreeMixin {

final bool mergeAllDescendantsIntoThisNodeValueChanged = _mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;

_identifier = config.identifier;
_attributedLabel = config.attributedLabel;
_attributedValue = config.attributedValue;
_attributedIncreasedValue = config.attributedIncreasedValue;
Expand Down Expand Up @@ -2569,6 +2599,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
// Can't use _effectiveActionsAsBits here. The filtering of action bits
// must be done after the merging the its descendants.
int actions = _actionsAsBits;
String identifier = _identifier;
AttributedString attributedLabel = _attributedLabel;
AttributedString attributedValue = _attributedValue;
AttributedString attributedIncreasedValue = _attributedIncreasedValue;
Expand Down Expand Up @@ -2625,6 +2656,9 @@ class SemanticsNode with DiagnosticableTreeMixin {
platformViewId ??= node._platformViewId;
maxValueLength ??= node._maxValueLength;
currentValueLength ??= node._currentValueLength;
if (identifier == '') {
identifier = node._identifier;
}
if (attributedValue.string == '') {
attributedValue = node._attributedValue;
}
Expand Down Expand Up @@ -2682,6 +2716,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
return SemanticsData(
flags: flags,
actions: _areUserActionsBlocked ? actions & _kUnblockedUserActions : actions,
identifier: identifier,
attributedLabel: attributedLabel,
attributedValue: attributedValue,
attributedIncreasedValue: attributedIncreasedValue,
Expand Down Expand Up @@ -2715,7 +2750,8 @@ class SemanticsNode with DiagnosticableTreeMixin {
static final Int32List _kEmptyCustomSemanticsActionsList = Int32List(0);
static final Float64List _kIdentityTransform = _initIdentityTransform();

void _addToUpdate(SemanticsUpdateBuilder builder, Set<int> customSemanticsActionIdsUpdate) {
// ignore: deprecated_member_use
void _addToUpdate(SemanticsUpdateBuilderNew builder, Set<int> customSemanticsActionIdsUpdate) {
assert(_dirty);
final SemanticsData data = getSemanticsData();
final Int32List childrenInTraversalOrder;
Expand Down Expand Up @@ -2750,6 +2786,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
flags: data.flags,
actions: data.actions,
rect: data.rect,
identifier: data.identifier,
label: data.attributedLabel.string,
labelAttributes: data.attributedLabel.attributes,
value: data.attributedValue.string,
Expand Down Expand Up @@ -2904,6 +2941,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
properties.add(IterableProperty<String>('flags', flags, ifEmpty: null));
properties.add(FlagProperty('isInvisible', value: isInvisible, ifTrue: 'invisible'));
properties.add(FlagProperty('isHidden', value: hasFlag(SemanticsFlag.isHidden), ifTrue: 'HIDDEN'));
properties.add(StringProperty('identifier', _identifier, defaultValue: ''));
properties.add(AttributedStringProperty('label', _attributedLabel));
properties.add(AttributedStringProperty('value', _attributedValue));
properties.add(AttributedStringProperty('increasedValue', _attributedIncreasedValue));
Expand Down Expand Up @@ -3406,7 +3444,8 @@ class SemanticsOwner extends ChangeNotifier {
}
}
visitedNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
final SemanticsUpdateBuilder builder = SemanticsBinding.instance.createSemanticsUpdateBuilder();
// ignore: deprecated_member_use
final SemanticsUpdateBuilderNew builder = SemanticsBinding.instance.createSemanticsUpdateBuilder();
for (final SemanticsNode node in visitedNodes) {
assert(node.parent?._dirty != true); // could be null (no parent) or false (not dirty)
// The _serialize() method marks the node as not dirty, and
Expand Down Expand Up @@ -4201,6 +4240,14 @@ class SemanticsConfiguration {
}
}

/// {@macro flutter.semantics.SemanticsProperties.identifier}
String get identifier => _identifier;
String _identifier = '';
set identifier(String identifier) {
_identifier = identifier;
_hasBeenAnnotated = true;
}

/// A textual description of the owning [RenderObject].
///
/// Setting this attribute will override the [attributedLabel].
Expand Down Expand Up @@ -4898,6 +4945,9 @@ class SemanticsConfiguration {

textDirection ??= child.textDirection;
_sortKey ??= child._sortKey;
if (_identifier == '') {
_identifier = child._identifier;
}
_attributedLabel = _concatAttributedString(
thisAttributedString: _attributedLabel,
thisTextDirection: textDirection,
Expand Down Expand Up @@ -4938,6 +4988,7 @@ class SemanticsConfiguration {
.._isMergingSemanticsOfDescendants = _isMergingSemanticsOfDescendants
.._textDirection = _textDirection
.._sortKey = _sortKey
.._identifier = _identifier
.._attributedLabel = _attributedLabel
.._attributedIncreasedValue = _attributedIncreasedValue
.._attributedValue = _attributedValue
Expand Down
2 changes: 2 additions & 0 deletions packages/flutter/lib/src/widgets/basic.dart
Expand Up @@ -7123,6 +7123,7 @@ class Semantics extends SingleChildRenderObjectWidget {
bool? expanded,
int? maxValueLength,
int? currentValueLength,
String? identifier,
String? label,
AttributedString? attributedLabel,
String? value,
Expand Down Expand Up @@ -7191,6 +7192,7 @@ class Semantics extends SingleChildRenderObjectWidget {
liveRegion: liveRegion,
maxValueLength: maxValueLength,
currentValueLength: currentValueLength,
identifier: identifier,
label: label,
attributedLabel: attributedLabel,
value: value,
Expand Down
2 changes: 2 additions & 0 deletions packages/flutter/test/semantics/semantics_test.dart
Expand Up @@ -682,6 +682,7 @@ void main() {
' flags: []\n'
' invisible\n'
' isHidden: false\n'
' identifier: ""\n'
' label: ""\n'
' value: ""\n'
' increasedValue: ""\n'
Expand Down Expand Up @@ -805,6 +806,7 @@ void main() {
' flags: []\n'
' invisible\n'
' isHidden: false\n'
' identifier: ""\n'
' label: ""\n'
' value: ""\n'
' increasedValue: ""\n'
Expand Down
11 changes: 8 additions & 3 deletions packages/flutter/test/semantics/semantics_update_test.dart
Expand Up @@ -157,6 +157,7 @@ void main() {
'Semantics('
'container: false, '
'properties: SemanticsProperties, '
'identifier: null, '// ignore: missing_whitespace_between_adjacent_strings
'attributedLabel: "label" [SpellOutStringAttribute(TextRange(start: 0, end: 5))], '
'attributedValue: "value" [LocaleStringAttribute(TextRange(start: 0, end: 5), en-MX)], '
'attributedHint: "hint" [SpellOutStringAttribute(TextRange(start: 1, end: 2))], '
Expand All @@ -171,13 +172,16 @@ void main() {

class SemanticsUpdateTestBinding extends AutomatedTestWidgetsFlutterBinding {
@override
ui.SemanticsUpdateBuilder createSemanticsUpdateBuilder() {
// ignore: deprecated_member_use
ui.SemanticsUpdateBuilderNew createSemanticsUpdateBuilder() {
return SemanticsUpdateBuilderSpy();
}
}

class SemanticsUpdateBuilderSpy extends Fake implements ui.SemanticsUpdateBuilder {
final SemanticsUpdateBuilder _builder = ui.SemanticsUpdateBuilder();
// ignore: deprecated_member_use
class SemanticsUpdateBuilderSpy extends Fake implements ui.SemanticsUpdateBuilderNew {
// ignore: deprecated_member_use
final SemanticsUpdateBuilderNew _builder = ui.SemanticsUpdateBuilderNew();

static Map<int, SemanticsNodeUpdateObservation> observations = <int, SemanticsNodeUpdateObservation>{};

Expand All @@ -199,6 +203,7 @@ class SemanticsUpdateBuilderSpy extends Fake implements ui.SemanticsUpdateBuilde
required double elevation,
required double thickness,
required Rect rect,
required String identifier,
required String label,
List<StringAttribute>? labelAttributes,
required String value,
Expand Down
6 changes: 6 additions & 0 deletions packages/flutter_test/lib/src/matchers.dart
Expand Up @@ -624,6 +624,7 @@ AsyncMatcher matchesReferenceImage(ui.Image image) {
/// * [SemanticsController.find] under [WidgetTester.semantics], the tester method which retrieves semantics.
/// * [containsSemantics], a similar matcher without default values for flags or actions.
Matcher matchesSemantics({
String? identifier,
String? label,
AttributedString? attributedLabel,
String? hint,
Expand Down Expand Up @@ -701,6 +702,7 @@ Matcher matchesSemantics({
List<Matcher>? children,
}) {
return _MatchesSemanticsData(
identifier: identifier,
label: label,
attributedLabel: attributedLabel,
hint: hint,
Expand Down Expand Up @@ -808,6 +810,7 @@ Matcher matchesSemantics({
/// * [SemanticsController.find] under [WidgetTester.semantics], the tester method which retrieves semantics.
/// * [matchesSemantics], a similar matcher with default values for flags and actions.
Matcher containsSemantics({
String? identifier,
String? label,
AttributedString? attributedLabel,
String? hint,
Expand Down Expand Up @@ -885,6 +888,7 @@ Matcher containsSemantics({
List<Matcher>? children,
}) {
return _MatchesSemanticsData(
identifier: identifier,
label: label,
attributedLabel: attributedLabel,
hint: hint,
Expand Down Expand Up @@ -2207,6 +2211,7 @@ class _MatchesReferenceImage extends AsyncMatcher {

class _MatchesSemanticsData extends Matcher {
_MatchesSemanticsData({
required this.identifier,
required this.label,
required this.attributedLabel,
required this.hint,
Expand Down Expand Up @@ -2344,6 +2349,7 @@ class _MatchesSemanticsData extends Matcher {
onLongPressHint: onLongPressHint,
);

final String? identifier;
final String? label;
final AttributedString? attributedLabel;
final String? hint;
Expand Down

0 comments on commit 5936783

Please sign in to comment.