Commit 104725f3 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Adds a widgets that blocks all semantics of widgets below it in paint order...

Adds a widgets that blocks all semantics of widgets below it in paint order within the same container (#10425)
parent 417df36b
......@@ -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(
......
......@@ -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();
});
}
// 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);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment