From 87c5b24e87c626f8dfd14132414ca5620a9aa3e8 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Tue, 30 May 2017 16:45:54 -0700 Subject: [PATCH] Exclude ModalBarrier from Semantics Tree if it is not dismissible (#10395) --- .../flutter/lib/src/rendering/proxy_box.dart | 33 ++++++++++++++++- packages/flutter/lib/src/widgets/basic.dart | 26 ++++++++++++- .../lib/src/widgets/modal_barrier.dart | 29 ++++++++------- .../test/widgets/modal_barrier_test.dart | 37 +++++++++++++++++++ 4 files changed, 108 insertions(+), 17 deletions(-) diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index a6aab7d661..47a3a1d3d6 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -2901,12 +2901,41 @@ class RenderMergeSemantics extends RenderProxyBox { /// Excludes this subtree from the semantic tree. /// +/// When [excluding] is true, this render object (and its subtree) is excluded +/// from the semantic tree. +/// /// Useful e.g. for hiding text that is redundant with other text next /// to it (e.g. text included only for the visual effect). class RenderExcludeSemantics extends RenderProxyBox { /// Creates a render object that ignores the semantics of its subtree. - RenderExcludeSemantics({ RenderBox child }) : super(child); + RenderExcludeSemantics({ + RenderBox child, + bool excluding: true, + }) : _excluding = excluding, super(child) { + assert(_excluding != null); + } + + /// Whether this render object is excluded from the semantic tree. + bool get excluding => _excluding; + bool _excluding; + set excluding(bool value) { + assert(value != null); + if (value == _excluding) + return; + _excluding = value; + markNeedsSemanticsUpdate(); + } @override - void visitChildrenForSemantics(RenderObjectVisitor visitor) { } + void visitChildrenForSemantics(RenderObjectVisitor visitor) { + if (excluding) + return; + super.visitChildrenForSemantics(visitor); + } + + @override + void debugFillDescription(List description) { + super.debugFillDescription(description); + description.add('excluding: $excluding'); + } } diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 246022a058..9776c2e545 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -3424,16 +3424,38 @@ class MergeSemantics extends SingleChildRenderObjectWidget { /// A widget that drops all the semantics of its descendants. /// +/// When [excluding] is true, this widget (and its subtree) is excluded from +/// the semantics tree. +/// /// This can be used to hide subwidgets that would otherwise be /// 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. class ExcludeSemantics extends SingleChildRenderObjectWidget { /// Creates a widget that drops all the semantics of its descendants. - const ExcludeSemantics({ Key key, Widget child }) : super(key: key, child: child); + const ExcludeSemantics({ + Key key, + this.excluding: true, + Widget child, + }) : assert(excluding != null), + super(key: key, child: child); + + /// Whether this widget is excluded in the semantics tree. + final bool excluding; @override - RenderExcludeSemantics createRenderObject(BuildContext context) => new RenderExcludeSemantics(); + RenderExcludeSemantics createRenderObject(BuildContext context) => new RenderExcludeSemantics(excluding: excluding); + + @override + void updateRenderObject(BuildContext context, RenderExcludeSemantics renderObject) { + renderObject.excluding = excluding; + } + + @override + void debugFillDescription(List description) { + super.debugFillDescription(description); + description.add('excluding: $excluding'); + } } /// A widget that builds its child. diff --git a/packages/flutter/lib/src/widgets/modal_barrier.dart b/packages/flutter/lib/src/widgets/modal_barrier.dart index 719fa189ee..0dbb979c40 100644 --- a/packages/flutter/lib/src/widgets/modal_barrier.dart +++ b/packages/flutter/lib/src/widgets/modal_barrier.dart @@ -26,19 +26,22 @@ class ModalBarrier extends StatelessWidget { @override Widget build(BuildContext context) { - return 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 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 + ) ) ) ) diff --git a/packages/flutter/test/widgets/modal_barrier_test.dart b/packages/flutter/test/widgets/modal_barrier_test.dart index f1117cb090..7a13eacf72 100644 --- a/packages/flutter/test/widgets/modal_barrier_test.dart +++ b/packages/flutter/test/widgets/modal_barrier_test.dart @@ -4,8 +4,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'semantics_tester.dart'; + void main() { bool tapped; Widget tapTarget; @@ -78,6 +81,40 @@ void main() { expect(find.byKey(const ValueKey('barrier')), findsNothing, reason: 'The route should have been dismissed by tapping the barrier.'); }); + + testWidgets('Undismissible ModalBarrier hidden in semantic tree', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + await tester.pumpWidget(const ModalBarrier(dismissible: false)); + + final TestSemantics expectedSemantics = new TestSemantics.root(); + expect(semantics, hasSemantics(expectedSemantics)); + + semantics.dispose(); + }); + + testWidgets('Dismissible ModalBarrier includes button in semantic tree', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + await tester.pumpWidget(const ModalBarrier(dismissible: true)); + + final TestSemantics expectedSemantics = new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + id: 1, + rect: TestSemantics.fullScreen, + children: [ + new TestSemantics( + id: 2, + rect: TestSemantics.fullScreen, + actions: SemanticsAction.tap.index, + ), + ] + ), + ] + ); + expect(semantics, hasSemantics(expectedSemantics)); + + semantics.dispose(); + }); } class FirstWidget extends StatelessWidget {