Reland "Refactor OverlayPortal semantics (#173005)" (#178095)

Reverts flutter/flutter#178007

This PR is to reland https://github.com/flutter/flutter/pull/173005 and
add a fix to avoid infinite loop. The fix doesn't contain engine
changes.
This commit is contained in:
Qun Cheng 2025-11-10 16:35:40 -08:00 committed by GitHub
parent c99db0dc0d
commit fccfa978a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 1678 additions and 515 deletions

View File

@ -176,6 +176,7 @@ void sendSemanticsUpdate() {
String tooltip = "tooltip";
final Float64List transform = Float64List(16);
final Float64List hitTestTransform = Float64List(16);
final Int32List childrenInTraversalOrder = Int32List(0);
final Int32List childrenInHitTestOrder = Int32List(0);
final Int32List additionalActions = Int32List(0);
@ -198,6 +199,26 @@ void sendSemanticsUpdate() {
transform[13] = 0;
transform[14] = 0;
transform[15] = 0;
hitTestTransform[0] = 1;
hitTestTransform[1] = 0;
hitTestTransform[2] = 0;
hitTestTransform[3] = 0;
hitTestTransform[4] = 0;
hitTestTransform[5] = 1;
hitTestTransform[6] = 0;
hitTestTransform[7] = 0;
hitTestTransform[8] = 0;
hitTestTransform[9] = 0;
hitTestTransform[10] = 1;
hitTestTransform[11] = 0;
hitTestTransform[12] = 0;
hitTestTransform[13] = 0;
hitTestTransform[14] = 0;
hitTestTransform[15] = 0;
builder.updateNode(
id: 0,
flags: SemanticsFlags.none,
@ -209,6 +230,7 @@ void sendSemanticsUpdate() {
platformViewId: -1,
scrollChildren: 0,
scrollIndex: 0,
traversalParent: 0,
scrollPosition: 0,
scrollExtentMax: 0,
scrollExtentMin: 0,
@ -227,6 +249,7 @@ void sendSemanticsUpdate() {
tooltip: tooltip,
textDirection: TextDirection.ltr,
transform: transform,
hitTestTransform: hitTestTransform,
childrenInTraversalOrder: childrenInTraversalOrder,
childrenInHitTestOrder: childrenInHitTestOrder,
additionalActions: additionalActions,
@ -244,6 +267,7 @@ void sendSemanticsUpdateWithRole() {
final SemanticsUpdateBuilder builder = SemanticsUpdateBuilder();
final Float64List transform = Float64List(16);
final Float64List hitTestTransform = Float64List(16);
final Int32List childrenInTraversalOrder = Int32List(0);
final Int32List childrenInHitTestOrder = Int32List(0);
final Int32List additionalActions = Int32List(0);
@ -251,6 +275,10 @@ void sendSemanticsUpdateWithRole() {
transform[0] = 1;
transform[5] = 1;
transform[10] = 1;
hitTestTransform[0] = 1;
hitTestTransform[5] = 1;
hitTestTransform[10] = 1;
builder.updateNode(
id: 0,
flags: SemanticsFlags.none,
@ -262,6 +290,7 @@ void sendSemanticsUpdateWithRole() {
platformViewId: -1,
scrollChildren: 0,
scrollIndex: 0,
traversalParent: 0,
scrollPosition: 0,
scrollExtentMax: 0,
scrollExtentMin: 0,
@ -280,6 +309,7 @@ void sendSemanticsUpdateWithRole() {
tooltip: "tooltip",
textDirection: TextDirection.ltr,
transform: transform,
hitTestTransform: hitTestTransform,
childrenInTraversalOrder: childrenInTraversalOrder,
childrenInHitTestOrder: childrenInHitTestOrder,
additionalActions: additionalActions,
@ -298,6 +328,7 @@ void sendSemanticsUpdateWithLocale() {
final SemanticsUpdateBuilder builder = SemanticsUpdateBuilder();
final Float64List transform = Float64List(16);
final Float64List hitTestTransform = Float64List(16);
final Int32List childrenInTraversalOrder = Int32List(0);
final Int32List childrenInHitTestOrder = Int32List(0);
final Int32List additionalActions = Int32List(0);
@ -305,6 +336,10 @@ void sendSemanticsUpdateWithLocale() {
transform[0] = 1;
transform[5] = 1;
transform[10] = 1;
hitTestTransform[0] = 1;
hitTestTransform[5] = 1;
hitTestTransform[10] = 1;
builder.updateNode(
id: 0,
flags: SemanticsFlags.none,
@ -319,6 +354,7 @@ void sendSemanticsUpdateWithLocale() {
scrollPosition: 0,
scrollExtentMax: 0,
scrollExtentMin: 0,
traversalParent: 0,
rect: Rect.fromLTRB(0, 0, 10, 10),
identifier: "identifier",
label: "label",
@ -334,6 +370,7 @@ void sendSemanticsUpdateWithLocale() {
tooltip: "tooltip",
textDirection: TextDirection.ltr,
transform: transform,
hitTestTransform: hitTestTransform,
childrenInTraversalOrder: childrenInTraversalOrder,
childrenInHitTestOrder: childrenInHitTestOrder,
additionalActions: additionalActions,
@ -370,6 +407,7 @@ void sendSemanticsUpdateWithIsLink() {
platformViewId: -1,
scrollChildren: 0,
scrollIndex: 0,
traversalParent: 0,
scrollPosition: 0,
scrollExtentMax: 0,
scrollExtentMin: 0,
@ -388,6 +426,7 @@ void sendSemanticsUpdateWithIsLink() {
tooltip: "tooltip",
textDirection: TextDirection.ltr,
transform: transform,
hitTestTransform: transform,
childrenInTraversalOrder: childrenInTraversalOrder,
childrenInHitTestOrder: childrenInHitTestOrder,
additionalActions: additionalActions,

View File

@ -1871,6 +1871,21 @@ abstract class SemanticsUpdateBuilder {
/// total number of child nodes that contribute semantics and `scrollIndex`
/// is the index of the first visible child node that contributes semantics.
///
/// The `traversalParent` specifies the ID of the semantics node that serves as
/// the logical parent of this node for accessibility traversal. This
/// parameter is only used by the web engine to establish parent-child
/// relationships between nodes that are not directly connected in paint order.
/// To ensure correct accessibility traversal, `traversalParent` should be set
/// to the logical traversal parent node ID. This parameter is web-specific
/// because other platforms can complete grafting when generating the
/// semantics tree in traversal order. After grafting, the traversal order and
/// hit-test order will be different, which is acceptable for other platforms.
/// However, the web engine assumes these two orders are exactly the same, so
/// grafting cannot be performed ahead of time on web. Instead, the traversal
/// order is updated in the web engine by setting the `aria-owns` attribute
/// through this parameter. A value of -1 indicates no special traversal
/// parent. This parameter has no effect on other platforms.
///
/// The `rect` is the region occupied by this node in its own coordinate
/// system.
///
@ -1929,6 +1944,7 @@ abstract class SemanticsUpdateBuilder {
required int platformViewId,
required int scrollChildren,
required int scrollIndex,
required int traversalParent,
required double scrollPosition,
required double scrollExtentMax,
required double scrollExtentMin,
@ -1947,6 +1963,7 @@ abstract class SemanticsUpdateBuilder {
required String tooltip,
required TextDirection? textDirection,
required Float64List transform,
required Float64List hitTestTransform,
required Int32List childrenInTraversalOrder,
required Int32List childrenInHitTestOrder,
required Int32List additionalActions,
@ -2008,6 +2025,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
required int platformViewId,
required int scrollChildren,
required int scrollIndex,
required int traversalParent,
required double scrollPosition,
required double scrollExtentMax,
required double scrollExtentMin,
@ -2026,6 +2044,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
required String tooltip,
required TextDirection? textDirection,
required Float64List transform,
required Float64List hitTestTransform,
required Int32List childrenInTraversalOrder,
required Int32List childrenInHitTestOrder,
required Int32List additionalActions,
@ -2054,6 +2073,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
platformViewId,
scrollChildren,
scrollIndex,
traversalParent,
scrollPosition,
scrollExtentMax,
scrollExtentMin,
@ -2075,6 +2095,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
tooltip,
textDirection != null ? textDirection.index + 1 : 0,
transform,
hitTestTransform,
childrenInTraversalOrder,
childrenInHitTestOrder,
additionalActions,
@ -2102,6 +2123,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
Int32,
Int32,
Int32,
Int32,
Double,
Double,
Double,
@ -2126,6 +2148,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
Handle,
Handle,
Handle,
Handle,
Int32,
Handle,
Int32,
@ -2147,6 +2170,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
int platformViewId,
int scrollChildren,
int scrollIndex,
int traversalParent,
double scrollPosition,
double scrollExtentMax,
double scrollExtentMin,
@ -2168,6 +2192,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
String tooltip,
int textDirection,
Float64List transform,
Float64List hitTestTransform,
Int32List childrenInTraversalOrder,
Int32List childrenInHitTestOrder,
Int32List additionalActions,

View File

@ -141,6 +141,7 @@ struct SemanticsNode {
int32_t platformViewId = -1;
int32_t scrollChildren = 0;
int32_t scrollIndex = 0;
int32_t traversalParent = 0;
double scrollPosition = std::nan("");
double scrollExtentMax = std::nan("");
double scrollExtentMin = std::nan("");
@ -160,6 +161,7 @@ struct SemanticsNode {
SkRect rect = SkRect::MakeEmpty(); // Local space, relative to parent.
SkM44 transform = SkM44{}; // Identity
SkM44 hitTestTransform = SkM44{}; // Identity
std::vector<int32_t> childrenInTraversalOrder;
std::vector<int32_t> childrenInHitTestOrder;
std::vector<int32_t> customAccessibilityActions;

View File

@ -41,6 +41,7 @@ void SemanticsUpdateBuilder::updateNode(
int platformViewId,
int scrollChildren,
int scrollIndex,
int traversalParent,
double scrollPosition,
double scrollExtentMax,
double scrollExtentMin,
@ -62,6 +63,7 @@ void SemanticsUpdateBuilder::updateNode(
std::string tooltip,
int textDirection,
const tonic::Float64List& transform,
const tonic::Float64List& hitTestTransform,
const tonic::Int32List& childrenInTraversalOrder,
const tonic::Int32List& childrenInHitTestOrder,
const tonic::Int32List& localContextActions,
@ -90,6 +92,7 @@ void SemanticsUpdateBuilder::updateNode(
node.platformViewId = platformViewId;
node.scrollChildren = scrollChildren;
node.scrollIndex = scrollIndex;
node.traversalParent = traversalParent;
node.scrollPosition = scrollPosition;
node.scrollExtentMax = scrollExtentMax;
node.scrollExtentMin = scrollExtentMin;
@ -113,6 +116,11 @@ void SemanticsUpdateBuilder::updateNode(
scalarTransform[i] = SafeNarrow(transform.data()[i]);
}
node.transform = SkM44::ColMajor(scalarTransform);
SkScalar scalarHitTestTransform[16];
for (int i = 0; i < 16; ++i) {
scalarHitTestTransform[i] = SafeNarrow(hitTestTransform.data()[i]);
}
node.hitTestTransform = SkM44::ColMajor(scalarHitTestTransform);
node.childrenInTraversalOrder =
std::vector<int32_t>(childrenInTraversalOrder.data(),
childrenInTraversalOrder.data() +

View File

@ -40,6 +40,7 @@ class SemanticsUpdateBuilder
int platformViewId,
int scrollChildren,
int scrollIndex,
int traversalParent,
double scrollPosition,
double scrollExtentMax,
double scrollExtentMin,
@ -61,6 +62,7 @@ class SemanticsUpdateBuilder
std::string tooltip,
int textDirection,
const tonic::Float64List& transform,
const tonic::Float64List& hitTestTransform,
const tonic::Int32List& childrenInTraversalOrder,
const tonic::Int32List& childrenInHitTestOrder,
const tonic::Int32List& customAccessibilityActions,

View File

@ -722,6 +722,7 @@ class SemanticsUpdateBuilder {
required int platformViewId,
required int scrollChildren,
required int scrollIndex,
required int? traversalParent,
required double scrollPosition,
required double scrollExtentMax,
required double scrollExtentMin,
@ -740,6 +741,7 @@ class SemanticsUpdateBuilder {
String? tooltip,
TextDirection? textDirection,
required Float64List transform,
required Float64List hitTestTransform,
required Int32List childrenInTraversalOrder,
required Int32List childrenInHitTestOrder,
required Int32List additionalActions,
@ -766,6 +768,7 @@ class SemanticsUpdateBuilder {
textSelectionExtent: textSelectionExtent,
scrollChildren: scrollChildren,
scrollIndex: scrollIndex,
traversalParent: traversalParent,
scrollPosition: scrollPosition,
scrollExtentMax: scrollExtentMax,
scrollExtentMin: scrollExtentMin,
@ -784,6 +787,7 @@ class SemanticsUpdateBuilder {
tooltip: tooltip,
textDirection: textDirection,
transform: engine.toMatrix32(transform),
hitTestTransform: engine.toMatrix32(hitTestTransform),
childrenInTraversalOrder: childrenInTraversalOrder,
childrenInHitTestOrder: childrenInHitTestOrder,
additionalActions: additionalActions,

View File

@ -244,6 +244,7 @@ class SemanticsNodeUpdate {
required this.platformViewId,
required this.scrollChildren,
required this.scrollIndex,
required this.traversalParent,
required this.scrollPosition,
required this.scrollExtentMax,
required this.scrollExtentMin,
@ -262,6 +263,7 @@ class SemanticsNodeUpdate {
this.tooltip,
this.textDirection,
required this.transform,
required this.hitTestTransform,
required this.childrenInTraversalOrder,
required this.childrenInHitTestOrder,
required this.additionalActions,
@ -305,6 +307,9 @@ class SemanticsNodeUpdate {
/// See [ui.SemanticsUpdateBuilder.updateNode].
final int scrollIndex;
/// See [ui.SemanticsUpdateBuilder.updateNode].
final int? traversalParent;
/// See [ui.SemanticsUpdateBuilder.updateNode].
final double scrollPosition;
@ -359,6 +364,9 @@ class SemanticsNodeUpdate {
/// See [ui.SemanticsUpdateBuilder.updateNode].
final Float32List transform;
/// See [ui.SemanticsUpdateBuilder.updateNode].
final Float32List hitTestTransform;
/// See [ui.SemanticsUpdateBuilder.updateNode].
final Int32List childrenInTraversalOrder;
@ -640,7 +648,7 @@ abstract class SemanticRole {
element.style
..position = 'absolute'
..overflow = 'visible';
element.setAttribute('id', '$kFlutterSemanticNodePrefix${semanticsObject.id}');
element.setAttribute('id', getIdAttribute(semanticsObject.id));
// The root node has some properties that other nodes do not.
if (semanticsObject.id == 0 && !configuration.debugShowSemanticsNodes) {
@ -716,6 +724,11 @@ abstract class SemanticRole {
Focusable? get focusable => _focusable;
Focusable? _focusable;
/// Convenience method to get the node id with prefix.
static String getIdAttribute(int semanticsId) {
return '$kFlutterSemanticNodePrefix$semanticsId';
}
/// Adds generic focus management features.
void addFocusManagement() {
addSemanticBehavior(_focusable = Focusable(semanticsObject, this));
@ -824,6 +837,10 @@ abstract class SemanticRole {
if (semanticsObject.isLocaleDirty) {
semanticsObject.owner.addOneTimePostUpdateCallback(_updateLocale);
}
if (semanticsObject.isTraversalParentDirty) {
semanticsObject.owner.addOneTimePostUpdateCallback(_updateTraversalParent);
}
}
void _updateIdentifier() {
@ -843,7 +860,7 @@ abstract class SemanticRole {
if (semanticNodeId == null) {
continue;
}
elementIds.add('$kFlutterSemanticNodePrefix$semanticNodeId');
elementIds.add(getIdAttribute(semanticNodeId));
}
if (elementIds.isNotEmpty) {
setAttribute('aria-controls', elementIds.join(' '));
@ -864,6 +881,32 @@ abstract class SemanticRole {
setAttribute('lang', locale);
}
void _updateTraversalParent() {
// Set up aria-owns relationship for traversal order.
if (semanticsObject.traversalParent != -1) {
final SemanticsObject? parent =
semanticsObject.owner._semanticsTree[semanticsObject.traversalParent!];
if (parent != null && parent.semanticRole != null) {
final List<String> children = parent.element.getAttribute('aria-owns')?.split(' ') ?? [];
children.add(getIdAttribute(semanticsObject.id));
parent.element.setAttribute('aria-owns', children.join(' '));
}
}
// Clean up aria-owns relationship.
else if (semanticsObject._previousTraversalParent != null &&
semanticsObject._previousTraversalParent != -1) {
final SemanticsObject? parent =
semanticsObject.owner._semanticsTree[semanticsObject._previousTraversalParent!];
if (parent != null) {
final List<String>? children = parent.element.getAttribute('aria-owns')?.split(' ');
if (children != null) {
children.removeWhere((String child) => child == getIdAttribute(semanticsObject.id));
parent.element.setAttribute('aria-owns', children.join(' '));
}
}
}
}
/// Applies the current [SemanticsObject.validationResult] to the DOM managed
/// by this role.
///
@ -1547,6 +1590,20 @@ class SemanticsObject {
_dirtyFields |= _localeIndex;
}
/// See [ui.SemanticsUpdateBuilder.updateNode].
int? get traversalParent => _traversalParent;
int? _traversalParent;
int? _previousTraversalParent;
static const int _traversalParentIndex = 1 << 29;
/// Whether the [traversalParent] field has been updated but has not been
/// applied to the DOM yet.
bool get isTraversalParentDirty => _isDirty(_traversalParentIndex);
void _markTraversalParentDirty() {
_dirtyFields |= _traversalParentIndex;
}
/// Bitfield showing which fields have been updated but have not yet been
/// applied to the DOM.
///
@ -1734,6 +1791,12 @@ class SemanticsObject {
_markScrollIndexDirty();
}
if (_traversalParent != update.traversalParent) {
_previousTraversalParent = _traversalParent;
_traversalParent = update.traversalParent;
_markTraversalParentDirty();
}
if (_scrollExtentMax != update.scrollExtentMax) {
_scrollExtentMax = update.scrollExtentMax;
_markScrollExtentMaxDirty();

View File

@ -145,6 +145,9 @@ void runSemanticsTests() {
group('requirable', () {
_testRequirable();
});
group('traversalOrder', () {
_testSemanticsTraversalOrder();
});
group('SemanticsValidationResult', () {
_testSemanticsValidationResult();
});
@ -5528,6 +5531,152 @@ void _testRequirable() {
});
}
void _testSemanticsTraversalOrder() {
test('aria-owns is correctly set', () async {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
final tester = SemanticsTester(owner());
tester.updateNode(
id: 0,
rect: const ui.Rect.fromLTRB(0, 0, 100, 60),
children: <SemanticsNodeUpdate>[
tester.updateNode(
id: 1,
rect: const ui.Rect.fromLTRB(0, 0, 100, 20),
children: <SemanticsNodeUpdate>[
tester.updateNode(
id: 4,
children: <SemanticsNodeUpdate>[tester.updateNode(id: 6), tester.updateNode(id: 7)],
),
tester.updateNode(id: 5),
],
),
tester.updateNode(id: 2, rect: const ui.Rect.fromLTRB(0, 20, 100, 40), traversalParent: 4),
tester.updateNode(id: 3, rect: const ui.Rect.fromLTRB(0, 40, 100, 60)),
],
);
tester.apply();
expectSemanticsTree(owner(), '''
<sem>
<sem>
<sem id="flt-semantic-node-4" aria-owns="flt-semantic-node-2">
<sem></sem>
<sem></sem>
</sem>
<sem></sem>
</sem>
<sem id="flt-semantic-node-2"></sem>
<sem></sem>
</sem>
''');
final SemanticsObject node4 = tester.getSemanticsObject(4);
expect(node4.element.getAttribute('aria-owns'), 'flt-semantic-node-2');
});
test('aria-owns is correctly set with nested traversalParent relationship', () async {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
final tester = SemanticsTester(owner());
tester.updateNode(
id: 0,
rect: const ui.Rect.fromLTRB(0, 0, 100, 60),
children: <SemanticsNodeUpdate>[
tester.updateNode(
id: 1,
rect: const ui.Rect.fromLTRB(0, 0, 100, 20),
children: <SemanticsNodeUpdate>[
tester.updateNode(
id: 4,
children: <SemanticsNodeUpdate>[tester.updateNode(id: 6), tester.updateNode(id: 7)],
),
tester.updateNode(id: 5),
],
),
tester.updateNode(id: 2, rect: const ui.Rect.fromLTRB(0, 20, 100, 40), traversalParent: 4),
tester.updateNode(id: 3, rect: const ui.Rect.fromLTRB(0, 40, 100, 60)),
tester.updateNode(id: 8, rect: const ui.Rect.fromLTRB(0, 40, 100, 60), traversalParent: 6),
],
);
tester.apply();
expectSemanticsTree(owner(), '''
<sem>
<sem>
<sem id="flt-semantic-node-4" aria-owns="flt-semantic-node-2">
<sem id="flt-semantic-node-6" aria-owns="flt-semantic-node-8"></sem>
<sem></sem>
</sem>
<sem></sem>
</sem>
<sem id="flt-semantic-node-2"></sem>
<sem></sem>
<sem id="flt-semantic-node-8"></sem>
</sem>
''');
final SemanticsObject node4 = tester.getSemanticsObject(4);
expect(node4.element.getAttribute('aria-owns'), 'flt-semantic-node-2');
final SemanticsObject node6 = tester.getSemanticsObject(6);
expect(node6.element.getAttribute('aria-owns'), 'flt-semantic-node-8');
});
test('aria-owns is correctly set when one traversalParent has multiple children', () async {
semantics()
..debugOverrideTimestampFunction(() => _testTime)
..semanticsEnabled = true;
final tester = SemanticsTester(owner());
tester.updateNode(
id: 0,
rect: const ui.Rect.fromLTRB(0, 0, 100, 60),
children: <SemanticsNodeUpdate>[
tester.updateNode(
id: 1,
rect: const ui.Rect.fromLTRB(0, 0, 100, 20),
children: <SemanticsNodeUpdate>[
tester.updateNode(
id: 4,
children: <SemanticsNodeUpdate>[tester.updateNode(id: 6), tester.updateNode(id: 7)],
),
tester.updateNode(id: 5),
],
),
tester.updateNode(id: 2, rect: const ui.Rect.fromLTRB(0, 20, 100, 40), traversalParent: 4),
tester.updateNode(id: 3, rect: const ui.Rect.fromLTRB(0, 40, 100, 60), traversalParent: 4),
tester.updateNode(id: 8, rect: const ui.Rect.fromLTRB(0, 40, 100, 60), traversalParent: 4),
],
);
tester.apply();
expectSemanticsTree(owner(), '''
<sem>
<sem>
<sem id="flt-semantic-node-4" aria-owns="flt-semantic-node-2 flt-semantic-node-3 flt-semantic-node-8">
<sem></sem>
<sem></sem>
</sem>
<sem></sem>
</sem>
<sem id="flt-semantic-node-2"></sem>
<sem id="flt-semantic-node-3"></sem>
<sem id="flt-semantic-node-8"></sem>
</sem>
''');
final SemanticsObject node4 = tester.getSemanticsObject(4);
expect(
node4.element.getAttribute('aria-owns'),
'flt-semantic-node-2 flt-semantic-node-3 flt-semantic-node-8',
);
});
}
void _testSemanticsValidationResult() {
test('renders validation result', () {
semantics()
@ -6018,6 +6167,7 @@ void updateNode(
int platformViewId = -1, // -1 means not a platform view
int scrollChildren = 0,
int scrollIndex = 0,
int traversalParent = -1,
double scrollPosition = 0.0,
double scrollExtentMax = 0.0,
double scrollExtentMin = 0.0,
@ -6036,6 +6186,7 @@ void updateNode(
String tooltip = '',
ui.TextDirection textDirection = ui.TextDirection.ltr,
Float64List? transform,
Float64List? hitTestTransform,
Int32List? childrenInTraversalOrder,
Int32List? childrenInHitTestOrder,
Int32List? additionalActions,
@ -6048,6 +6199,7 @@ void updateNode(
ui.Locale? locale,
}) {
transform ??= Float64List.fromList(Matrix4.identity().storage);
hitTestTransform ??= Float64List.fromList(Matrix4.identity().storage);
childrenInTraversalOrder ??= Int32List(0);
childrenInHitTestOrder ??= Int32List(0);
additionalActions ??= Int32List(0);
@ -6063,6 +6215,7 @@ void updateNode(
platformViewId: platformViewId,
scrollChildren: scrollChildren,
scrollIndex: scrollIndex,
traversalParent: traversalParent,
scrollPosition: scrollPosition,
scrollExtentMax: scrollExtentMax,
scrollExtentMin: scrollExtentMin,
@ -6081,6 +6234,7 @@ void updateNode(
tooltip: tooltip,
textDirection: textDirection,
transform: transform,
hitTestTransform: hitTestTransform,
childrenInTraversalOrder: childrenInTraversalOrder,
childrenInHitTestOrder: childrenInHitTestOrder,
additionalActions: additionalActions,

View File

@ -62,6 +62,7 @@ class SemanticsTester {
int? platformViewId,
int? scrollChildren,
int? scrollIndex,
int? traversalParent,
double? scrollPosition,
double? scrollExtentMax,
double? scrollExtentMin,
@ -80,6 +81,7 @@ class SemanticsTester {
String? tooltip,
ui.TextDirection? textDirection,
Float64List? transform,
Float64List? hitTestTransform,
Int32List? additionalActions,
List<SemanticsNodeUpdate>? children,
int? headingLevel,
@ -194,6 +196,7 @@ class SemanticsTester {
platformViewId: platformViewId ?? -1,
scrollChildren: scrollChildren ?? 0,
scrollIndex: scrollIndex ?? 0,
traversalParent: traversalParent ?? -1,
scrollPosition: scrollPosition ?? 0,
scrollExtentMax: scrollExtentMax ?? 0,
scrollExtentMin: scrollExtentMin ?? 0,
@ -211,6 +214,9 @@ class SemanticsTester {
decreasedValueAttributes: decreasedValueAttributes ?? const <ui.StringAttribute>[],
tooltip: tooltip ?? '',
transform: transform != null ? toMatrix32(transform) : Matrix4.identity().storage,
hitTestTransform: hitTestTransform != null
? toMatrix32(hitTestTransform)
: Matrix4.identity().storage,
childrenInTraversalOrder: childIds,
childrenInHitTestOrder: childIds,
additionalActions: additionalActions ?? Int32List(0),

View File

@ -290,6 +290,7 @@ void sendSemanticsUpdate() {
platformViewId: -1,
scrollChildren: 0,
scrollIndex: 0,
traversalParent: 0,
scrollPosition: 0,
scrollExtentMax: 0,
scrollExtentMin: 0,
@ -308,6 +309,7 @@ void sendSemanticsUpdate() {
tooltip: tooltip,
textDirection: TextDirection.ltr,
transform: transform,
hitTestTransform: transform,
childrenInTraversalOrder: childrenInTraversalOrder,
childrenInHitTestOrder: childrenInHitTestOrder,
additionalActions: additionalActions,

View File

@ -575,6 +575,16 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
return stringIndex == EMPTY_STRING_INDEX ? null : strings[stringIndex];
}
private static float[] getMatrix4FromBuffer(@NonNull ByteBuffer buffer, float[] transform) {
if (transform == null) {
transform = new float[16];
}
for (int i = 0; i < 16; ++i) {
transform[i] = buffer.getFloat();
}
return transform;
}
/**
* Disconnects any listeners and/or delegates that were initialized in {@code
* AccessibilityBridge}'s constructor, or added after.
@ -1622,6 +1632,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
* View#onHoverEvent(MotionEvent)}.
*/
public boolean onAccessibilityHoverEvent(MotionEvent event, boolean ignorePlatformViews) {
if (!accessibilityManager.isTouchExplorationEnabled()) {
return false;
}
@ -1681,6 +1692,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
if (flutterSemanticsTree.isEmpty()) {
return;
}
SemanticsNode semanticsNodeUnderCursor =
getRootSemanticsNode().hitTest(new float[] {x, y, 0, 1}, ignorePlatformViews);
if (semanticsNodeUnderCursor != hoveredObject) {
@ -2382,6 +2394,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
private int platformViewId;
private int scrollChildren;
private int scrollIndex;
private int traversalParent;
private float scrollPosition;
private float scrollExtentMax;
private float scrollExtentMin;
@ -2444,6 +2457,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
private float right;
private float bottom;
private float[] transform;
private float[] hitTestTransform;
private SemanticsNode parent;
private List<SemanticsNode> childrenInTraversalOrder = new ArrayList<>();
@ -2603,6 +2617,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
platformViewId = buffer.getInt();
scrollChildren = buffer.getInt();
scrollIndex = buffer.getInt();
traversalParent = buffer.getInt();
scrollPosition = buffer.getFloat();
scrollExtentMax = buffer.getFloat();
scrollExtentMin = buffer.getFloat();
@ -2636,24 +2651,23 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
right = buffer.getFloat();
bottom = buffer.getFloat();
if (transform == null) {
transform = new float[16];
}
for (int i = 0; i < 16; ++i) {
transform[i] = buffer.getFloat();
}
transform = getMatrix4FromBuffer(buffer, transform);
hitTestTransform = getMatrix4FromBuffer(buffer, hitTestTransform);
inverseTransformDirty = true;
globalGeometryDirty = true;
final int childCount = buffer.getInt();
final int traversalOrderChildCount = buffer.getInt();
childrenInTraversalOrder.clear();
childrenInHitTestOrder.clear();
for (int i = 0; i < childCount; ++i) {
for (int i = 0; i < traversalOrderChildCount; ++i) {
SemanticsNode child = accessibilityBridge.getOrCreateSemanticsNode(buffer.getInt());
child.parent = this;
childrenInTraversalOrder.add(child);
}
for (int i = 0; i < childCount; ++i) {
final int hitTestOrderChildCount = buffer.getInt();
childrenInHitTestOrder.clear();
for (int i = 0; i < hitTestOrderChildCount; ++i) {
SemanticsNode child = accessibilityBridge.getOrCreateSemanticsNode(buffer.getInt());
child.parent = this;
childrenInHitTestOrder.add(child);
@ -2737,7 +2751,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
if (inverseTransform == null) {
inverseTransform = new float[16];
}
if (!Matrix.invertM(inverseTransform, 0, transform, 0)) {
if (!Matrix.invertM(inverseTransform, 0, hitTestTransform, 0)) {
Arrays.fill(inverseTransform, 0);
}
}

View File

@ -210,6 +210,7 @@ void PlatformViewAndroidDelegate::UpdateSemantics(
buffer_int32[position++] = node.platformViewId;
buffer_int32[position++] = node.scrollChildren;
buffer_int32[position++] = node.scrollIndex;
buffer_int32[position++] = node.traversalParent;
buffer_float32[position++] = static_cast<float>(node.scrollPosition);
buffer_float32[position++] = static_cast<float>(node.scrollExtentMax);
buffer_float32[position++] = static_cast<float>(node.scrollExtentMin);
@ -250,12 +251,14 @@ void PlatformViewAndroidDelegate::UpdateSemantics(
buffer_float32[position++] = node.rect.bottom();
node.transform.getColMajor(&buffer_float32[position]);
position += 16;
node.hitTestTransform.getColMajor(&buffer_float32[position]);
position += 16;
buffer_int32[position++] = node.childrenInTraversalOrder.size();
for (int32_t child : node.childrenInTraversalOrder) {
buffer_int32[position++] = child;
}
buffer_int32[position++] = node.childrenInHitTestOrder.size();
for (int32_t child : node.childrenInHitTestOrder) {
buffer_int32[position++] = child;
}

View File

@ -17,7 +17,7 @@ namespace flutter {
class PlatformViewAndroidDelegate {
public:
static constexpr size_t kBytesPerNode =
52 * sizeof(int32_t); // The # fields in SemanticsNode
70 * sizeof(int32_t); // The # fields in SemanticsNode
static constexpr size_t kBytesPerChild = sizeof(int32_t);
static constexpr size_t kBytesPerCustomAction = sizeof(int32_t);
static constexpr size_t kBytesPerAction = 4 * sizeof(int32_t);

View File

@ -41,6 +41,7 @@ TEST(PlatformViewShell, UpdateSemanticsDoesFlutterViewUpdateSemantics) {
buffer_int32[position++] = node0.platformViewId;
buffer_int32[position++] = node0.scrollChildren;
buffer_int32[position++] = node0.scrollIndex;
buffer_int32[position++] = node0.traversalParent;
buffer_float32[position++] = static_cast<float>(node0.scrollPosition);
buffer_float32[position++] = static_cast<float>(node0.scrollExtentMax);
buffer_float32[position++] = static_cast<float>(node0.scrollExtentMin);
@ -69,6 +70,8 @@ TEST(PlatformViewShell, UpdateSemanticsDoesFlutterViewUpdateSemantics) {
buffer_float32[position++] = node0.rect.bottom();
node0.transform.getColMajor(&buffer_float32[position]);
position += 16;
node0.hitTestTransform.getColMajor(&buffer_float32[position]);
position += 16;
buffer_int32[position++] = 0; // node0.childrenInTraversalOrder.size();
buffer_int32[position++] = 0; // node0.customAccessibilityActions.size();
EXPECT_CALL(*jni_mock,
@ -109,6 +112,7 @@ TEST(PlatformViewShell, UpdateSemanticsDoesUpdateLinkUrl) {
buffer_int32[position++] = node0.platformViewId;
buffer_int32[position++] = node0.scrollChildren;
buffer_int32[position++] = node0.scrollIndex;
buffer_int32[position++] = node0.traversalParent;
buffer_float32[position++] = static_cast<float>(node0.scrollPosition);
buffer_float32[position++] = static_cast<float>(node0.scrollExtentMax);
buffer_float32[position++] = static_cast<float>(node0.scrollExtentMin);
@ -137,6 +141,8 @@ TEST(PlatformViewShell, UpdateSemanticsDoesUpdateLinkUrl) {
buffer_float32[position++] = node0.rect.bottom();
node0.transform.getColMajor(&buffer_float32[position]);
position += 16;
node0.hitTestTransform.getColMajor(&buffer_float32[position]);
position += 16;
buffer_int32[position++] = 0; // node0.childrenInTraversalOrder.size();
buffer_int32[position++] = 0; // node0.customAccessibilityActions.size();
EXPECT_CALL(*jni_mock,
@ -157,6 +163,7 @@ TEST(PlatformViewShell, UpdateSemanticsDoesUpdateLocale) {
node0.identifier = "identifier";
node0.label = "label";
node0.locale = "es-MX";
node0.traversalParent = -1;
update.insert(std::make_pair(0, node0));
std::vector<uint8_t> expected_buffer(
@ -177,6 +184,7 @@ TEST(PlatformViewShell, UpdateSemanticsDoesUpdateLocale) {
buffer_int32[position++] = node0.platformViewId;
buffer_int32[position++] = node0.scrollChildren;
buffer_int32[position++] = node0.scrollIndex;
buffer_int32[position++] = node0.traversalParent;
buffer_float32[position++] = static_cast<float>(node0.scrollPosition);
buffer_float32[position++] = static_cast<float>(node0.scrollExtentMax);
buffer_float32[position++] = static_cast<float>(node0.scrollExtentMin);
@ -205,6 +213,8 @@ TEST(PlatformViewShell, UpdateSemanticsDoesUpdateLocale) {
buffer_float32[position++] = node0.rect.bottom();
node0.transform.getColMajor(&buffer_float32[position]);
position += 16;
node0.hitTestTransform.getColMajor(&buffer_float32[position]);
position += 16;
buffer_int32[position++] = 0; // node0.childrenInTraversalOrder.size();
buffer_int32[position++] = 0; // node0.customAccessibilityActions.size();
EXPECT_CALL(*jni_mock,
@ -261,6 +271,7 @@ TEST(PlatformViewShell,
buffer_int32[position++] = node0.platformViewId;
buffer_int32[position++] = node0.scrollChildren;
buffer_int32[position++] = node0.scrollIndex;
buffer_int32[position++] = node0.traversalParent;
buffer_float32[position++] = static_cast<float>(node0.scrollPosition);
buffer_float32[position++] = static_cast<float>(node0.scrollExtentMax);
buffer_float32[position++] = static_cast<float>(node0.scrollExtentMin);
@ -300,6 +311,8 @@ TEST(PlatformViewShell,
buffer_float32[position++] = node0.rect.bottom();
node0.transform.getColMajor(&buffer_float32[position]);
position += 16;
node0.hitTestTransform.getColMajor(&buffer_float32[position]);
position += 16;
buffer_int32[position++] = 0; // node0.childrenInTraversalOrder.size();
buffer_int32[position++] = 0; // node0.customAccessibilityActions.size();
EXPECT_CALL(*jni_mock,

View File

@ -2513,6 +2513,7 @@ public class AccessibilityBridgeTest {
int platformViewId = -1;
int scrollChildren = 0;
int scrollIndex = 0;
int traversalParent = -1;
float scrollPosition = 0.0f;
float scrollExtentMax = 0.0f;
float scrollExtentMin = 0.0f;
@ -2543,6 +2544,14 @@ public class AccessibilityBridgeTest {
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
};
float[] hitTestTransform =
new float[] {
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f
};
final List<TestSemanticsNode> children = new ArrayList<TestSemanticsNode>();
public void addChild(TestSemanticsNode child) {
@ -2574,6 +2583,7 @@ public class AccessibilityBridgeTest {
bytes.putInt(platformViewId);
bytes.putInt(scrollChildren);
bytes.putInt(scrollIndex);
bytes.putInt(traversalParent);
bytes.putFloat(scrollPosition);
bytes.putFloat(scrollExtentMax);
bytes.putFloat(scrollExtentMin);
@ -2616,12 +2626,17 @@ public class AccessibilityBridgeTest {
for (int i = 0; i < 16; i++) {
bytes.putFloat(transform[i]);
}
// hitTestTransform.
for (int i = 0; i < 16; i++) {
bytes.putFloat(hitTestTransform[i]);
}
// children in traversal order.
bytes.putInt(children.size());
for (TestSemanticsNode node : children) {
bytes.putInt(node.id);
}
// children in hit test order.
bytes.putInt(children.size());
for (TestSemanticsNode node : children) {
bytes.putInt(node.id);
}

View File

@ -98,15 +98,17 @@ void AccessibilityBridge::UpdateSemantics(
scrollOccured = scrollOccured || [object nodeWillCauseScroll:&node];
needsAnnouncement = [object nodeShouldTriggerAnnouncement:&node];
[object setSemanticsNode:&node];
NSUInteger newChildCount = node.childrenInTraversalOrder.size();
NSMutableArray* newChildren = [[NSMutableArray alloc] initWithCapacity:newChildCount];
for (NSUInteger i = 0; i < newChildCount; ++i) {
NSUInteger newChildCountInTraversalOrder = node.childrenInTraversalOrder.size();
NSMutableArray* newChildren =
[[NSMutableArray alloc] initWithCapacity:newChildCountInTraversalOrder];
for (NSUInteger i = 0; i < newChildCountInTraversalOrder; ++i) {
SemanticsObject* child = GetOrCreateObject(node.childrenInTraversalOrder[i], nodes);
[newChildren addObject:child];
}
NSUInteger newChildCountInHitTestOrder = node.childrenInHitTestOrder.size();
NSMutableArray* newChildrenInHitTestOrder =
[[NSMutableArray alloc] initWithCapacity:newChildCount];
for (NSUInteger i = 0; i < newChildCount; ++i) {
[[NSMutableArray alloc] initWithCapacity:newChildCountInHitTestOrder];
for (NSUInteger i = 0; i < newChildCountInHitTestOrder; ++i) {
SemanticsObject* child = GetOrCreateObject(node.childrenInHitTestOrder[i], nodes);
[newChildrenInHitTestOrder addObject:child];
}

View File

@ -170,6 +170,8 @@ Future<void> a11y_main() async {
labelAttributes: <StringAttribute>[],
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
transform: kTestTransform,
hitTestTransform: kTestTransform,
traversalParent: 0,
childrenInTraversalOrder: Int32List.fromList(<int>[84, 96]),
childrenInHitTestOrder: Int32List.fromList(<int>[96, 84]),
actions: 0,
@ -206,6 +208,8 @@ Future<void> a11y_main() async {
labelAttributes: <StringAttribute>[],
rect: const Rect.fromLTRB(40.0, 40.0, 80.0, 80.0),
transform: kTestTransform,
hitTestTransform: kTestTransform,
traversalParent: 0,
actions: 0,
flags: SemanticsFlags.none,
maxValueLength: 0,
@ -242,6 +246,8 @@ Future<void> a11y_main() async {
labelAttributes: <StringAttribute>[],
rect: const Rect.fromLTRB(40.0, 40.0, 80.0, 80.0),
transform: kTestTransform,
hitTestTransform: kTestTransform,
traversalParent: 0,
childrenInTraversalOrder: Int32List.fromList(<int>[128]),
childrenInHitTestOrder: Int32List.fromList(<int>[128]),
actions: 0,
@ -278,6 +284,8 @@ Future<void> a11y_main() async {
labelAttributes: <StringAttribute>[],
rect: const Rect.fromLTRB(40.0, 40.0, 80.0, 80.0),
transform: kTestTransform,
hitTestTransform: kTestTransform,
traversalParent: 0,
additionalActions: Int32List.fromList(<int>[21]),
platformViewId: 0x3f3,
actions: 0,
@ -350,6 +358,8 @@ Future<void> a11y_string_attributes() async {
],
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
transform: kTestTransform,
hitTestTransform: kTestTransform,
traversalParent: 0,
childrenInTraversalOrder: Int32List.fromList(<int>[84, 96]),
childrenInHitTestOrder: Int32List.fromList(<int>[96, 84]),
actions: 0,
@ -1650,6 +1660,8 @@ Future<void> a11y_main_multi_view() async {
labelAttributes: <StringAttribute>[],
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
transform: kTestTransform,
hitTestTransform: kTestTransform,
traversalParent: 0,
childrenInTraversalOrder: Int32List.fromList(<int>[84, 96]),
childrenInHitTestOrder: Int32List.fromList(<int>[96, 84]),
actions: 0,

View File

@ -184,7 +184,9 @@ void fl_view_accessible_handle_update_semantics(
for (size_t i = 0; i < child_count; i++) {
FlAccessibleNode* child =
lookup_node(self, children_in_traversal_order[i]);
g_assert(child != nullptr);
if (child == nullptr) {
continue;
}
fl_accessible_node_set_parent(child, ATK_OBJECT(parent), i);
g_ptr_array_add(children, child);
}

View File

@ -432,6 +432,7 @@ Future<void> sendSemanticsTreeInfo() async {
ui.SemanticsUpdate createSemanticsUpdate(int nodeId) {
final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
final Float64List transform = Float64List(16);
final Float64List hitTestTransform = Float64List(16);
final Int32List childrenInTraversalOrder = Int32List(0);
final Int32List childrenInHitTestOrder = Int32List(0);
final Int32List additionalActions = Int32List(0);
@ -450,6 +451,7 @@ Future<void> sendSemanticsTreeInfo() async {
platformViewId: -1,
scrollChildren: 0,
scrollIndex: 0,
traversalParent: -1,
scrollPosition: 0,
scrollExtentMax: 0,
scrollExtentMin: 0,
@ -468,6 +470,7 @@ Future<void> sendSemanticsTreeInfo() async {
tooltip: 'tooltip',
textDirection: ui.TextDirection.ltr,
transform: transform,
hitTestTransform: hitTestTransform,
childrenInTraversalOrder: childrenInTraversalOrder,
childrenInHitTestOrder: childrenInHitTestOrder,
additionalActions: additionalActions,

View File

@ -56,10 +56,12 @@ class LocaleInitialization extends Scenario {
currentValueLength: 0,
scrollChildren: 0,
scrollIndex: 0,
traversalParent: -1,
scrollPosition: 0.0,
scrollExtentMax: 0.0,
scrollExtentMin: 0.0,
transform: Matrix4.identity().storage,
hitTestTransform: Matrix4.identity().storage,
hint: '',
hintAttributes: <StringAttribute>[],
value: '',
@ -116,10 +118,12 @@ class LocaleInitialization extends Scenario {
currentValueLength: 0,
scrollChildren: 0,
scrollIndex: 0,
traversalParent: -1,
scrollPosition: 0.0,
scrollExtentMax: 0.0,
scrollExtentMin: 0.0,
transform: Matrix4.identity().storage,
hitTestTransform: Matrix4.identity().storage,
hint: '',
hintAttributes: <StringAttribute>[],
value: '',

View File

@ -1021,6 +1021,12 @@ class RenderCustomPaint extends RenderProxyBox {
if (properties.identifier != null) {
config.identifier = properties.identifier!;
}
if (properties.traversalParentIdentifier != null) {
config.traversalParentIdentifier = properties.traversalParentIdentifier;
}
if (properties.traversalChildIdentifier != null) {
config.traversalChildIdentifier = properties.traversalChildIdentifier;
}
if (properties.tooltip != null) {
config.tooltip = properties.tooltip!;
}

View File

@ -1428,7 +1428,7 @@ base class PipelineOwner with DiagnosticableTreeMixin {
///
/// See [RendererBinding] for an example of how this function is used.
// See [_RenderObjectSemantics]'s documentation for detailed explanations on
// what this method does
// what this method does.
void flushSemantics() {
if (_semanticsOwner == null) {
return;
@ -1461,7 +1461,7 @@ base class PipelineOwner with DiagnosticableTreeMixin {
// (via SemanticsConfiguration.isBlockingSemanticsOfPreviouslyPaintedNodes)
// or is hidden by parent through visitChildrenForSemantics. Otherwise,
// the parent node would have updated this node's parent data and it
// the not be dirty.
// would not be dirty.
//
// Updating the parent data now may create a gap of render object with
// dirty parent data when this branch later rejoin the rendering tree.
@ -2012,15 +2012,6 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
RenderObject? get parent => _parent;
RenderObject? _parent;
/// The semantics parent of this render object in the semantics tree.
///
/// This is typically the same as [parent].
///
/// [OverlayPortal] overrides this field to change how it forms its
/// semantics sub-tree.
@visibleForOverriding
RenderObject? get semanticsParent => _parent;
/// Called by subclasses when they decide a render object is a child.
///
/// Only for use by subclasses when changing their child lists. Calling this
@ -4883,6 +4874,12 @@ mixin SemanticsAnnotationsMixin on RenderObject {
if (_properties.identifier != null) {
config.identifier = _properties.identifier!;
}
if (_properties.traversalParentIdentifier != null) {
config.traversalParentIdentifier = _properties.traversalParentIdentifier;
}
if (_properties.traversalChildIdentifier != null) {
config.traversalChildIdentifier = _properties.traversalChildIdentifier;
}
if (_attributedLabel != null) {
config.attributedLabel = _attributedLabel!;
}
@ -5453,7 +5450,7 @@ class _RenderObjectSemantics extends _SemanticsFragment with DiagnosticableTreeM
isRoot;
}
bool get isRoot => renderObject.semanticsParent == null;
bool get isRoot => renderObject.parent == null;
bool get shouldFormSemanticsNode {
if (configProvider.effective.isSemanticBoundary) {
@ -6168,8 +6165,7 @@ class _RenderObjectSemantics extends _SemanticsFragment with DiagnosticableTreeM
// node, thus marking this semantics boundary dirty is not enough, it needs
// to find the first parent semantics boundary that does not have any
// possible sibling node.
while (node.semanticsParent != null &&
(mayProduceSiblingNodes || !isEffectiveSemanticsBoundary)) {
while (node.parent != null && (mayProduceSiblingNodes || !isEffectiveSemanticsBoundary)) {
if (node != renderObject && node._semantics.parentDataDirty && !mayProduceSiblingNodes) {
break;
}
@ -6185,7 +6181,7 @@ class _RenderObjectSemantics extends _SemanticsFragment with DiagnosticableTreeM
mayProduceSiblingNodes |=
node._semantics.configProvider.effective.childConfigurationsDelegate != null;
node = node.semanticsParent!;
node = node.parent!;
// If node._semantics.built is false, this branch is currently blocked.
// In that case, it should continue dirty upward until it reach a
// unblocked semantics boundary because blocked branch will not rebuild
@ -6211,10 +6207,7 @@ class _RenderObjectSemantics extends _SemanticsFragment with DiagnosticableTreeM
}
if (!node._semantics.parentDataDirty) {
if (renderObject.owner != null) {
assert(
node._semantics.configProvider.effective.isSemanticBoundary ||
node.semanticsParent == null,
);
assert(node._semantics.configProvider.effective.isSemanticBoundary || node.parent == null);
if (renderObject.owner!._nodesNeedingSemantics.add(node)) {
renderObject.owner!.requestVisualUpdate();
}

View File

@ -996,6 +996,8 @@ class SemanticsData with Diagnosticable {
required this.flagsCollection,
required this.actions,
required this.identifier,
required this.traversalParentIdentifier,
required this.traversalChildIdentifier,
required this.attributedLabel,
required this.attributedValue,
required this.attributedIncreasedValue,
@ -1069,6 +1071,12 @@ class SemanticsData with Diagnosticable {
/// {@macro flutter.semantics.SemanticsProperties.identifier}
final String identifier;
/// {@macro flutter.semantics.SemanticsProperties.traversalParentIdentifier}
final Object? traversalParentIdentifier;
/// {@macro flutter.semantics.SemanticsProperties.traversalChildIdentifier}
final Object? traversalChildIdentifier;
/// A textual description for the current label of the node.
///
/// The reading direction is given by [textDirection].
@ -1322,6 +1330,20 @@ class SemanticsData with Diagnosticable {
final List<String> flagSummary = flagsCollection.toStrings();
properties.add(IterableProperty<String>('flags', flagSummary, ifEmpty: null));
properties.add(StringProperty('identifier', identifier, defaultValue: ''));
properties.add(
DiagnosticsProperty<Object>(
'traversalParentIdentifier',
traversalParentIdentifier,
defaultValue: null,
),
);
properties.add(
DiagnosticsProperty<Object>(
'traversalChildIdentifier',
traversalChildIdentifier,
defaultValue: null,
),
);
properties.add(AttributedStringProperty('label', attributedLabel));
properties.add(AttributedStringProperty('value', attributedValue));
properties.add(AttributedStringProperty('increasedValue', attributedIncreasedValue));
@ -1367,6 +1389,8 @@ class SemanticsData with Diagnosticable {
other.flags == flags &&
other.actions == actions &&
other.identifier == identifier &&
other.traversalParentIdentifier == traversalParentIdentifier &&
other.traversalChildIdentifier == traversalChildIdentifier &&
other.attributedLabel == attributedLabel &&
other.attributedValue == attributedValue &&
other.attributedIncreasedValue == attributedIncreasedValue &&
@ -1427,6 +1451,8 @@ class SemanticsData with Diagnosticable {
validationResult,
controlsNodes == null ? null : Object.hashAll(controlsNodes!),
inputType,
traversalParentIdentifier,
traversalChildIdentifier,
),
);
@ -1564,6 +1590,8 @@ class SemanticsProperties extends DiagnosticableTree {
this.maxValueLength,
this.currentValueLength,
this.identifier,
this.traversalParentIdentifier,
this.traversalChildIdentifier,
this.label,
this.attributedLabel,
this.value,
@ -1904,6 +1932,51 @@ class SemanticsProperties extends DiagnosticableTree {
/// {@endtemplate}
final String? identifier;
/// {@template flutter.semantics.SemanticsProperties.traversalParentIdentifier}
/// Provides an identifier for establishing parent-child relationships in the semantics
/// traversal tree.
///
/// This property is used to create a logical parent-child relationship between
/// semantics nodes that may not be directly connected in the widget tree. It's
/// primarily used with [OverlayPortal] to ensure proper accessibility traversal
/// order when overlay content needs to be semantically connected to its parent
/// widget.
///
/// When a semantics node has a [traversalParentIdentifier], it indicates that
/// this node can act as a parent for other nodes that reference this identifier
/// in their [traversalChildIdentifier]. This allows assistive technologies
/// to navigate the UI in the correct logical order.
///
/// The `traversalParentIdentifier` must be unique in the semantics. No two
/// semantics node can have the same `traversalParentIdentifier`. This unique
/// identifier serves as the only reference for its traversal children. To
/// graft other nodes as the traversal children of this node, assign this same
/// value to their `traversalChildIdentifier`.
/// {@endtemplate}
final Object? traversalParentIdentifier;
/// {@template flutter.semantics.SemanticsProperties.traversalChildIdentifier}
/// Provides an identifier for establishing parent-child relationships in the semantics
/// traversal tree.
///
/// This property is used to create a logical parent-child relationship between
/// semantics nodes that may not be directly connected in the widget tree. It's
/// primarily used with [OverlayPortal] to ensure proper accessibility traversal
/// order when overlay content needs to be semantically connected to its parent
/// widget.
///
/// When a semantics node has a [traversalChildIdentifier], it indicates that
/// this node should be treated as a child of another node that has this same
/// identifier as its [traversalParentIdentifier]. This allows assistive technologies
/// to navigate the UI in the correct logical order.
///
/// The `traversalChildIdentifier` value may be duplicated across multiple
/// semantics nodes. To establish one or more nodes as the traversal children
/// of a parent node, assign this identifier the same value as the parent's
/// `traversalParentIdentifier`.
/// {@endtemplate}
final Object? traversalChildIdentifier;
/// Provides a textual description of the widget.
///
/// If a label is provided, there must either by an ambient [Directionality]
@ -2502,6 +2575,20 @@ class SemanticsProperties extends DiagnosticableTree {
properties.add(DiagnosticsProperty<bool>('selected', selected, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('isRequired', isRequired, defaultValue: null));
properties.add(StringProperty('identifier', identifier, defaultValue: null));
properties.add(
DiagnosticsProperty<Object>(
'traversalParentIdentifier',
traversalParentIdentifier,
defaultValue: null,
),
);
properties.add(
DiagnosticsProperty<Object>(
'traversalChildIdentifier',
traversalChildIdentifier,
defaultValue: null,
),
);
properties.add(StringProperty('label', label, defaultValue: null));
properties.add(
AttributedStringProperty('attributedLabel', attributedLabel, defaultValue: null),
@ -2877,9 +2964,12 @@ class SemanticsNode with DiagnosticableTreeMixin {
bool get hasChildren => _children?.isNotEmpty ?? false;
bool _dead = false;
/// The number of children this node has.
/// The number of children this node has in hit-test(paint) order.
int get childrenCount => hasChildren ? _children!.length : 0;
/// The number of children this node has in traversal order.
int get childrenCountInTraversalOrder => _childrenInTraversalOrder().length;
/// Visits the immediate children of this node.
///
/// This function calls visitor for each immediate child until visitor returns
@ -2929,6 +3019,24 @@ class SemanticsNode with DiagnosticableTreeMixin {
SemanticsNode? get parent => _parent;
SemanticsNode? _parent;
/// The real parent of this node in traversal order.
///
/// This is useful for an [OverlayPortal] or a similar scenario where the node's
/// hit-test parent (i.e., [parent]) and its traversal parent (i.e., [traversalParent])
/// are different. If this node indicates an overlay portal child,
/// [traversalParent] is its overlay portal parent node in traversal order.
/// Otherwise, it is the same as [parent]. The [traversalParent] is used when
/// the transform of this node needs to be updated in traversal order.
SemanticsNode? get traversalParent => _traversalParent ?? parent;
SemanticsNode? _traversalParent;
set traversalParent(SemanticsNode? value) {
if (_traversalParent == value) {
return;
}
_traversalParent = value;
_markDirty();
}
/// The depth of this node in the semantics tree.
///
/// The depth of nodes in a tree monotonically increases as you traverse down
@ -3040,6 +3148,14 @@ class SemanticsNode with DiagnosticableTreeMixin {
assert(!owner!._detachedNodes.contains(this));
owner!._nodes.remove(id);
owner!._detachedNodes.add(this);
// Clean up the according entry in owner._traversalParentNodes map.
owner!._traversalParentNodes.removeWhere((Object key, SemanticsNode node) => node == this);
// Clean up this node from the value set in owner._traversalChildNodes map.
for (final Set<SemanticsNode> childSet in owner!._traversalChildNodes.values) {
childSet.removeWhere((SemanticsNode node) => node == this);
}
_owner = null;
assert(parent == null || attached == parent!.attached);
if (_children != null) {
@ -3151,6 +3267,17 @@ class SemanticsNode with DiagnosticableTreeMixin {
String get identifier => _identifier;
String _identifier = _kEmptyConfig.identifier;
/// {@macro flutter.semantics.SemanticsProperties.traversalParentIdentifier}
Object? get traversalParentIdentifier => _traversalParentIdentifier;
Object? _traversalParentIdentifier;
/// {@macro flutter.semantics.SemanticsProperties.traversalChildIdentifier}
Object? get traversalChildIdentifier => _traversalChildIdentifier;
Object? _traversalChildIdentifier;
bool get _isTraversalParent => _traversalParentIdentifier != null;
bool get _isTraversalChild => _traversalChildIdentifier != null;
/// A textual description of this node.
///
/// The reading direction is given by [textDirection].
@ -3439,6 +3566,8 @@ class SemanticsNode with DiagnosticableTreeMixin {
_mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;
_identifier = config.identifier;
_traversalParentIdentifier = config.traversalParentIdentifier;
_traversalChildIdentifier = config.traversalChildIdentifier;
_attributedLabel = config.attributedLabel;
_attributedValue = config.attributedValue;
_attributedIncreasedValue = config.attributedIncreasedValue;
@ -3502,6 +3631,8 @@ class SemanticsNode with DiagnosticableTreeMixin {
// must be done after the merging the its descendants.
int actions = _actionsAsBits;
String identifier = _identifier;
Object? traversalParentIdentifier = _traversalParentIdentifier;
Object? traversalChildIdentifier = _traversalChildIdentifier;
AttributedString attributedLabel = _attributedLabel;
AttributedString attributedValue = _attributedValue;
AttributedString attributedIncreasedValue = _attributedIncreasedValue;
@ -3571,6 +3702,8 @@ class SemanticsNode with DiagnosticableTreeMixin {
if (identifier == '') {
identifier = node._identifier;
}
traversalParentIdentifier ??= node.traversalParentIdentifier;
traversalChildIdentifier ??= node.traversalChildIdentifier;
if (attributedValue.string == '') {
attributedValue = node._attributedValue;
}
@ -3650,6 +3783,8 @@ class SemanticsNode with DiagnosticableTreeMixin {
flagsCollection: flags,
actions: _areUserActionsBlocked ? actions & _kUnblockedUserActions : actions,
identifier: identifier,
traversalParentIdentifier: traversalParentIdentifier,
traversalChildIdentifier: traversalChildIdentifier,
attributedLabel: attributedLabel,
attributedValue: attributedValue,
attributedIncreasedValue: attributedIncreasedValue,
@ -3688,8 +3823,64 @@ class SemanticsNode with DiagnosticableTreeMixin {
static final Int32List _kEmptyCustomSemanticsActionsList = Int32List(0);
static final Float64List _kIdentityTransform = _initIdentityTransform();
static Matrix4 _computeChildTransform({
required Matrix4? parentTransform,
required Rect? parentPaintClipRect,
required Rect? parentSemanticsClipRect,
required SemanticsNode parent,
required SemanticsNode child,
}) {
final Matrix4 transform = parentTransform?.clone() ?? Matrix4.identity();
Matrix4? parentToCommonAncestorTransform;
Matrix4? childToCommonAncestorTransform;
SemanticsNode childSemanticsNode = child;
SemanticsNode parentSemanticsNode = parent;
// Find the common ancestor.
while (!identical(childSemanticsNode, parentSemanticsNode)) {
final int fromDepth = childSemanticsNode.depth;
final int toDepth = parentSemanticsNode.depth;
if (fromDepth >= toDepth) {
childToCommonAncestorTransform ??= Matrix4.identity();
childToCommonAncestorTransform.multiply(childSemanticsNode.transform ?? Matrix4.identity());
childSemanticsNode = childSemanticsNode.traversalParent!;
}
if (fromDepth <= toDepth) {
parentToCommonAncestorTransform ??= Matrix4.identity();
parentToCommonAncestorTransform.multiply(
parentSemanticsNode.transform ?? Matrix4.identity(),
);
if (parentSemanticsNode.traversalParent == null) {
break;
}
parentSemanticsNode = parentSemanticsNode.traversalParent!;
}
}
if (parentToCommonAncestorTransform != null) {
if (parentToCommonAncestorTransform.invert() != 0) {
transform.multiply(parentToCommonAncestorTransform);
} else {
transform.setZero();
}
}
return transform;
}
Int32List _childrenIdInTraversalOrder() {
final List<SemanticsNode> sortedChildren = _childrenInTraversalOrder();
final Int32List childrenInTraversalOrder = Int32List(sortedChildren.length);
for (int i = 0; i < sortedChildren.length; i += 1) {
childrenInTraversalOrder[i] = sortedChildren[i].id;
}
return childrenInTraversalOrder;
}
void _addToUpdate(SemanticsUpdateBuilder builder, Set<int> customSemanticsActionIdsUpdate) {
assert(_dirty);
assert(_dirty || _isTraversalParent);
final SemanticsData data = getSemanticsData();
assert(() {
final FlutterError? error = _DebugSemanticsRoleChecks._checkSemanticsData(this);
@ -3701,15 +3892,33 @@ class SemanticsNode with DiagnosticableTreeMixin {
final Int32List childrenInTraversalOrder;
final Int32List childrenInHitTestOrder;
if (!hasChildren || mergeAllDescendantsIntoThisNode) {
childrenInTraversalOrder = _kEmptyChildList;
childrenInHitTestOrder = _kEmptyChildList;
} else {
final int childCount = _children!.length;
final List<SemanticsNode> sortedChildren = _childrenInTraversalOrder();
childrenInTraversalOrder = Int32List(childCount);
for (int i = 0; i < childCount; i += 1) {
childrenInTraversalOrder[i] = sortedChildren[i].id;
if (_isTraversalParent && !kIsWeb) {
// If the current node is a traversal parent node but it has no
// children in hit-test order, it means childrenIntraversalOrder will
// only contain its traversalChildren in _traversalChildNodes map.
if (owner != null && owner!._traversalChildNodes.containsKey(traversalParentIdentifier)) {
final Set<SemanticsNode> traversalChildren =
owner!._traversalChildNodes[traversalParentIdentifier]!;
int index = 0;
childrenInTraversalOrder = Int32List(traversalChildren.length);
for (final SemanticsNode node in traversalChildren) {
if (node.attached) {
childrenInTraversalOrder[index] = node.id;
index += 1;
}
}
} else {
childrenInTraversalOrder = _kEmptyChildList;
}
childrenInHitTestOrder = _kEmptyChildList;
} else {
childrenInTraversalOrder = _kEmptyChildList;
childrenInHitTestOrder = _kEmptyChildList;
}
} else {
childrenInTraversalOrder = _childrenIdInTraversalOrder();
final int childCount = _children!.length;
// _children is sorted in paint order, so we invert it to get the hit test
// order.
childrenInHitTestOrder = Int32List(childCount);
@ -3717,6 +3926,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
childrenInHitTestOrder[i] = _children![childCount - i - 1].id;
}
}
Int32List? customSemanticsActionIds;
if (data.customSemanticsActionIds?.isNotEmpty ?? false) {
customSemanticsActionIds = Int32List(data.customSemanticsActionIds!.length);
@ -3725,6 +3935,33 @@ class SemanticsNode with DiagnosticableTreeMixin {
customSemanticsActionIdsUpdate.add(data.customSemanticsActionIds![i]);
}
}
if (_isTraversalChild) {
traversalParent = owner!._traversalParentNodes[traversalChildIdentifier];
transform = _computeChildTransform(
parentPaintClipRect: traversalParent!.parentPaintClipRect,
parentSemanticsClipRect: traversalParent!.parentSemanticsClipRect,
parentTransform: null,
parent: traversalParent!,
child: this,
);
}
int traversalParentId = -1;
if (data.traversalChildIdentifier != null) {
final Object identifier = data.traversalChildIdentifier!;
if (owner!._traversalParentNodes.containsKey(identifier)) {
traversalParentId = owner!._traversalParentNodes[identifier]!.id;
}
}
final Float64List updatedTransform;
if (kIsWeb) {
updatedTransform = data.transform?.storage ?? _kIdentityTransform;
} else {
updatedTransform = transform?.storage ?? data.transform?.storage ?? _kIdentityTransform;
}
builder.updateNode(
id: id,
flags: data.flagsCollection,
@ -3753,7 +3990,9 @@ class SemanticsNode with DiagnosticableTreeMixin {
scrollPosition: data.scrollPosition ?? double.nan,
scrollExtentMax: data.scrollExtentMax ?? double.nan,
scrollExtentMin: data.scrollExtentMin ?? double.nan,
transform: data.transform?.storage ?? _kIdentityTransform,
transform: updatedTransform,
traversalParent: traversalParentId,
hitTestTransform: data.transform?.storage ?? _kIdentityTransform,
childrenInTraversalOrder: childrenInTraversalOrder,
childrenInHitTestOrder: childrenInHitTestOrder,
additionalActions: customSemanticsActionIds ?? _kEmptyCustomSemanticsActionsList,
@ -3768,8 +4007,86 @@ class SemanticsNode with DiagnosticableTreeMixin {
_dirty = false;
}
// Generate a children list in traversal order. Add tree grafting when needed
// so that all overlay portal child nodes have correct overlay portal parent
// in traversal order. On web, the childrenInTraversalOrder is kept the same
// as the _children(paint-order) list because of the assumption that requires both
// childrenInTraversalOrder and childrenInHitTestOrder have the same length.
// To ensure the correctness of the childrenInTraversalOrder, ARIA-owns is used
// on the web side.
List<SemanticsNode>? _updateChildrenInTraversalOrder() {
if (kIsWeb) {
return _children;
}
final List<SemanticsNode> updatedChildren = <SemanticsNode>[];
for (final SemanticsNode child in _children!) {
if (child._isTraversalChild && !_isTraversalParent) {
// If the child node is a traversal child, but the current node is
// not a traversal parent, it means the child node should be
// grafted to be a child of a traversal parent node that has the
// same identifier as the child. So this child should be removed from
// the current node's children list; i.e., we don't add it to
// updatedChildren list.
//
// A corner case is the traversal parent of the traversal child, in paint
// order, is the child of the traversal child. In this case, no grafting
// needed, otherwise, it will cause infinite loop.
SemanticsNode? traversalParent =
owner!._traversalParentNodes[child.getSemanticsData().traversalChildIdentifier];
final int? traversalParentId = traversalParent?.id;
while (traversalParent != null) {
if (traversalParent == child) {
throw FlutterError(
'The traversalParent $traversalParentId cannot be the child of the traversalChild ${child.id} in hit-test order',
);
}
traversalParent = traversalParent.parent;
}
continue;
}
updatedChildren.add(child);
}
// If the current node is a traversal parent node, it means that part of its
// traversal children might be on other branches of the hit-test tree and
// need to be grafted. To fix the traversal order, get the according traversal
// children from _traversalChildNodes and add them to the children list
// of this current node.
if (_isTraversalParent) {
if (owner != null && owner!._traversalChildNodes.containsKey(traversalParentIdentifier)) {
final Set<SemanticsNode> traversalChildren =
owner!._traversalChildNodes[traversalParentIdentifier]!;
// When traversal children are grafted from other branches, make sure
// these children are not ancestors of the traversal parent. Otherwise,
// it will cause infinite loop.
SemanticsNode currentNode = this;
while (currentNode.parent != null) {
currentNode = currentNode.parent!;
if (traversalChildren.contains(currentNode)) {
throw FlutterError(
'The traversalParent $id cannot be the child of the traversalChild ${currentNode.id} in hit-test order',
);
}
}
for (final SemanticsNode node in traversalChildren) {
if (node.attached) {
updatedChildren.add(node);
}
}
}
}
return updatedChildren;
}
/// Builds a new list made of [_children] sorted in semantic traversal order.
List<SemanticsNode> _childrenInTraversalOrder() {
final List<SemanticsNode>? updatedChildren = _updateChildrenInTraversalOrder();
TextDirection? inheritedTextDirection = textDirection;
SemanticsNode? ancestor = parent;
while (inheritedTextDirection == null && ancestor != null) {
@ -3779,12 +4096,11 @@ class SemanticsNode with DiagnosticableTreeMixin {
List<SemanticsNode>? childrenInDefaultOrder;
if (inheritedTextDirection != null) {
childrenInDefaultOrder = _childrenInDefaultOrder(_children!, inheritedTextDirection);
childrenInDefaultOrder = _childrenInDefaultOrder(updatedChildren!, inheritedTextDirection);
} else {
// In the absence of text direction default to paint order.
childrenInDefaultOrder = _children;
childrenInDefaultOrder = updatedChildren;
}
// List.sort does not guarantee stable sort order. Therefore, children are
// first partitioned into groups that have compatible sort keys, i.e. keys
// in the same group can be compared to each other. These groups stay in
@ -3819,7 +4135,6 @@ class SemanticsNode with DiagnosticableTreeMixin {
sortNodes.sort();
}
everythingSorted.addAll(sortNodes);
return everythingSorted
.map<SemanticsNode>((_TraversalSortNode sortNode) => sortNode.node)
.toList();
@ -3927,6 +4242,20 @@ class SemanticsNode with DiagnosticableTreeMixin {
properties.add(FlagProperty('isInvisible', value: isInvisible, ifTrue: 'invisible'));
properties.add(FlagProperty('isHidden', value: flagsCollection.isHidden, ifTrue: 'HIDDEN'));
properties.add(StringProperty('identifier', _identifier, defaultValue: ''));
properties.add(
DiagnosticsProperty<Object>(
'traversalParentIdentifier',
traversalParentIdentifier,
defaultValue: null,
),
);
properties.add(
DiagnosticsProperty<Object>(
'traversalChildIdentifier',
traversalChildIdentifier,
defaultValue: null,
),
);
properties.add(AttributedStringProperty('label', _attributedLabel));
properties.add(AttributedStringProperty('value', _attributedValue));
properties.add(AttributedStringProperty('increasedValue', _attributedIncreasedValue));
@ -4005,7 +4334,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
@override
List<DiagnosticsNode> debugDescribeChildren({
DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.inverseHitTest,
DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.traversalOrder,
}) {
return debugListChildrenInOrder(childOrder)
.map<DiagnosticsNode>(
@ -4343,6 +4672,8 @@ class SemanticsOwner extends ChangeNotifier {
final Set<SemanticsNode> _dirtyNodes = <SemanticsNode>{};
final Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{};
final Set<SemanticsNode> _detachedNodes = <SemanticsNode>{};
final Map<Object, SemanticsNode> _traversalParentNodes = <Object, SemanticsNode>{};
final Map<Object, Set<SemanticsNode>> _traversalChildNodes = <Object, Set<SemanticsNode>>{};
/// The root node of the semantics tree, if any.
///
@ -4355,6 +4686,8 @@ class SemanticsOwner extends ChangeNotifier {
_dirtyNodes.clear();
_nodes.clear();
_detachedNodes.clear();
_traversalChildNodes.clear();
_traversalParentNodes.clear();
super.dispose();
}
@ -4446,12 +4779,77 @@ class SemanticsOwner extends ChangeNotifier {
node._dirty = false; // Do not send update for this node, as it's now part of its parent
}
}
// Clean up the dirty entry in owner._traversalParentNodes map because it
// will be updated later.
_traversalParentNodes.removeWhere((Object key, SemanticsNode oldNode) => node == oldNode);
// Clean up the node from the value set in owner._traversalChildNodes.
for (final Set<SemanticsNode> childSet in _traversalChildNodes.values) {
childSet.removeWhere((SemanticsNode oldNode) => node == oldNode);
}
}
}
visitedNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
final SemanticsUpdateBuilder builder = SemanticsBinding.instance.createSemanticsUpdateBuilder();
final List<SemanticsNode> updatedVisitedNodes = <SemanticsNode>[];
for (final SemanticsNode node in visitedNodes) {
assert(node.parent?._dirty != true); // could be null (no parent) or false (not dirty)
final bool isTraversalParent = node._isTraversalParent;
final bool isTraversalChild = node._isTraversalChild;
if (kIsWeb) {
updatedVisitedNodes.add(node);
} else {
if (!isTraversalParent && !isTraversalChild) {
updatedVisitedNodes.add(node);
continue;
}
if (isTraversalChild) {
// If the node has a non-null `_traversalChildIdentifier`, it indicates
// that its hit-test parent and traversal parent are different, and
// its traversal parent should update its children to include this node.
// Therefore, its traversal parent node should be added to the
// `updatedVisitedNodes` list for later grafting, in order to generate
// a correct `childrenIntraversalOrder`. This is typically used in
// `OverlayPortal` widget.
final SemanticsNode? parentNode = _traversalParentNodes[node.traversalChildIdentifier];
if (parentNode != null && !updatedVisitedNodes.contains(parentNode)) {
updatedVisitedNodes.add(parentNode);
}
}
updatedVisitedNodes.add(node);
}
// If the node is a traversal parent, then add it to the
// _traversalParentNodes map for later grafting. Similarly, add the node
// to the _traversalChildNodes map if it is a traversal child.
if (isTraversalParent) {
assert(
!_traversalParentNodes.containsKey(node._traversalParentIdentifier) ||
_traversalParentNodes[node.traversalParentIdentifier!] == node,
'The traversalParentIdentifier must be unique. No two semantics nodes can share the same traversalParentIdentifier.',
);
_traversalParentNodes[node.traversalParentIdentifier!] = node;
} else if (isTraversalChild) {
_traversalChildNodes[node.traversalChildIdentifier!] ??= <SemanticsNode>{};
_traversalChildNodes[node.traversalChildIdentifier!]!.add(node);
}
}
for (final SemanticsNode node in updatedVisitedNodes) {
assert(
node.parent?._dirty != true || node._isTraversalParent,
); // could be null (no parent) or false (not dirty)
// The traversalParentNode is added to updatedVisitedNodes for later
// grafting; its traversalChildren should be grafted to its children in
// the traversal order. This grafting process is skipped on web because
// the traversal order will be handled in the web engine.
final bool needUpdateTraversalParent = !kIsWeb && node._isTraversalParent;
// The _serialize() method marks the node as not dirty, and
// recurses through the tree to do a deep serialization of all
// contiguous dirty nodes. This means that when we return here,
@ -4462,7 +4860,7 @@ class SemanticsOwner extends ChangeNotifier {
// calls reset() on its SemanticsNode if onlyChanges isn't set,
// which happens e.g. when the node is no longer contributing
// semantics).
if (node._dirty && node.attached) {
if ((node._dirty || needUpdateTraversalParent) && node.attached) {
node._addToUpdate(builder, customSemanticsActionIds);
}
}
@ -5344,6 +5742,28 @@ class SemanticsConfiguration {
_hasBeenAnnotated = true;
}
/// {@macro flutter.semantics.SemanticsProperties.traversalParentIdentifier}
Object? get traversalParentIdentifier => _traversalParentIdentifier;
Object? _traversalParentIdentifier;
set traversalParentIdentifier(Object? value) {
if (value == traversalParentIdentifier) {
return;
}
_traversalParentIdentifier = value;
_hasBeenAnnotated = true;
}
/// {@macro flutter.semantics.SemanticsProperties.traversalChildIdentifier}
Object? get traversalChildIdentifier => _traversalChildIdentifier;
Object? _traversalChildIdentifier;
set traversalChildIdentifier(Object? value) {
if (value == traversalChildIdentifier) {
return;
}
_traversalChildIdentifier = value;
_hasBeenAnnotated = true;
}
/// {@macro flutter.semantics.SemanticsProperties.role}
SemanticsRole get role => _role;
SemanticsRole _role = SemanticsRole.none;
@ -6080,7 +6500,16 @@ class SemanticsConfiguration {
/// Two configurations are said to be compatible if they can be added to the
/// same [SemanticsNode] without losing any semantics information.
bool isCompatibleWith(SemanticsConfiguration? other) {
if (other == null || !other.hasBeenAnnotated || !hasBeenAnnotated) {
if (other == null || !other.hasBeenAnnotated) {
return true;
}
// The parent node should reject child node as long as their
// traversalChildIdentifiers are different, even if the parent node has not
// been annotated.
if (_traversalChildIdentifier != other._traversalChildIdentifier) {
return false;
}
if (!hasBeenAnnotated) {
return true;
}
if (_actionsAsBits & other._actionsAsBits != 0) {
@ -6149,6 +6578,12 @@ class SemanticsConfiguration {
_platformViewId ??= child._platformViewId;
_maxValueLength ??= child._maxValueLength;
_currentValueLength ??= child._currentValueLength;
// A node cannot have both `_traversalChildIdentifier` and
// `_traversalParentIdentifier` not null.
if (_traversalChildIdentifier == null) {
_traversalParentIdentifier ??= child._traversalParentIdentifier;
}
_traversalChildIdentifier ??= child._traversalChildIdentifier;
_headingLevel = _mergeHeadingLevels(
sourceLevel: child._headingLevel,
@ -6223,6 +6658,8 @@ class SemanticsConfiguration {
.._textDirection = _textDirection
.._sortKey = _sortKey
.._identifier = _identifier
.._traversalParentIdentifier = _traversalParentIdentifier
.._traversalChildIdentifier = _traversalChildIdentifier
.._attributedLabel = _attributedLabel
.._attributedIncreasedValue = _attributedIncreasedValue
.._attributedValue = _attributedValue

View File

@ -4005,6 +4005,8 @@ sealed class _SemanticsBase extends SingleChildRenderObjectWidget {
required int? maxValueLength,
required int? currentValueLength,
required String? identifier,
required Object? traversalParentIdentifier,
required Object? traversalChildIdentifier,
required String? label,
required AttributedString? attributedLabel,
required String? value,
@ -4087,6 +4089,8 @@ sealed class _SemanticsBase extends SingleChildRenderObjectWidget {
maxValueLength: maxValueLength,
currentValueLength: currentValueLength,
identifier: identifier,
traversalParentIdentifier: traversalParentIdentifier,
traversalChildIdentifier: traversalChildIdentifier,
label: label,
attributedLabel: attributedLabel,
value: value,
@ -4335,6 +4339,8 @@ class SliverSemantics extends _SemanticsBase {
super.maxValueLength,
super.currentValueLength,
super.identifier,
super.traversalParentIdentifier,
super.traversalChildIdentifier,
super.label,
super.attributedLabel,
super.value,
@ -7911,6 +7917,8 @@ class Semantics extends _SemanticsBase {
super.maxValueLength,
super.currentValueLength,
super.identifier,
super.traversalParentIdentifier,
super.traversalChildIdentifier,
super.label,
super.attributedLabel,
super.value,

View File

@ -1500,6 +1500,8 @@ class _RenderTheater extends RenderBox
while (child != null) {
visitor(child);
final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
childParentData.visitOverlayPortalChildrenOnOverlayEntry(visitor);
child = childParentData.nextSibling;
}
}
@ -2021,12 +2023,19 @@ class _OverlayPortalState extends State<OverlayPortal> {
Widget build(BuildContext context) {
final int? zOrderIndex = _zOrderIndex;
if (zOrderIndex == null) {
return _OverlayPortal(overlayLocation: null, overlayChild: null, child: widget.child);
return _OverlayPortal(
overlayLocation: null,
overlayChild: null,
child: Semantics(traversalParentIdentifier: this, child: widget.child),
);
}
return _OverlayPortal(
overlayLocation: _getLocation(zOrderIndex, widget.overlayLocation),
overlayChild: _DeferredLayout(child: Builder(builder: widget.overlayChildBuilder)),
child: widget.child,
overlayChild: _DeferredLayout(
childIdentifier: this,
child: Builder(builder: widget.overlayChildBuilder),
),
child: Semantics(traversalParentIdentifier: this, child: widget.child),
);
}
}
@ -2406,8 +2415,11 @@ class _DeferredLayout extends SingleChildRenderObjectWidget {
// This widget must not be given a key: we currently do not support
// reparenting between the overlayChild and child.
required Widget child,
this.childIdentifier,
}) : super(child: child);
final Object? childIdentifier;
_RenderLayoutSurrogateProxyBox getLayoutParent(BuildContext context) {
return context.findAncestorRenderObjectOfType<_RenderLayoutSurrogateProxyBox>()!;
}
@ -2415,7 +2427,7 @@ class _DeferredLayout extends SingleChildRenderObjectWidget {
@override
_RenderDeferredLayoutBox createRenderObject(BuildContext context) {
final _RenderLayoutSurrogateProxyBox parent = getLayoutParent(context);
final _RenderDeferredLayoutBox renderObject = _RenderDeferredLayoutBox(parent);
final _RenderDeferredLayoutBox renderObject = _RenderDeferredLayoutBox(parent, childIdentifier);
parent._deferredLayoutChild = renderObject;
return renderObject;
}
@ -2424,6 +2436,7 @@ class _DeferredLayout extends SingleChildRenderObjectWidget {
void updateRenderObject(BuildContext context, _RenderDeferredLayoutBox renderObject) {
assert(renderObject._layoutSurrogate == getLayoutParent(context));
assert(getLayoutParent(context)._deferredLayoutChild == renderObject);
renderObject.childIdentifier = childIdentifier;
}
}
@ -2449,11 +2462,21 @@ class _DeferredLayout extends SingleChildRenderObjectWidget {
// like an `Overlay` that has only one entry.
final class _RenderDeferredLayoutBox extends RenderProxyBox
with _RenderTheaterMixin, LinkedListEntry<_RenderDeferredLayoutBox> {
_RenderDeferredLayoutBox(this._layoutSurrogate);
_RenderDeferredLayoutBox(this._layoutSurrogate, Object? childIdentifier)
: _childIdentifier = childIdentifier;
StackParentData get stackParentData => parentData! as StackParentData;
final _RenderLayoutSurrogateProxyBox _layoutSurrogate;
Object? get childIdentifier => _childIdentifier;
Object? _childIdentifier;
set childIdentifier(Object? value) {
if (_childIdentifier == childIdentifier) {
return;
}
_childIdentifier = value;
}
@override
Iterable<RenderBox> _childrenInPaintOrder() {
final RenderBox? child = this.child;
@ -2492,9 +2515,6 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox
super.markNeedsLayout();
}
@override
RenderObject? get semanticsParent => _layoutSurrogate;
@override
double? computeDryBaseline(BoxConstraints constraints, TextBaseline baseline) {
final RenderBox? child = this.child;
@ -2590,6 +2610,14 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox
_needsLayout = false;
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
if (childIdentifier != null) {
config.traversalChildIdentifier = childIdentifier;
}
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
final BoxParentData childParentData = child.parentData! as BoxParentData;
@ -2646,15 +2674,6 @@ class _RenderLayoutSurrogateProxyBox extends RenderProxyBox {
deferredChild._doLayoutFrom(this, constraints: BoxConstraints.tight(boxSize));
}
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
super.visitChildrenForSemantics(visitor);
final _RenderDeferredLayoutBox? deferredChild = _deferredLayoutChild;
if (deferredChild != null) {
visitor(deferredChild);
}
}
}
class _OverlayChildLayoutBuilder extends AbstractLayoutBuilder<OverlayChildLayoutInfo> {
@ -2719,6 +2738,7 @@ class _RenderLayoutBuilder extends RenderProxyBox
OverlayChildLayoutInfo get layoutInfo => _layoutInfo!;
// The size here is the child size of the regular child in its own parent's coordinates.
OverlayChildLayoutInfo? _layoutInfo;
OverlayChildLayoutInfo _computeNewLayoutInfo() {
final _RenderTheater theater = this.theater;
final _RenderDeferredLayoutBox parent = this.parent! as _RenderDeferredLayoutBox;

View File

@ -337,6 +337,9 @@ class _SemanticsDebuggerPainter extends CustomPainter {
}
void _paint(Canvas canvas, SemanticsNode node, int rank, int indexInParent, int level) {
if (node.traversalChildIdentifier != null) {
return;
}
canvas.save();
if (node.transform != null) {
canvas.transform(node.transform!.storage);

View File

@ -6,6 +6,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
class User {
const User({required this.email, required this.name});
@ -816,6 +818,44 @@ void main() {
expect(field2.controller!.text, textSelection);
});
testWidgets('Autocomplete suggestions are hit-tested before ListTiles', (
WidgetTester tester,
) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Column(
children: <Widget>[
Autocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) {
const List<String> options = <String>['Apple', 'Banana', 'Cherry'];
return options.where(
(String option) => option.toLowerCase().contains(textEditingValue.text),
);
},
),
for (int i = 0; i < 3; i++) ListTile(title: Text('Item $i'), onTap: () {}),
],
),
),
),
);
await tester.tap(find.byType(TextField));
await tester.pump();
final Finder cherryFinder = find.text('Cherry');
expect(cherryFinder, findsOneWidget);
await tester.tap(cherryFinder);
await tester.pump();
expect(find.widgetWithText(TextField, 'Cherry'), findsOneWidget);
semantics.dispose();
});
testWidgets('Autocomplete renders at zero area', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(

View File

@ -4377,13 +4377,18 @@ void main() {
children: <TestSemantics>[
TestSemantics(
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
flags: <SemanticsFlag>[
if (kIsWeb) SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.hasExpandedState,
children: <TestSemantics>[
TestSemantics(
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
flags: <SemanticsFlag>[
if (kIsWeb) SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.hasExpandedState,
],
label: 'ABC',
textDirection: TextDirection.ltr,
),
],
label: 'ABC',
textDirection: TextDirection.ltr,
),
],
),
@ -4425,61 +4430,67 @@ void main() {
children: <TestSemantics>[
TestSemantics(
id: 1,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
children: <TestSemantics>[
TestSemantics(
id: 3,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[
if (kIsWeb) SemanticsFlag.isButton,
SemanticsFlag.isFocused,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
SemanticsFlag.hasExpandedState,
SemanticsFlag.isExpanded,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.focus,
],
label: 'ABC',
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
),
TestSemantics(
id: 6,
rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 64.0),
children: <TestSemantics>[
TestSemantics(
id: 7,
rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0),
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
children: <TestSemantics>[
TestSemantics(
id: 8,
label: 'Item 0',
rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0),
flags: <SemanticsFlag>[
if (kIsWeb) SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.focus,
children: <TestSemantics>[
TestSemantics(
id: 9,
flags: <SemanticsFlag>[
if (kIsWeb) SemanticsFlag.isButton,
SemanticsFlag.hasImplicitScrolling,
],
children: <TestSemantics>[
TestSemantics(
id: 10,
label: 'Item 0',
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.focus,
],
),
],
),
],
),
],
),
TestSemantics(
id: 5,
label: 'ABC',
flags: <SemanticsFlag>[
SemanticsFlag.isFocused,
if (kIsWeb) SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
SemanticsFlag.hasExpandedState,
SemanticsFlag.isExpanded,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.focus,
],
),
],
),
],
@ -4491,12 +4502,14 @@ void main() {
],
),
ignoreTransform: true,
ignoreRect: true,
),
);
// Test collapsed state.
await tester.tap(find.text('ABC'));
await tester.pumpAndSettle();
expect(find.byType(MenuItemButton), findsNothing);
expect(
semantics,
hasSemantics(
@ -4504,33 +4517,36 @@ void main() {
children: <TestSemantics>[
TestSemantics(
id: 1,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
children: <TestSemantics>[
TestSemantics(
id: 3,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[
if (kIsWeb) SemanticsFlag.isButton,
SemanticsFlag.hasExpandedState,
SemanticsFlag.isFocused,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
children: <TestSemantics>[
TestSemantics(
id: 5,
label: 'ABC',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[
if (kIsWeb) SemanticsFlag.isButton,
SemanticsFlag.isFocused,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
SemanticsFlag.hasExpandedState,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.focus,
],
),
],
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.focus,
],
label: 'ABC',
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
),
],
),
@ -4541,12 +4557,13 @@ void main() {
],
),
ignoreTransform: true,
ignoreRect: true,
),
);
semantics.dispose();
});
});
}, skip: kIsWeb); // [intended] the web traversal order by using ARIA-OWNS.
// This is a regression test for https://github.com/flutter/flutter/issues/131676.
testWidgets('Material3 - Menu uses correct text styles', (WidgetTester tester) async {

View File

@ -1927,28 +1927,32 @@ void main() {
matchesSemantics(
children: <Matcher>[
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '10%',
increasedValue: '10%',
decreasedValue: '5%',
label: '',
),
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '12%',
increasedValue: '17%',
decreasedValue: '12%',
label: '',
children: <Matcher>[
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '10%',
increasedValue: '10%',
decreasedValue: '5%',
label: '',
),
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '12%',
increasedValue: '17%',
decreasedValue: '12%',
label: '',
),
],
),
],
),
@ -1987,28 +1991,32 @@ void main() {
matchesSemantics(
children: <Matcher>[
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '10%',
increasedValue: '15%',
decreasedValue: '5%',
rect: const Rect.fromLTRB(75.2, 276.0, 123.2, 324.0),
),
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '30%',
increasedValue: '35%',
decreasedValue: '25%',
rect: const Rect.fromLTRB(225.6, 276.0, 273.6, 324.0),
children: <Matcher>[
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '10%',
increasedValue: '15%',
decreasedValue: '5%',
rect: const Rect.fromLTRB(75.2, 276.0, 123.2, 324.0),
),
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '30%',
increasedValue: '35%',
decreasedValue: '25%',
rect: const Rect.fromLTRB(225.6, 276.0, 273.6, 324.0),
),
],
),
],
),
@ -2023,15 +2031,18 @@ void main() {
final List<Rect> rects = <Rect>[];
semanticsNode.visitChildren((SemanticsNode node) {
node.visitChildren((SemanticsNode node) {
// Round rect values to avoid floating point errors.
rects.add(
Rect.fromLTRB(
node.rect.left.roundToDouble(),
node.rect.top.roundToDouble(),
node.rect.right.roundToDouble(),
node.rect.bottom.roundToDouble(),
),
);
node.visitChildren((SemanticsNode node) {
// Round rect values to avoid floating point errors.
rects.add(
Rect.fromLTRB(
node.rect.left.roundToDouble(),
node.rect.top.roundToDouble(),
node.rect.right.roundToDouble(),
node.rect.bottom.roundToDouble(),
),
);
return true;
});
return true;
});
return true;
@ -2073,26 +2084,30 @@ void main() {
matchesSemantics(
children: <Matcher>[
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '10%',
increasedValue: '15%',
decreasedValue: '5%',
),
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '30%',
increasedValue: '35%',
decreasedValue: '25%',
children: <Matcher>[
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '10%',
increasedValue: '15%',
decreasedValue: '5%',
),
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '30%',
increasedValue: '35%',
decreasedValue: '25%',
),
],
),
],
),
@ -2107,15 +2122,18 @@ void main() {
final List<Rect> rects = <Rect>[];
semanticsNode.visitChildren((SemanticsNode node) {
node.visitChildren((SemanticsNode node) {
// Round rect values to avoid floating point errors.
rects.add(
Rect.fromLTRB(
node.rect.left.roundToDouble(),
node.rect.top.roundToDouble(),
node.rect.right.roundToDouble(),
node.rect.bottom.roundToDouble(),
),
);
node.visitChildren((SemanticsNode node) {
// Round rect values to avoid floating point errors.
rects.add(
Rect.fromLTRB(
node.rect.left.roundToDouble(),
node.rect.top.roundToDouble(),
node.rect.right.roundToDouble(),
node.rect.bottom.roundToDouble(),
),
);
return true;
});
return true;
});
return true;
@ -2621,29 +2639,33 @@ void main() {
matchesSemantics(
children: <Matcher>[
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
isFocused: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '10%',
increasedValue: '15%',
decreasedValue: '5%',
rect: const Rect.fromLTRB(75.2, 276.0, 123.2, 324.0),
),
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '30%',
increasedValue: '35%',
decreasedValue: '25%',
rect: const Rect.fromLTRB(225.6, 276.0, 273.6, 324.0),
children: <Matcher>[
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
isFocused: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '10%',
increasedValue: '15%',
decreasedValue: '5%',
rect: const Rect.fromLTRB(75.2, 276.0, 123.2, 324.0),
),
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '30%',
increasedValue: '35%',
decreasedValue: '25%',
rect: const Rect.fromLTRB(225.6, 276.0, 273.6, 324.0),
),
],
),
],
),
@ -2664,29 +2686,33 @@ void main() {
matchesSemantics(
children: <Matcher>[
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '10%',
increasedValue: '15%',
decreasedValue: '5%',
rect: const Rect.fromLTRB(75.2, 276.0, 123.2, 324.0),
),
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
isFocused: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '30%',
increasedValue: '35%',
decreasedValue: '25%',
rect: const Rect.fromLTRB(225.6, 276.0, 273.6, 324.0),
children: <Matcher>[
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '10%',
increasedValue: '15%',
decreasedValue: '5%',
rect: const Rect.fromLTRB(75.2, 276.0, 123.2, 324.0),
),
matchesSemantics(
isEnabled: true,
isSlider: true,
isFocusable: true,
isFocused: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '30%',
increasedValue: '35%',
decreasedValue: '25%',
rect: const Rect.fromLTRB(225.6, 276.0, 273.6, 324.0),
),
],
),
],
),

View File

@ -1352,6 +1352,7 @@ void main() {
increasedValue: '55%',
decreasedValue: '45%',
textDirection: TextDirection.ltr,
children: <TestSemantics>[TestSemantics(id: 5)],
),
],
),
@ -1405,6 +1406,7 @@ void main() {
increasedValue: '55%',
decreasedValue: '45%',
textDirection: TextDirection.ltr,
children: <TestSemantics>[TestSemantics(id: 5)],
),
],
),
@ -1446,6 +1448,7 @@ void main() {
increasedValue: '55%',
decreasedValue: '45%',
textDirection: TextDirection.ltr,
children: <TestSemantics>[TestSemantics(id: 5)],
),
],
),
@ -1467,6 +1470,7 @@ void main() {
TargetPlatform.fuchsia,
TargetPlatform.linux,
}),
skip: kIsWeb, // [intended] the web traversal order by using ARIA-OWNS.
);
testWidgets(
@ -1520,6 +1524,7 @@ void main() {
increasedValue: '60%',
decreasedValue: '40%',
textDirection: TextDirection.ltr,
children: <TestSemantics>[TestSemantics(id: 5)],
),
],
),
@ -1561,7 +1566,7 @@ void main() {
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 5,
id: 6,
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isSlider,
@ -1570,6 +1575,7 @@ void main() {
increasedValue: '60%',
decreasedValue: '40%',
textDirection: TextDirection.ltr,
children: <TestSemantics>[TestSemantics(id: 7)],
),
],
),
@ -1589,170 +1595,179 @@ void main() {
TargetPlatform.iOS,
TargetPlatform.macOS,
}),
skip: kIsWeb, // [intended] the web traversal order by using ARIA-OWNS.
);
testWidgets('Slider Semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
testWidgets(
'Slider Semantics',
(WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(child: Slider(value: 0.5, onChanged: (double v) {})),
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(child: Slider(value: 0.5, onChanged: (double v) {})),
),
),
),
);
);
await tester.pumpAndSettle();
await tester.pumpAndSettle();
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
SemanticsFlag.isSlider,
],
actions: <SemanticsAction>[
SemanticsAction.focus,
SemanticsAction.increase,
SemanticsAction.decrease,
SemanticsAction.didGainAccessibilityFocus,
],
value: '50%',
increasedValue: '55%',
decreasedValue: '45%',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
SemanticsFlag.isSlider,
],
actions: <SemanticsAction>[
SemanticsAction.focus,
SemanticsAction.increase,
SemanticsAction.decrease,
SemanticsAction.didGainAccessibilityFocus,
],
value: '50%',
increasedValue: '55%',
decreasedValue: '45%',
textDirection: TextDirection.ltr,
children: <TestSemantics>[TestSemantics(id: 5)],
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
ignoreRect: true,
ignoreTransform: true,
),
);
);
// Disable slider
await tester.pumpWidget(
const MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(child: Slider(value: 0.5, onChanged: null)),
// Disable slider
await tester.pumpWidget(
const MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(child: Slider(value: 0.5, onChanged: null)),
),
),
),
);
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
// isFocusable is delayed by 1 frame.
SemanticsFlag.isFocusable,
SemanticsFlag.isSlider,
],
actions: <SemanticsAction>[
SemanticsAction.focus,
SemanticsAction.didGainAccessibilityFocus,
],
value: '50%',
increasedValue: '55%',
decreasedValue: '45%',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
// isFocusable is delayed by 1 frame.
SemanticsFlag.isFocusable,
SemanticsFlag.isSlider,
],
actions: <SemanticsAction>[
SemanticsAction.focus,
SemanticsAction.didGainAccessibilityFocus,
],
value: '50%',
increasedValue: '55%',
decreasedValue: '45%',
textDirection: TextDirection.ltr,
children: <TestSemantics>[TestSemantics(id: 5)],
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
ignoreRect: true,
ignoreTransform: true,
),
);
);
await tester.pump();
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isSlider,
],
actions: <SemanticsAction>[SemanticsAction.didGainAccessibilityFocus],
value: '50%',
increasedValue: '55%',
decreasedValue: '45%',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
await tester.pump();
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isSlider,
],
actions: <SemanticsAction>[SemanticsAction.didGainAccessibilityFocus],
value: '50%',
increasedValue: '55%',
decreasedValue: '45%',
textDirection: TextDirection.ltr,
children: <TestSemantics>[TestSemantics(id: 5)],
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
ignoreRect: true,
ignoreTransform: true,
),
);
);
semantics.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.windows}));
semantics.dispose();
},
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.windows}),
skip: kIsWeb, // [intended] the web traversal order by using ARIA-OWNS.
);
testWidgets('Slider semantics with custom formatter', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
@ -1807,6 +1822,7 @@ void main() {
increasedValue: '60',
decreasedValue: '20',
textDirection: TextDirection.ltr,
children: <TestSemantics>[TestSemantics(id: 5)],
),
],
),
@ -1821,7 +1837,7 @@ void main() {
),
);
semantics.dispose();
});
}, skip: kIsWeb); // [intended] the web traversal order by using ARIA-OWNS.
// Regression test for https://github.com/flutter/flutter/issues/101868
testWidgets('Slider.label info should not write to semantic node', (WidgetTester tester) async {
@ -1880,6 +1896,7 @@ void main() {
decreasedValue: '20',
textDirection: TextDirection.ltr,
label: label,
children: <TestSemantics>[TestSemantics(id: 5)],
),
],
),
@ -1894,7 +1911,7 @@ void main() {
),
);
semantics.dispose();
});
}, skip: kIsWeb); // [intended] the web traversal order by using ARIA-OWNS.
testWidgets('Material3 - Slider is focusable and has correct focus color', (
WidgetTester tester,
@ -2828,6 +2845,7 @@ void main() {
increasedValue: '55%',
decreasedValue: '45%',
textDirection: TextDirection.ltr,
children: <TestSemantics>[TestSemantics(id: 5)],
),
],
),
@ -2849,6 +2867,7 @@ void main() {
semantics.dispose();
},
variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.windows}),
skip: kIsWeb, // [intended] the web traversal order by using ARIA-OWNS.
);
testWidgets('Value indicator appears when it should', (WidgetTester tester) async {

View File

@ -1993,10 +1993,20 @@ void main() {
await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
expect(semantics, hasSemantics(expected, ignoreTransform: true, ignoreRect: true));
final TestSemantics expected1 = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
tooltip: 'TIP',
textDirection: TextDirection.ltr,
children: <TestSemantics>[TestSemantics(id: 2)],
),
],
);
expect(semantics, hasSemantics(expected1, ignoreTransform: true, ignoreRect: true));
semantics.dispose();
});
}, skip: kIsWeb); // [intended] the web traversal order by using ARIA-OWNS.
testWidgets('Tooltip semantics does not merge into child', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
@ -2047,17 +2057,25 @@ void main() {
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
children: <TestSemantics>[
TestSemantics(
id: 5,
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
children: <TestSemantics>[
TestSemantics(label: 'before'),
TestSemantics(id: 2, label: 'before'),
TestSemantics(
id: 3,
label: 'child',
tooltip: 'B',
children: <TestSemantics>[TestSemantics(label: 'B')],
children: <TestSemantics>[
TestSemantics(
id: 6,
children: <TestSemantics>[TestSemantics(id: 7, label: 'B')],
),
],
),
TestSemantics(label: 'after'),
TestSemantics(id: 4, label: 'after'),
],
),
],
@ -2071,7 +2089,7 @@ void main() {
);
semantics.dispose();
});
}, skip: kIsWeb); // [intended] the web traversal order by using ARIA-OWNS.
testWidgets('Material2 - Tooltip text scales with textScaler', (WidgetTester tester) async {
Widget buildApp(String text, {required TextScaler textScaler}) {

View File

@ -704,6 +704,8 @@ void main() {
' invisible\n'
' isHidden: false\n'
' identifier: ""\n'
' traversalParentIdentifier: null\n'
' traversalChildIdentifier: null\n'
' label: ""\n'
' value: ""\n'
' increasedValue: ""\n'
@ -850,6 +852,8 @@ void main() {
' invisible\n'
' isHidden: false\n'
' identifier: ""\n'
' traversalParentIdentifier: null\n'
' traversalChildIdentifier: null\n'
' label: ""\n'
' value: ""\n'
' increasedValue: ""\n'

View File

@ -203,6 +203,7 @@ class SemanticsUpdateBuilderSpy extends Fake implements ui.SemanticsUpdateBuilde
required int platformViewId,
required int scrollChildren,
required int scrollIndex,
required int? traversalParent,
required double scrollPosition,
required double scrollExtentMax,
required double scrollExtentMin,
@ -221,6 +222,7 @@ class SemanticsUpdateBuilderSpy extends Fake implements ui.SemanticsUpdateBuilde
String? tooltip,
TextDirection? textDirection,
required Float64List transform,
required Float64List hitTestTransform,
required Int32List childrenInTraversalOrder,
required Int32List childrenInHitTestOrder,
required Int32List additionalActions,

View File

@ -4,6 +4,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/src/foundation/constants.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
@ -212,7 +213,7 @@ void main() {
});
// stop before updating semantics.
await tester.pump(null, EnginePhase.composite);
expect(renderObject.debugNeedsSemanticsUpdate, isFalse);
expect(renderObject.debugNeedsSemanticsUpdate, isTrue);
});
testWidgets('Safe to deactivate and re-activate OverlayPortal', (WidgetTester tester) async {
@ -2031,79 +2032,6 @@ void main() {
verifyTreeIsClean();
});
testWidgets('Nested overlay children: swap inner and outer', (WidgetTester tester) async {
final GlobalKey outerKey = GlobalKey(debugLabel: 'Original Outer Widget');
final GlobalKey innerKey = GlobalKey(debugLabel: 'Original Inner Widget');
final RenderBox child1Box = RenderConstrainedBox(
additionalConstraints: const BoxConstraints(),
);
final RenderBox child2Box = RenderConstrainedBox(
additionalConstraints: const BoxConstraints(),
);
final RenderBox overlayChildBox = RenderConstrainedBox(
additionalConstraints: const BoxConstraints(),
);
addTearDown(overlayChildBox.dispose);
late StateSetter setState;
bool swapped = false;
// WidgetToRenderBoxAdapter has its own builtin GlobalKey.
final Widget child1 = WidgetToRenderBoxAdapter(renderBox: child1Box);
final Widget child2 = WidgetToRenderBoxAdapter(renderBox: child2Box);
final Widget child3 = WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
late final OverlayEntry entry;
addTearDown(() {
entry.remove();
entry.dispose();
});
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
entry = OverlayEntry(
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter stateSetter) {
setState = stateSetter;
return OverlayPortal(
key: swapped ? outerKey : innerKey,
controller: swapped ? controller2 : controller1,
overlayChildBuilder: (BuildContext context) {
return OverlayPortal(
key: swapped ? innerKey : outerKey,
controller: swapped ? controller1 : controller2,
overlayChildBuilder: (BuildContext context) {
return OverlayPortal(
controller: OverlayPortalController(),
overlayChildBuilder: (BuildContext context) => child3,
);
},
child: child2,
);
},
child: child1,
);
},
);
},
),
],
),
),
);
setState(() {
swapped = true;
});
await tester.pump();
verifyTreeIsClean();
});
testWidgets('Paint order', (WidgetTester tester) async {
final GlobalKey outerKey = GlobalKey(debugLabel: 'Original Outer Widget');
final GlobalKey innerKey = GlobalKey(debugLabel: 'Original Inner Widget');
@ -2866,7 +2794,7 @@ void main() {
final Matrix4 node1Transform = Matrix4.identity()
..scale(3.0, 3.0, 1.0)
..translate(0.0, TestSemantics.fullScreen.height - 10.0);
final Matrix4 node4Transform = node1Transform.clone()..translate(10.0);
final Matrix4 node3Transform = node1Transform.clone()..translate(10.0);
final TestSemantics expected = TestSemantics.root(
children: <TestSemantics>[
@ -2875,28 +2803,42 @@ void main() {
rect: Offset.zero & const Size(10, 10),
transform: node1Transform,
children: <TestSemantics>[
TestSemantics(id: 2, label: 'A', rect: Offset.zero & const Size(10, 10)),
// The crossAxisAlignment is set to `end`. The size of node 1 is 30 x 10.
TestSemantics(
id: 3,
label: 'BBBB',
rect: Offset.zero & const Size(40, 10),
transform: Matrix4.translationValues(0, -rowOriginY, 0),
id: 2,
label: 'A',
rect: Offset.zero & const Size(10, 10),
children: <TestSemantics>[
// The crossAxisAlignment is set to `end`. The size of node 1 is 30 x 10.
TestSemantics(
id: 4,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
transform: Matrix4.identity()
..scale(1 / 3, 1 / 3, 1)
..setTranslationRaw(0, -rowOriginY, 0),
children: <TestSemantics>[
TestSemantics(
id: 5,
label: 'BBBB',
rect: const Rect.fromLTRB(0.0, 0.0, 40.0, 10.0),
),
],
),
],
),
],
),
TestSemantics(
id: 4,
id: 3,
label: 'CC',
rect: Offset.zero & const Size(20, 10),
transform: node4Transform,
transform: node3Transform,
),
],
);
expect(semantics, hasSemantics(expected));
semantics.dispose();
});
}, skip: kIsWeb); // [intended] the web traversal order by using ARIA-OWNS.
testWidgets('OverlayPortal overlay child clipping', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
@ -3010,11 +2952,10 @@ void main() {
final SemanticsNode clippedOverlayChild = semantics.nodesWith(label: 'B').single;
expect(clippedOverlayPortal.rect, Offset.zero & const Size(800, 10));
expect(clippedOverlayChild.rect, Offset.zero & const Size(10, 10));
expect(clippedOverlayChild.rect, Offset.zero & const Size(10.0, 10.0));
expect(clippedOverlayPortal.transform, isNull);
// The parent SemanticsNode is created by OverlayPortal.
expect(clippedOverlayChild.transform, Matrix4.translationValues(0.0, -600.0, 0.0));
expect(clippedOverlayChild.transform, isNull);
semantics.dispose();
});
@ -3080,7 +3021,7 @@ void main() {
await tester.pump();
expect(semantics.nodesWith(label: 'A'), isEmpty);
expect(semantics.nodesWith(label: 'B'), isEmpty);
expect(semantics.nodesWith(label: 'B'), isNotEmpty);
semantics.dispose();
final RenderObject overlayRenderObject = tester.renderObject(find.byType(Overlay));

View File

@ -5,6 +5,7 @@
import 'dart:math';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
@ -2187,6 +2188,200 @@ void main() {
semantics.dispose();
});
testWidgets('semantics grafting in traversal order', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const String identifier = '111';
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Scaffold(
body: Column(
children: <Widget>[
Semantics(
traversalParentIdentifier: identifier,
child: const SizedBox.square(dimension: 10),
),
Semantics(
traversalChildIdentifier: identifier,
child: const SizedBox.square(dimension: 10),
),
const SizedBox.square(dimension: 10),
],
),
),
),
);
// Semantics tree in traversal order. On web, no grafting.
expect(
semantics,
kIsWeb
? hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(id: 1, traversalParentIdentifier: identifier),
TestSemantics(id: 2, traversalChildIdentifier: identifier),
],
),
ignoreRect: true,
ignoreTransform: true,
)
: hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
traversalParentIdentifier: identifier,
children: <TestSemantics>[
TestSemantics(id: 2, traversalChildIdentifier: identifier),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
// Semantics tree in hit-test order.
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(id: 1, traversalParentIdentifier: identifier),
TestSemantics(id: 2, traversalChildIdentifier: identifier),
],
),
ignoreRect: true,
ignoreTransform: true,
childOrder: DebugSemanticsDumpOrder.inverseHitTest,
),
);
semantics.dispose();
});
testWidgets('semantics grafting in traversal order with multiple same traversalChildIdentifier', (
WidgetTester tester,
) async {
final SemanticsTester semantics = SemanticsTester(tester);
const String identifier = '111';
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Scaffold(
body: Column(
children: <Widget>[
Semantics(
traversalParentIdentifier: identifier,
child: const SizedBox.square(dimension: 10),
),
Semantics(
traversalChildIdentifier: identifier,
child: const SizedBox.square(dimension: 10),
),
Semantics(
traversalChildIdentifier: identifier,
child: const SizedBox.square(dimension: 10),
),
const SizedBox.square(dimension: 10),
],
),
),
),
);
// Semantics tree in traversal order. On web, no grafting.
expect(
semantics,
kIsWeb
? hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(id: 1, traversalParentIdentifier: identifier),
TestSemantics(id: 2, traversalChildIdentifier: identifier),
TestSemantics(id: 3, traversalChildIdentifier: identifier),
],
),
ignoreRect: true,
ignoreTransform: true,
)
: hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
traversalParentIdentifier: identifier,
children: <TestSemantics>[
TestSemantics(id: 2, traversalChildIdentifier: identifier),
TestSemantics(id: 3, traversalChildIdentifier: identifier),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
// Semantics tree in hit-test order.
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(id: 1, traversalParentIdentifier: identifier),
TestSemantics(id: 2, traversalChildIdentifier: identifier),
TestSemantics(id: 3, traversalChildIdentifier: identifier),
],
),
ignoreRect: true,
ignoreTransform: true,
childOrder: DebugSemanticsDumpOrder.inverseHitTest,
),
);
semantics.dispose();
});
testWidgets(
'no grafting in traversal order when traversal child is the parent of its traversal parent',
(WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const String identifier = '111';
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Scaffold(
body: Column(
children: <Widget>[
Semantics(
traversalChildIdentifier: identifier,
child: TextButton(
onPressed: () {},
child: Semantics(traversalParentIdentifier: identifier),
),
),
const SizedBox.square(dimension: 10),
],
),
),
),
);
final Object? exception = tester.takeException();
expect(exception, isFlutterError);
final FlutterError error = exception! as FlutterError;
expect(
error.message,
'The traversalParent 2 cannot be the child of the traversalChild 1 in hit-test order',
);
semantics.dispose();
},
skip: kIsWeb, // [intended] the web traversal order by using ARIA-OWNS.
);
}
class CustomSortKey extends OrdinalSortKey {

View File

@ -61,6 +61,8 @@ class TestSemantics {
this.maxValueLength,
this.currentValueLength,
this.identifier = '',
this.traversalParentIdentifier,
this.traversalChildIdentifier,
this.hintOverrides,
}) : assert(flags is int || flags is List<SemanticsFlag> || flags is SemanticsFlags),
assert(actions is int || actions is List<SemanticsAction>),
@ -93,6 +95,8 @@ class TestSemantics {
this.maxValueLength,
this.currentValueLength,
this.identifier = '',
this.traversalParentIdentifier,
this.traversalChildIdentifier,
this.hintOverrides,
}) : id = 0,
assert(flags is int || flags is List<SemanticsFlag> || flags is SemanticsFlags),
@ -137,6 +141,8 @@ class TestSemantics {
this.maxValueLength,
this.currentValueLength,
this.identifier = '',
this.traversalParentIdentifier,
this.traversalChildIdentifier,
this.hintOverrides,
}) : assert(flags is int || flags is List<SemanticsFlag> || flags is SemanticsFlags),
assert(actions is int || actions is List<SemanticsAction>),
@ -285,6 +291,16 @@ class TestSemantics {
/// Defaults to an empty string if not set.
final String identifier;
/// The expected traversalParentIdentifier for the node.
///
/// Defaults to null if not set.
final Object? traversalParentIdentifier;
/// The expected traversalChildIdentifier for the node.
///
/// Defaults to null if not set.
final Object? traversalChildIdentifier;
/// The expected hint overrides for the node.
///
/// Defaults to null if not set.
@ -310,6 +326,7 @@ class TestSemantics {
bool ignoreRect = false,
bool ignoreTransform = false,
bool ignoreId = false,
bool ignoreTraversalIdentifier = false,
DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.inverseHitTest,
}) {
bool fail(String message) {
@ -426,7 +443,14 @@ class TestSemantics {
'expected node id $id to have scrollIndex $scrollChildren but found scrollIndex ${nodeData.scrollChildCount}.',
);
}
final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount;
final int childrenCount;
if (childOrder == DebugSemanticsDumpOrder.traversalOrder) {
childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCountInTraversalOrder;
} else {
childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount;
}
if (children.length != childrenCount) {
return fail(
'expected node id $id to have ${children.length} child${children.length == 1 ? "" : "ren"} but found $childrenCount.',
@ -474,10 +498,17 @@ class TestSemantics {
'expected node id $id to have current value length $currentValueLength but found current value length ${node.currentValueLength}',
);
}
if (identifier != node.identifier) {
return fail(
'expected node id $id to have identifier $identifier but found identifier ${node.identifier}',
);
if (!ignoreTraversalIdentifier) {
if (traversalChildIdentifier != node.traversalChildIdentifier) {
return fail(
'expected node id $id to have traversalChildIdentifier $traversalChildIdentifier but found identifier ${node.traversalChildIdentifier}',
);
}
if (traversalParentIdentifier != node.traversalParentIdentifier) {
return fail(
'expected node id $id to have traversalParentIdentifier $traversalParentIdentifier but found identifier ${node.traversalParentIdentifier}',
);
}
}
if (hintOverrides != node.hintOverrides) {
return fail(
@ -498,6 +529,7 @@ class TestSemantics {
ignoreRect: ignoreRect,
ignoreTransform: ignoreTransform,
ignoreId: ignoreId,
ignoreTraversalIdentifier: ignoreTraversalIdentifier,
childOrder: childOrder,
);
if (!childMatches) {
@ -973,6 +1005,7 @@ class _HasSemantics extends Matcher {
required this.ignoreRect,
required this.ignoreTransform,
required this.ignoreId,
required this.ignoreTraversalIdentifier,
required this.childOrder,
});
@ -980,6 +1013,7 @@ class _HasSemantics extends Matcher {
final bool ignoreRect;
final bool ignoreTransform;
final bool ignoreId;
final bool ignoreTraversalIdentifier;
final DebugSemanticsDumpOrder childOrder;
@override
@ -990,6 +1024,7 @@ class _HasSemantics extends Matcher {
ignoreTransform: ignoreTransform,
ignoreRect: ignoreRect,
ignoreId: ignoreId,
ignoreTraversalIdentifier: ignoreTraversalIdentifier,
childOrder: childOrder,
);
if (!doesMatch) {
@ -1051,6 +1086,7 @@ Matcher hasSemantics(
bool ignoreRect = false,
bool ignoreTransform = false,
bool ignoreId = false,
bool ignoreTraversalIdentifier = true,
DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.traversalOrder,
}) {
return _HasSemantics(
@ -1058,6 +1094,7 @@ Matcher hasSemantics(
ignoreRect: ignoreRect,
ignoreTransform: ignoreTransform,
ignoreId: ignoreId,
ignoreTraversalIdentifier: ignoreTraversalIdentifier,
childOrder: childOrder,
);
}

View File

@ -664,6 +664,8 @@ AsyncMatcher matchesReferenceImage(ui.Image image) {
/// * [containsSemantics], a similar matcher without default values for flags or actions.
Matcher matchesSemantics({
String? identifier,
String? traversalParentIdentifier,
String? traversalChildIdentifier,
String? label,
AttributedString? attributedLabel,
String? hint,
@ -748,6 +750,8 @@ Matcher matchesSemantics({
}) {
return _MatchesSemanticsData(
identifier: identifier,
traversalParentIdentifier: traversalParentIdentifier,
traversalChildIdentifier: traversalChildIdentifier,
label: label,
attributedLabel: attributedLabel,
hint: hint,
@ -860,6 +864,8 @@ Matcher matchesSemantics({
/// * [matchesSemantics], a similar matcher with default values for flags and actions.
Matcher containsSemantics({
String? identifier,
String? traversalParentIdentifier,
String? traversalChildIdentifier,
String? label,
AttributedString? attributedLabel,
String? hint,
@ -944,6 +950,8 @@ Matcher containsSemantics({
}) {
return _MatchesSemanticsData(
identifier: identifier,
traversalChildIdentifier: traversalChildIdentifier,
traversalParentIdentifier: traversalParentIdentifier,
label: label,
attributedLabel: attributedLabel,
hint: hint,
@ -2375,6 +2383,8 @@ class _MatchesReferenceImage extends AsyncMatcher {
class _MatchesSemanticsData extends Matcher {
_MatchesSemanticsData({
required this.identifier,
required this.traversalParentIdentifier,
required this.traversalChildIdentifier,
required this.label,
required this.attributedLabel,
required this.hint,
@ -2517,6 +2527,8 @@ class _MatchesSemanticsData extends Matcher {
: SemanticsHintOverrides(onTapHint: onTapHint, onLongPressHint: onLongPressHint);
final String? identifier;
final String? traversalParentIdentifier;
final String? traversalChildIdentifier;
final String? label;
final AttributedString? attributedLabel;
final String? hint;

View File

@ -728,6 +728,8 @@ void main() {
flagsCollection: allFlags,
actions: actions,
identifier: 'i',
traversalParentIdentifier: '01',
traversalChildIdentifier: '01',
attributedLabel: AttributedString('a'),
attributedIncreasedValue: AttributedString('b'),
attributedValue: AttributedString('c'),
@ -1028,6 +1030,8 @@ void main() {
flagsCollection: allFlags,
actions: actions,
identifier: 'i',
traversalChildIdentifier: '01',
traversalParentIdentifier: '01',
attributedLabel: AttributedString('a'),
attributedIncreasedValue: AttributedString('b'),
attributedValue: AttributedString('c'),
@ -1129,6 +1133,8 @@ void main() {
flagsCollection: SemanticsFlags.none,
actions: 0,
identifier: 'i',
traversalParentIdentifier: '01',
traversalChildIdentifier: '01',
attributedLabel: AttributedString('a'),
attributedIncreasedValue: AttributedString('b'),
attributedValue: AttributedString('c'),
@ -1234,6 +1240,8 @@ void main() {
flagsCollection: SemanticsFlags.none,
actions: 0,
identifier: 'i',
traversalChildIdentifier: '01',
traversalParentIdentifier: '01',
attributedLabel: AttributedString('a'),
attributedIncreasedValue: AttributedString('b'),
attributedValue: AttributedString('c'),
@ -1266,6 +1274,8 @@ void main() {
flagsCollection: allFlags,
actions: allActions,
identifier: 'i',
traversalChildIdentifier: '01',
traversalParentIdentifier: '01',
attributedLabel: AttributedString('a'),
attributedIncreasedValue: AttributedString('b'),
attributedValue: AttributedString('c'),
@ -1398,6 +1408,8 @@ void main() {
flagsCollection: SemanticsFlags.none,
actions: SemanticsAction.customAction.index,
identifier: 'i',
traversalChildIdentifier: '01',
traversalParentIdentifier: '01',
attributedLabel: AttributedString('a'),
attributedIncreasedValue: AttributedString('b'),
attributedValue: AttributedString('c'),