mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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:
parent
c99db0dc0d
commit
fccfa978a9
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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() +
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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];
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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: '',
|
||||
|
||||
@ -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!;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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}) {
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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'),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user