Adds a widgets that blocks all semantics of widgets below it in paint order within the same container (#10425)
This commit is contained in:
parent
417df36b45
commit
104725f384
@ -270,10 +270,12 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
|
||||
child: new RepaintBoundary(
|
||||
child: new Stack(
|
||||
children: <Widget>[
|
||||
new GestureDetector(
|
||||
onTap: close,
|
||||
child: new Container(
|
||||
color: _color.evaluate(_controller)
|
||||
new BlockSemantics(
|
||||
child: new GestureDetector(
|
||||
onTap: close,
|
||||
child: new Container(
|
||||
color: _color.evaluate(_controller)
|
||||
),
|
||||
),
|
||||
),
|
||||
new Align(
|
||||
|
@ -662,7 +662,8 @@ abstract class _SemanticsFragment {
|
||||
_SemanticsFragment({
|
||||
@required RenderObject renderObjectOwner,
|
||||
this.annotator,
|
||||
List<_SemanticsFragment> children
|
||||
List<_SemanticsFragment> children,
|
||||
this.dropSemanticsOfPreviousSiblings,
|
||||
}) {
|
||||
assert(renderObjectOwner != null);
|
||||
_ancestorChain = <RenderObject>[renderObjectOwner];
|
||||
@ -678,6 +679,9 @@ abstract class _SemanticsFragment {
|
||||
}
|
||||
|
||||
final SemanticsAnnotator annotator;
|
||||
bool dropSemanticsOfPreviousSiblings;
|
||||
|
||||
bool get producesSemanticNodes => true;
|
||||
|
||||
List<RenderObject> _ancestorChain;
|
||||
void addAncestor(RenderObject ancestor) {
|
||||
@ -695,6 +699,20 @@ abstract class _SemanticsFragment {
|
||||
String toString() => '$runtimeType#$hashCode';
|
||||
}
|
||||
|
||||
/// A SemanticsFragment that doesn't produce any [SemanticsNode]s when compiled.
|
||||
class _EmptySemanticsFragment extends _SemanticsFragment {
|
||||
_EmptySemanticsFragment({
|
||||
@required RenderObject renderObjectOwner,
|
||||
bool dropSemanticsOfPreviousSiblings,
|
||||
}) : super(renderObjectOwner: renderObjectOwner, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings);
|
||||
|
||||
@override
|
||||
Iterable<SemanticsNode> compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics }) sync* { }
|
||||
|
||||
@override
|
||||
bool get producesSemanticNodes => false;
|
||||
}
|
||||
|
||||
/// Represents a [RenderObject] which is in no way dirty.
|
||||
///
|
||||
/// This class has no children and no annotators, and when compiled, it returns
|
||||
@ -702,8 +720,9 @@ abstract class _SemanticsFragment {
|
||||
/// the matrix, since that comes from the (dirty) ancestors.)
|
||||
class _CleanSemanticsFragment extends _SemanticsFragment {
|
||||
_CleanSemanticsFragment({
|
||||
@required RenderObject renderObjectOwner
|
||||
}) : super(renderObjectOwner: renderObjectOwner) {
|
||||
@required RenderObject renderObjectOwner,
|
||||
bool dropSemanticsOfPreviousSiblings,
|
||||
}) : super(renderObjectOwner: renderObjectOwner, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings) {
|
||||
assert(renderObjectOwner != null);
|
||||
assert(renderObjectOwner._semantics != null);
|
||||
}
|
||||
@ -728,8 +747,9 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
|
||||
_InterestingSemanticsFragment({
|
||||
RenderObject renderObjectOwner,
|
||||
SemanticsAnnotator annotator,
|
||||
Iterable<_SemanticsFragment> children
|
||||
}) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children);
|
||||
Iterable<_SemanticsFragment> children,
|
||||
bool dropSemanticsOfPreviousSiblings,
|
||||
}) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings);
|
||||
|
||||
bool get haveConcreteNode => true;
|
||||
|
||||
@ -765,8 +785,9 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
|
||||
_RootSemanticsFragment({
|
||||
RenderObject renderObjectOwner,
|
||||
SemanticsAnnotator annotator,
|
||||
Iterable<_SemanticsFragment> children
|
||||
}) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children);
|
||||
Iterable<_SemanticsFragment> children,
|
||||
bool dropSemanticsOfPreviousSiblings,
|
||||
}) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings);
|
||||
|
||||
@override
|
||||
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) {
|
||||
@ -798,8 +819,9 @@ class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment {
|
||||
_ConcreteSemanticsFragment({
|
||||
RenderObject renderObjectOwner,
|
||||
SemanticsAnnotator annotator,
|
||||
Iterable<_SemanticsFragment> children
|
||||
}) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children);
|
||||
Iterable<_SemanticsFragment> children,
|
||||
bool dropSemanticsOfPreviousSiblings,
|
||||
}) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings);
|
||||
|
||||
@override
|
||||
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) {
|
||||
@ -833,8 +855,9 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment {
|
||||
_ImplicitSemanticsFragment({
|
||||
RenderObject renderObjectOwner,
|
||||
SemanticsAnnotator annotator,
|
||||
Iterable<_SemanticsFragment> children
|
||||
}) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children);
|
||||
Iterable<_SemanticsFragment> children,
|
||||
bool dropSemanticsOfPreviousSiblings,
|
||||
}) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings);
|
||||
|
||||
@override
|
||||
bool get haveConcreteNode => _haveConcreteNode;
|
||||
@ -878,8 +901,9 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment {
|
||||
class _ForkingSemanticsFragment extends _SemanticsFragment {
|
||||
_ForkingSemanticsFragment({
|
||||
RenderObject renderObjectOwner,
|
||||
@required Iterable<_SemanticsFragment> children
|
||||
}) : super(renderObjectOwner: renderObjectOwner, children: children) {
|
||||
@required Iterable<_SemanticsFragment> children,
|
||||
bool dropSemanticsOfPreviousSiblings,
|
||||
}) : super(renderObjectOwner: renderObjectOwner, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings) {
|
||||
assert(children != null);
|
||||
assert(children.length > 1);
|
||||
}
|
||||
@ -1414,6 +1438,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
|
||||
super.adoptChild(child);
|
||||
markNeedsLayout();
|
||||
markNeedsCompositingBitsUpdate();
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
/// Called by subclasses when they decide a render object is no longer a child.
|
||||
@ -1431,6 +1456,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
|
||||
super.dropChild(child);
|
||||
markNeedsLayout();
|
||||
markNeedsCompositingBitsUpdate();
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
/// Calls visitor for each immediate child of this render object.
|
||||
@ -2408,6 +2434,22 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
|
||||
/// setting [isSemanticBoundary] to true.
|
||||
bool get isSemanticBoundary => false;
|
||||
|
||||
/// Whether this [RenderObject] makes other [RenderObject]s previously painted
|
||||
/// within the same semantic boundary unreachable for accessibility purposes.
|
||||
///
|
||||
/// If `true` is returned, the [SemanticsNode]s for all siblings and cousins
|
||||
/// of this node, that are earlier in a depth-first pre-order traversal, are
|
||||
/// dropped from the semantics tree up until a semantic boundary (as defined
|
||||
/// by [isSemanticBoundary]) is reached.
|
||||
///
|
||||
/// If [isSemanticBoundary] and [isBlockingSemanticsOfPreviouslyPaintedNodes]
|
||||
/// is set on the same node, all previously painted siblings and cousins
|
||||
/// up until the next ancestor that is a semantic boundary are dropped.
|
||||
///
|
||||
/// Paint order as established by [visitChildrenForSemantics] is used to
|
||||
/// determine if a node is previous to this one.
|
||||
bool get isBlockingSemanticsOfPreviouslyPaintedNodes => false;
|
||||
|
||||
/// The bounding box, in the local coordinate system, of this
|
||||
/// object, for accessibility purposes.
|
||||
Rect get semanticBounds;
|
||||
@ -2545,9 +2587,10 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
|
||||
// early-exit if we're not dirty and have our own semantics
|
||||
if (!_needsSemanticsUpdate && isSemanticBoundary) {
|
||||
assert(_semantics != null);
|
||||
return new _CleanSemanticsFragment(renderObjectOwner: this);
|
||||
return new _CleanSemanticsFragment(renderObjectOwner: this, dropSemanticsOfPreviousSiblings: isBlockingSemanticsOfPreviouslyPaintedNodes);
|
||||
}
|
||||
List<_SemanticsFragment> children;
|
||||
bool dropSemanticsOfPreviousSiblings = isBlockingSemanticsOfPreviouslyPaintedNodes;
|
||||
visitChildrenForSemantics((RenderObject child) {
|
||||
if (_needsSemanticsGeometryUpdate) {
|
||||
// If our geometry changed, make sure the child also does a
|
||||
@ -2557,31 +2600,40 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
|
||||
child._needsSemanticsGeometryUpdate = true;
|
||||
}
|
||||
final _SemanticsFragment fragment = child._getSemanticsFragment();
|
||||
if (fragment != null) {
|
||||
assert(fragment != null);
|
||||
if (fragment.dropSemanticsOfPreviousSiblings) {
|
||||
children = null; // throw away all left siblings of [child].
|
||||
dropSemanticsOfPreviousSiblings = true;
|
||||
}
|
||||
if (fragment.producesSemanticNodes) {
|
||||
fragment.addAncestor(this);
|
||||
children ??= <_SemanticsFragment>[];
|
||||
assert(!children.contains(fragment));
|
||||
children.add(fragment);
|
||||
}
|
||||
});
|
||||
if (isSemanticBoundary && !isBlockingSemanticsOfPreviouslyPaintedNodes) {
|
||||
// Don't propagate [dropSemanticsOfPreviousSiblings] up through a semantic boundary.
|
||||
dropSemanticsOfPreviousSiblings = false;
|
||||
}
|
||||
_needsSemanticsUpdate = false;
|
||||
_needsSemanticsGeometryUpdate = false;
|
||||
final SemanticsAnnotator annotator = semanticsAnnotator;
|
||||
if (parent is! RenderObject)
|
||||
return new _RootSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children);
|
||||
return new _RootSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings);
|
||||
if (isSemanticBoundary)
|
||||
return new _ConcreteSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children);
|
||||
return new _ConcreteSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings);
|
||||
if (annotator != null)
|
||||
return new _ImplicitSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children);
|
||||
return new _ImplicitSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings);
|
||||
_semantics = null;
|
||||
if (children == null) {
|
||||
// Introduces no semantics and has no descendants that introduce semantics.
|
||||
return null;
|
||||
return new _EmptySemanticsFragment(renderObjectOwner: this, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings);
|
||||
}
|
||||
if (children.length > 1)
|
||||
return new _ForkingSemanticsFragment(renderObjectOwner: this, children: children);
|
||||
return new _ForkingSemanticsFragment(renderObjectOwner: this, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings);
|
||||
assert(children.length == 1);
|
||||
return children.single;
|
||||
return children.single..dropSemanticsOfPreviousSiblings = dropSemanticsOfPreviousSiblings;
|
||||
}
|
||||
|
||||
/// Called when collecting the semantics of this node.
|
||||
@ -2727,6 +2779,10 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
|
||||
description.add('layer: $_layer');
|
||||
if (_semantics != null)
|
||||
description.add('semantics: $_semantics');
|
||||
if (isBlockingSemanticsOfPreviouslyPaintedNodes)
|
||||
description.add('blocks semantics of earlier render objects below the common boundary');
|
||||
if (isSemanticBoundary)
|
||||
description.add('semantic boundary');
|
||||
}
|
||||
|
||||
/// Returns a string describing the current node's descendants. Each line of
|
||||
|
@ -2891,6 +2891,18 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
|
||||
}
|
||||
}
|
||||
|
||||
/// Causes the semantics of all siblings and cousins painted before it in the
|
||||
/// same semantic container to be dropped.
|
||||
///
|
||||
/// This is useful in a stack where an overlay should prevent interactions
|
||||
/// with the underlying layers.
|
||||
class RenderBlockSemantics extends RenderProxyBox {
|
||||
RenderBlockSemantics({ RenderBox child }) : super(child);
|
||||
|
||||
@override
|
||||
bool get isBlockingSemanticsOfPreviouslyPaintedNodes => true;
|
||||
}
|
||||
|
||||
/// Causes the semantics of all descendants to be merged into this
|
||||
/// node such that the entire subtree becomes a single leaf in the
|
||||
/// semantics tree.
|
||||
|
@ -3613,6 +3613,27 @@ class MergeSemantics extends SingleChildRenderObjectWidget {
|
||||
RenderMergeSemantics createRenderObject(BuildContext context) => new RenderMergeSemantics();
|
||||
}
|
||||
|
||||
/// A widget that drops the semantics of all widget that were painted before it
|
||||
/// in the same semantic container.
|
||||
///
|
||||
/// This is useful to hide widgets from accessibility tools that are painted
|
||||
/// behind a certain widget, e.g. an alert should usually disallow interaction
|
||||
/// with any widget located "behind" the alert (even when they are still
|
||||
/// partially visible). Similarly, an open [Drawer] blocks interactions with
|
||||
/// any widget outside the drawer.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ExcludeSemantics] which drops all semantics of its descendants.
|
||||
class BlockSemantics extends SingleChildRenderObjectWidget {
|
||||
/// Creates a widget that excludes the semantics of all widgets painted before
|
||||
/// it in the same semantic container.
|
||||
const BlockSemantics({ Key key, Widget child }) : super(key: key, child: child);
|
||||
|
||||
@override
|
||||
RenderBlockSemantics createRenderObject(BuildContext context) => new RenderBlockSemantics();
|
||||
}
|
||||
|
||||
/// A widget that drops all the semantics of its descendants.
|
||||
///
|
||||
/// When [excluding] is true, this widget (and its subtree) is excluded from
|
||||
@ -3622,6 +3643,10 @@ class MergeSemantics extends SingleChildRenderObjectWidget {
|
||||
/// reported but that would only be confusing. For example, the
|
||||
/// material library's [Chip] widget hides the avatar since it is
|
||||
/// redundant with the chip label.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [BlockSemantics] which drops semantics of widgets earlier in the tree.
|
||||
class ExcludeSemantics extends SingleChildRenderObjectWidget {
|
||||
/// Creates a widget that drops all the semantics of its descendants.
|
||||
const ExcludeSemantics({
|
||||
|
@ -26,21 +26,23 @@ class ModalBarrier extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new ExcludeSemantics(
|
||||
excluding: !dismissible,
|
||||
child: new Semantics(
|
||||
container: true,
|
||||
child: new GestureDetector(
|
||||
onTapDown: (TapDownDetails details) {
|
||||
if (dismissible)
|
||||
Navigator.pop(context);
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: new ConstrainedBox(
|
||||
constraints: const BoxConstraints.expand(),
|
||||
child: color == null ? null : new DecoratedBox(
|
||||
decoration: new BoxDecoration(
|
||||
color: color
|
||||
return new BlockSemantics(
|
||||
child: new ExcludeSemantics(
|
||||
excluding: !dismissible,
|
||||
child: new Semantics(
|
||||
container: true,
|
||||
child: new GestureDetector(
|
||||
onTapDown: (TapDownDetails details) {
|
||||
if (dismissible)
|
||||
Navigator.pop(context);
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: new ConstrainedBox(
|
||||
constraints: const BoxConstraints.expand(),
|
||||
child: color == null ? null : new DecoratedBox(
|
||||
decoration: new BoxDecoration(
|
||||
color: color
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -4,6 +4,9 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:matcher/matcher.dart';
|
||||
|
||||
import '../widgets/semantics_tester.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Dialog is scrollable', (WidgetTester tester) async {
|
||||
@ -192,4 +195,38 @@ void main() {
|
||||
expect(find.text('Dialog2'), findsOneWidget);
|
||||
|
||||
});
|
||||
|
||||
testWidgets('Dialog hides underlying semantics tree', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||
const String buttonText = 'A button covered by dialog overlay';
|
||||
await tester.pumpWidget(
|
||||
new MaterialApp(
|
||||
home: const Material(
|
||||
child: const Center(
|
||||
child: const RaisedButton(
|
||||
onPressed: null,
|
||||
child: const Text(buttonText),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(semantics, includesNodeWithLabel(buttonText));
|
||||
|
||||
final BuildContext context = tester.element(find.text(buttonText));
|
||||
|
||||
const String alertText = 'A button in an overlay alert';
|
||||
showDialog<Null>(
|
||||
context: context,
|
||||
child: const AlertDialog(title: const Text(alertText)),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
|
||||
expect(semantics, includesNodeWithLabel(alertText));
|
||||
expect(semantics, isNot(includesNodeWithLabel(buttonText)));
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import '../widgets/semantics_tester.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Scaffold control test', (WidgetTester tester) async {
|
||||
final Key bodyKey = new UniqueKey();
|
||||
@ -440,4 +442,40 @@ void main() {
|
||||
expect(tester.renderObject<RenderBox>(find.byKey(testKey)).localToGlobal(Offset.zero), const Offset(0.0, 0.0));
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('Open drawer hides underlying semantics tree', (WidgetTester tester) async {
|
||||
const String bodyLabel = 'I am the body';
|
||||
const String persistentFooterButtonLabel = 'a button on the bottom';
|
||||
const String bottomNavigationBarLabel = 'a bar in an app';
|
||||
const String floatingActionButtonLabel = 'I float in space';
|
||||
const String drawerLabel = 'I am the reason for this test';
|
||||
|
||||
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||
await tester.pumpWidget(new MaterialApp(home: new Scaffold(
|
||||
body: new Semantics(label: bodyLabel, child: new Container()),
|
||||
persistentFooterButtons: <Widget>[new Semantics(label: persistentFooterButtonLabel, child: new Container())],
|
||||
bottomNavigationBar: new Semantics(label: bottomNavigationBarLabel, child: new Container()),
|
||||
floatingActionButton: new Semantics(label: floatingActionButtonLabel, child: new Container()),
|
||||
drawer: new Drawer(child:new Semantics(label: drawerLabel, child: new Container())),
|
||||
)));
|
||||
|
||||
expect(semantics, includesNodeWithLabel(bodyLabel));
|
||||
expect(semantics, includesNodeWithLabel(persistentFooterButtonLabel));
|
||||
expect(semantics, includesNodeWithLabel(bottomNavigationBarLabel));
|
||||
expect(semantics, includesNodeWithLabel(floatingActionButtonLabel));
|
||||
expect(semantics, isNot(includesNodeWithLabel(drawerLabel)));
|
||||
|
||||
final ScaffoldState state = tester.firstState(find.byType(Scaffold));
|
||||
state.openDrawer();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
expect(semantics, isNot(includesNodeWithLabel(bodyLabel)));
|
||||
expect(semantics, isNot(includesNodeWithLabel(persistentFooterButtonLabel)));
|
||||
expect(semantics, isNot(includesNodeWithLabel(bottomNavigationBarLabel)));
|
||||
expect(semantics, isNot(includesNodeWithLabel(floatingActionButtonLabel)));
|
||||
expect(semantics, includesNodeWithLabel(drawerLabel));
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
}
|
||||
|
149
packages/flutter/test/widgets/semantics_9_test.dart
Normal file
149
packages/flutter/test/widgets/semantics_9_test.dart
Normal file
@ -0,0 +1,149 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'semantics_tester.dart';
|
||||
|
||||
void main() {
|
||||
group('BlockSemantics', () {
|
||||
testWidgets('hides semantic nodes of siblings', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||
|
||||
await tester.pumpWidget(new Stack(
|
||||
children: <Widget>[
|
||||
new Semantics(
|
||||
label: 'layer#1',
|
||||
child: new Container(),
|
||||
),
|
||||
const BlockSemantics(),
|
||||
new Semantics(
|
||||
label: 'layer#2',
|
||||
child: new Container(),
|
||||
),
|
||||
],
|
||||
));
|
||||
|
||||
expect(semantics, isNot(includesNodeWithLabel('layer#1')));
|
||||
|
||||
await tester.pumpWidget(new Stack(
|
||||
children: <Widget>[
|
||||
new Semantics(
|
||||
label: 'layer#1',
|
||||
child: new Container(),
|
||||
),
|
||||
],
|
||||
));
|
||||
|
||||
expect(semantics, includesNodeWithLabel('layer#1'));
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('does not hides semantic nodes of siblings outside the current semantic boundary', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||
|
||||
await tester.pumpWidget(new Stack(
|
||||
children: <Widget>[
|
||||
new Semantics(
|
||||
label: '#1',
|
||||
child: new Container(),
|
||||
),
|
||||
new Semantics(
|
||||
label: '#2',
|
||||
container: true,
|
||||
child: new Stack(
|
||||
children: <Widget>[
|
||||
new Semantics(
|
||||
label: 'NOT#2.1',
|
||||
child: new Container(),
|
||||
),
|
||||
new Semantics(
|
||||
label: '#2.2',
|
||||
child: new BlockSemantics(
|
||||
child: new Semantics(
|
||||
container: true,
|
||||
label: '#2.2.1',
|
||||
child: new Container(),
|
||||
),
|
||||
),
|
||||
),
|
||||
new Semantics(
|
||||
label: '#2.3',
|
||||
child: new Container(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
new Semantics(
|
||||
label: '#3',
|
||||
child: new Container(),
|
||||
),
|
||||
],
|
||||
));
|
||||
|
||||
expect(semantics, includesNodeWithLabel('#1'));
|
||||
expect(semantics, includesNodeWithLabel('#2'));
|
||||
expect(semantics, isNot(includesNodeWithLabel('NOT#2.1')));
|
||||
expect(semantics, includesNodeWithLabel('#2.2'));
|
||||
expect(semantics, includesNodeWithLabel('#2.2.1'));
|
||||
expect(semantics, includesNodeWithLabel('#2.3'));
|
||||
expect(semantics, includesNodeWithLabel('#3'));
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('node is semantic boundary and blocking previously painted nodes', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||
final GlobalKey stackKey = new GlobalKey();
|
||||
|
||||
await tester.pumpWidget(new Stack(
|
||||
key: stackKey,
|
||||
children: <Widget>[
|
||||
new Semantics(
|
||||
label: 'NOT#1',
|
||||
child: new Container(),
|
||||
),
|
||||
new BoundaryBlockSemantics(
|
||||
child: new Semantics(
|
||||
label: '#2.1',
|
||||
child: new Container(),
|
||||
)
|
||||
),
|
||||
new Semantics(
|
||||
label: '#3',
|
||||
child: new Container(),
|
||||
),
|
||||
],
|
||||
));
|
||||
|
||||
expect(semantics, isNot(includesNodeWithLabel('NOT#1')));
|
||||
expect(semantics, includesNodeWithLabel('#2.1'));
|
||||
expect(semantics, includesNodeWithLabel('#3'));
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class BoundaryBlockSemantics extends SingleChildRenderObjectWidget {
|
||||
const BoundaryBlockSemantics({ Key key, Widget child }) : super(key: key, child: child);
|
||||
|
||||
@override
|
||||
RenderBoundaryBlockSemantics createRenderObject(BuildContext context) => new RenderBoundaryBlockSemantics();
|
||||
}
|
||||
|
||||
class RenderBoundaryBlockSemantics extends RenderProxyBox {
|
||||
RenderBoundaryBlockSemantics({ RenderBox child }) : super(child);
|
||||
|
||||
@override
|
||||
bool get isBlockingSemanticsOfPreviouslyPaintedNodes => true;
|
||||
|
||||
@override
|
||||
bool get isSemanticBoundary => true;
|
||||
}
|
||||
|
@ -194,6 +194,8 @@ class SemanticsTester {
|
||||
String toString() => 'SemanticsTester';
|
||||
}
|
||||
|
||||
const String _matcherHelp = 'Try dumping the semantics with debugDumpSemanticsTree() from the rendering library to see what the semantics tree looks like.';
|
||||
|
||||
class _HasSemantics extends Matcher {
|
||||
const _HasSemantics(this._semantics) : assert(_semantics != null);
|
||||
|
||||
@ -211,30 +213,65 @@ class _HasSemantics extends Matcher {
|
||||
|
||||
@override
|
||||
Description describeMismatch(dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
|
||||
const String help = 'Try dumping the semantics with debugDumpSemanticsTree() from the rendering library to see what the semantics tree looks like.';
|
||||
final TestSemantics testNode = matchState[TestSemantics];
|
||||
final SemanticsNode node = matchState[SemanticsNode];
|
||||
if (node == null)
|
||||
return mismatchDescription.add('could not find node with id ${testNode.id}.\n$help');
|
||||
return mismatchDescription.add('could not find node with id ${testNode.id}.\n$_matcherHelp');
|
||||
if (testNode.id != node.id)
|
||||
return mismatchDescription.add('expected node id ${testNode.id} but found id ${node.id}.\n$help');
|
||||
return mismatchDescription.add('expected node id ${testNode.id} but found id ${node.id}.\n$_matcherHelp');
|
||||
final SemanticsData data = node.getSemanticsData();
|
||||
if (testNode.flags != data.flags)
|
||||
return mismatchDescription.add('expected node id ${testNode.id} to have flags ${testNode.flags} but found flags ${data.flags}.\n$help');
|
||||
return mismatchDescription.add('expected node id ${testNode.id} to have flags ${testNode.flags} but found flags ${data.flags}.\n$_matcherHelp');
|
||||
if (testNode.actions != data.actions)
|
||||
return mismatchDescription.add('expected node id ${testNode.id} to have actions ${testNode.actions} but found actions ${data.actions}.\n$help');
|
||||
return mismatchDescription.add('expected node id ${testNode.id} to have actions ${testNode.actions} but found actions ${data.actions}.\n$_matcherHelp');
|
||||
if (testNode.label != data.label)
|
||||
return mismatchDescription.add('expected node id ${testNode.id} to have label "${testNode.label}" but found label "${data.label}".\n$help');
|
||||
return mismatchDescription.add('expected node id ${testNode.id} to have label "${testNode.label}" but found label "${data.label}".\n$_matcherHelp');
|
||||
if (testNode.rect != data.rect)
|
||||
return mismatchDescription.add('expected node id ${testNode.id} to have rect ${testNode.rect} but found rect ${data.rect}.\n$help');
|
||||
return mismatchDescription.add('expected node id ${testNode.id} to have rect ${testNode.rect} but found rect ${data.rect}.\n$_matcherHelp');
|
||||
if (testNode.transform != data.transform)
|
||||
return mismatchDescription.add('expected node id ${testNode.id} to have transform ${testNode.transform} but found transform:.\n${data.transform}.\n$help');
|
||||
return mismatchDescription.add('expected node id ${testNode.id} to have transform ${testNode.transform} but found transform:.\n${data.transform}.\n$_matcherHelp');
|
||||
final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount;
|
||||
if (testNode.children.length != childrenCount)
|
||||
return mismatchDescription.add('expected node id ${testNode.id} to have ${testNode.children.length} children but found $childrenCount.\n$help');
|
||||
return mismatchDescription.add('expected node id ${testNode.id} to have ${testNode.children.length} children but found $childrenCount.\n$_matcherHelp');
|
||||
return mismatchDescription;
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts that a [SemanticsTester] has a semantics tree that exactly matches the given semantics.
|
||||
Matcher hasSemantics(TestSemantics semantics) => new _HasSemantics(semantics);
|
||||
|
||||
class _IncludesNodeWithLabel extends Matcher {
|
||||
const _IncludesNodeWithLabel(this._label) : assert(_label != null);
|
||||
|
||||
final String _label;
|
||||
|
||||
@override
|
||||
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
|
||||
bool result = false;
|
||||
SemanticsNodeVisitor visitor;
|
||||
visitor = (SemanticsNode node) {
|
||||
if (node.label == _label) {
|
||||
result = true;
|
||||
} else {
|
||||
node.visitChildren(visitor);
|
||||
}
|
||||
return !result;
|
||||
};
|
||||
final SemanticsNode root = item.tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
|
||||
visitor(root);
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Description describe(Description description) {
|
||||
return description.add('includes node with label "$_label"');
|
||||
}
|
||||
|
||||
@override
|
||||
Description describeMismatch(dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
|
||||
return mismatchDescription.add('could not find node with label "$_label".\n$_matcherHelp');
|
||||
}
|
||||
}
|
||||
|
||||
/// Asserts that a node in the semantics tree of [SemanticsTester] has [label].
|
||||
Matcher includesNodeWithLabel(String label) => new _IncludesNodeWithLabel(label);
|
||||
|
Loading…
x
Reference in New Issue
Block a user