From 034a663d33f23a549895ecc750997ddda42282f3 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Thu, 19 Apr 2018 17:59:19 -0700 Subject: [PATCH] Semantics object support for edge triggered semantics (#16081) Semantics object support for edge triggered semantics --- .../flutter_gallery/lib/demo/pesto_demo.dart | 49 ++++---- packages/flutter/lib/src/cupertino/route.dart | 6 +- .../flutter/lib/src/material/app_bar.dart | 9 ++ packages/flutter/lib/src/material/dialog.dart | 116 ++++++++++++++---- packages/flutter/lib/src/material/drawer.dart | 39 +++++- .../lib/src/material/flexible_space_bar.dart | 18 ++- packages/flutter/lib/src/material/page.dart | 8 +- .../flutter/lib/src/material/popup_menu.dart | 37 +++++- .../lib/src/rendering/custom_paint.dart | 6 + .../flutter/lib/src/rendering/proxy_box.dart | 30 +++++ .../flutter/lib/src/semantics/semantics.dart | 40 ++++++ packages/flutter/lib/src/widgets/basic.dart | 12 ++ .../test/material/date_picker_test.dart | 5 +- .../flutter/test/material/dialog_test.dart | 49 ++++++++ .../material/floating_action_button_test.dart | 21 ++-- .../test/material/popup_menu_test.dart | 37 +++++- .../flutter/test/material/tooltip_test.dart | 33 +++-- .../user_accounts_drawer_header_test.dart | 66 +++++----- .../test/widgets/custom_painter_test.dart | 8 +- .../flutter/test/widgets/drawer_test.dart | 38 ++++++ .../test/widgets/editable_text_test.dart | 53 ++++---- .../flutter/test/widgets/navigator_test.dart | 50 ++++++++ .../flutter/test/widgets/semantics_test.dart | 10 +- ...xpressionForCurrentSemanticsTree_test.dart | 44 ++++--- .../test/widgets/simple_semantics_test.dart | 19 ++- .../test/widgets/sliver_semantics_test.dart | 9 ++ 26 files changed, 650 insertions(+), 162 deletions(-) diff --git a/examples/flutter_gallery/lib/demo/pesto_demo.dart b/examples/flutter_gallery/lib/demo/pesto_demo.dart index 8d39eb37622..ff7ef6e63b3 100644 --- a/examples/flutter_gallery/lib/demo/pesto_demo.dart +++ b/examples/flutter_gallery/lib/demo/pesto_demo.dart @@ -96,7 +96,7 @@ class _RecipeGridPageState extends State { _buildBody(context, statusBarHeight), ], ), - ) + ), ); } @@ -215,30 +215,33 @@ class _PestoLogoState extends State { @override Widget build(BuildContext context) { - return new Transform( - transform: new Matrix4.identity()..scale(widget.height / kLogoHeight), - alignment: Alignment.topCenter, - child: new SizedBox( - width: kLogoWidth, - child: new Stack( - overflow: Overflow.visible, - children: [ - new Positioned.fromRect( - rect: _imageRectTween.lerp(widget.t), - child: new Image.asset( - _kSmallLogoImage, - package: _kGalleryAssetsPackage, - fit: BoxFit.contain, + return new Semantics( + namesRoute: true, + child: new Transform( + transform: new Matrix4.identity()..scale(widget.height / kLogoHeight), + alignment: Alignment.topCenter, + child: new SizedBox( + width: kLogoWidth, + child: new Stack( + overflow: Overflow.visible, + children: [ + new Positioned.fromRect( + rect: _imageRectTween.lerp(widget.t), + child: new Image.asset( + _kSmallLogoImage, + package: _kGalleryAssetsPackage, + fit: BoxFit.contain, + ), ), - ), - new Positioned.fromRect( - rect: _textRectTween.lerp(widget.t), - child: new Opacity( - opacity: _textOpacity.transform(widget.t), - child: new Text('PESTO', style: titleStyle, textAlign: TextAlign.center), + new Positioned.fromRect( + rect: _textRectTween.lerp(widget.t), + child: new Opacity( + opacity: _textOpacity.transform(widget.t), + child: new Text('PESTO', style: titleStyle, textAlign: TextAlign.center), + ), ), - ), - ], + ], + ), ), ), ); diff --git a/packages/flutter/lib/src/cupertino/route.dart b/packages/flutter/lib/src/cupertino/route.dart index 9c2671b7552..bed4a461f5e 100644 --- a/packages/flutter/lib/src/cupertino/route.dart +++ b/packages/flutter/lib/src/cupertino/route.dart @@ -237,7 +237,11 @@ class CupertinoPageRoute extends PageRoute { @override Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { - final Widget result = builder(context); + final Widget result = new Semantics( + scopesRoute: true, + explicitChildNodes: true, + child: builder(context), + ); assert(() { if (result == null) { throw new FlutterError( diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index 2d33bd207cc..d165a6a2760 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -383,6 +383,15 @@ class _AppBarState extends State { Widget title = widget.title; if (title != null) { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + title = new Semantics(namesRoute: true, child: title); + break; + case TargetPlatform.iOS: + break; + } + title = new DefaultTextStyle( style: centerStyle, softWrap: false, diff --git a/packages/flutter/lib/src/material/dialog.dart b/packages/flutter/lib/src/material/dialog.dart index 52a3a7908de..0b10724a0ce 100644 --- a/packages/flutter/lib/src/material/dialog.dart +++ b/packages/flutter/lib/src/material/dialog.dart @@ -166,6 +166,7 @@ class AlertDialog extends StatelessWidget { this.content, this.contentPadding: const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0), this.actions, + this.semanticLabel, }) : assert(contentPadding != null), super(key: key); @@ -216,18 +217,41 @@ class AlertDialog extends StatelessWidget { /// from the [actions]. final List actions; + /// The semantic label of the dialog used by accessibility frameworks to + /// announce screen transitions when the dialog is opened and closed. + /// + /// If this label is not provided, a semantic label will be infered from the + /// [title] if it is not null. If there is no title, the label will be taken + /// from [MaterialLocalizations.alertDialogLabel]. + /// + /// See also: + /// + /// * [SemanticsConfiguration.isRouteName], for a description of how this + /// value is used. + final String semanticLabel; + @override Widget build(BuildContext context) { final List children = []; + String label = semanticLabel; if (title != null) { children.add(new Padding( padding: titlePadding ?? new EdgeInsets.fromLTRB(24.0, 24.0, 24.0, content == null ? 20.0 : 0.0), child: new DefaultTextStyle( style: Theme.of(context).textTheme.title, - child: title, + child: new Semantics(child: title, namesRoute: true), ), )); + } else { + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + label = semanticLabel; + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + label = semanticLabel ?? MaterialLocalizations.of(context)?.alertDialogLabel; + } } if (content != null) { @@ -250,15 +274,22 @@ class AlertDialog extends StatelessWidget { )); } - return new Dialog( - child: new IntrinsicWidth( - child: new Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: children, - ), + Widget dialogChild = new IntrinsicWidth( + child: new Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, ), ); + + if (label != null) + dialogChild = new Semantics( + namesRoute: true, + label: label, + child: dialogChild + ); + + return new Dialog(child: dialogChild); } } @@ -402,6 +433,7 @@ class SimpleDialog extends StatelessWidget { this.titlePadding: const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0), this.children, this.contentPadding: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0), + this.semanticLabel, }) : assert(titlePadding != null), assert(contentPadding != null), super(key: key); @@ -443,18 +475,41 @@ class SimpleDialog extends StatelessWidget { /// the top padding ends up being 24 pixels. final EdgeInsetsGeometry contentPadding; + /// The semantic label of the dialog used by accessibility frameworks to + /// announce screen transitions when the dialog is opened and closed. + /// + /// If this label is not provided, a semantic label will be infered from the + /// [title] if it is not null. If there is no title, the label will be taken + /// from [MaterialLocalizations.dialogLabel]. + /// + /// See also: + /// + /// * [SemanticsConfiguration.isRouteName], for a description of how this + /// value is used. + final String semanticLabel; + @override Widget build(BuildContext context) { final List body = []; + String label = semanticLabel; if (title != null) { body.add(new Padding( padding: titlePadding, child: new DefaultTextStyle( style: Theme.of(context).textTheme.title, - child: title, + child: new Semantics(namesRoute: true, child: title), ) )); + } else { + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + label = semanticLabel; + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + label = semanticLabel ?? MaterialLocalizations.of(context)?.dialogLabel; + } } if (children != null) { @@ -466,19 +521,25 @@ class SimpleDialog extends StatelessWidget { )); } - return new Dialog( - child: new IntrinsicWidth( - stepWidth: 56.0, - child: new ConstrainedBox( - constraints: const BoxConstraints(minWidth: 280.0), - child: new Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: body, - ) - ) - ) + Widget dialogChild = new IntrinsicWidth( + stepWidth: 56.0, + child: new ConstrainedBox( + constraints: const BoxConstraints(minWidth: 280.0), + child: new Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: body, + ), + ), ); + + if (label != null) + dialogChild = new Semantics( + namesRoute: true, + label: label, + child: dialogChild, + ); + return new Dialog(child: dialogChild); } } @@ -514,7 +575,14 @@ class _DialogRoute extends PopupRoute { return new SafeArea( child: new Builder( builder: (BuildContext context) { - return theme != null ? new Theme(data: theme, child: child) : child; + final Widget annotatedChild = new Semantics( + child: child, + scopesRoute: true, + explicitChildNodes: true, + ); + return theme != null + ? new Theme(data: theme, child: annotatedChild) + : annotatedChild; } ), ); @@ -570,9 +638,9 @@ Future showDialog({ ) Widget child, WidgetBuilder builder, }) { - assert(child == null || builder == null); // ignore: deprecated_member_use + assert(child == null || builder == null); return Navigator.of(context, rootNavigator: true).push(new _DialogRoute( - child: child ?? new Builder(builder: builder), // ignore: deprecated_member_use + child: child ?? new Builder(builder: builder), theme: Theme.of(context, shadowThemeOnly: true), barrierDismissible: barrierDismissible, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, diff --git a/packages/flutter/lib/src/material/drawer.dart b/packages/flutter/lib/src/material/drawer.dart index 125123a81bd..f496eb76b96 100644 --- a/packages/flutter/lib/src/material/drawer.dart +++ b/packages/flutter/lib/src/material/drawer.dart @@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart'; import 'colors.dart'; import 'list_tile.dart'; import 'material.dart'; +import 'material_localizations.dart'; /// The possible alignments of a [Drawer]. enum DrawerAlignment { @@ -85,6 +86,7 @@ class Drawer extends StatelessWidget { Key key, this.elevation: 16.0, this.child, + this.semanticLabel, }) : super(key: key); /// The z-coordinate at which to place this drawer. This controls the size of @@ -100,13 +102,40 @@ class Drawer extends StatelessWidget { /// {@macro flutter.widgets.child} final Widget child; + /// The semantic label of the dialog used by accessibility frameworks to + /// announce screen transitions when the drawer is opened and closed. + /// + /// If this label is not provided, it will default to + /// [MaterialLocalizations.drawerLabel]. + /// + /// See also: + /// + /// * [SemanticsConfiguration.namesRoute], for a description of how this + /// value is used. + final String semanticLabel; + @override Widget build(BuildContext context) { - return new ConstrainedBox( - constraints: const BoxConstraints.expand(width: _kWidth), - child: new Material( - elevation: elevation, - child: child, + String label = semanticLabel; + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + label = semanticLabel; + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + label = semanticLabel ?? MaterialLocalizations.of(context)?.drawerLabel; + } + return new Semantics( + scopesRoute: true, + namesRoute: true, + explicitChildNodes: true, + label: label, + child: new ConstrainedBox( + constraints: const BoxConstraints.expand(width: _kWidth), + child: new Material( + elevation: elevation, + child: child, + ), ), ); } diff --git a/packages/flutter/lib/src/material/flexible_space_bar.dart b/packages/flutter/lib/src/material/flexible_space_bar.dart index 96e2ca303df..50440df0d59 100644 --- a/packages/flutter/lib/src/material/flexible_space_bar.dart +++ b/packages/flutter/lib/src/material/flexible_space_bar.dart @@ -141,6 +141,19 @@ class _FlexibleSpaceBarState extends State { } if (widget.title != null) { + Widget title; + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + title = widget.title; + break; + case TargetPlatform.fuchsia: + case TargetPlatform.android: + title = new Semantics( + namesRoute: true, + child: widget.title, + ); + } + final ThemeData theme = Theme.of(context); final double opacity = settings.toolbarOpacity; if (opacity > 0.0) { @@ -163,7 +176,10 @@ class _FlexibleSpaceBarState extends State { transform: scaleTransform, child: new Align( alignment: titleAlignment, - child: new DefaultTextStyle(style: titleStyle, child: widget.title) + child: new DefaultTextStyle( + style: titleStyle, + child: title, + ) ) ) )); diff --git a/packages/flutter/lib/src/material/page.dart b/packages/flutter/lib/src/material/page.dart index 5bd8aee6e96..495c629bf3b 100644 --- a/packages/flutter/lib/src/material/page.dart +++ b/packages/flutter/lib/src/material/page.dart @@ -57,7 +57,7 @@ class _MountainViewPageTransition extends StatelessWidget { /// The transition is adaptive to the platform and on iOS, the page slides in /// from the right and exits in reverse. The page also shifts to the left in /// parallax when another page enters to cover it. (These directions are flipped -/// in environements with a right-to-left reading direction.) +/// in environments with a right-to-left reading direction.) /// /// By default, when a modal route is replaced by another, the previous route /// remains in memory. To free all the resources when this is not necessary, set @@ -151,7 +151,11 @@ class MaterialPageRoute extends PageRoute { @override Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { - final Widget result = builder(context); + final Widget result = new Semantics( + scopesRoute: true, + explicitChildNodes: true, + child: builder(context), + ); assert(() { if (result == null) { throw new FlutterError( diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index 6d6026cbe2d..f318574c1d1 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -417,10 +417,12 @@ class _CheckedPopupMenuItemState extends PopupMenuItemState extends StatelessWidget { const _PopupMenu({ Key key, - this.route + this.route, + this.semanticLabel, }) : super(key: key); final _PopupMenuRoute route; + final String semanticLabel; @override Widget build(BuildContext context) { @@ -479,7 +481,13 @@ class _PopupMenu extends StatelessWidget { alignment: AlignmentDirectional.topEnd, widthFactor: width.evaluate(route.animation), heightFactor: height.evaluate(route.animation), - child: child, + child: new Semantics( + scopesRoute: true, + namesRoute: true, + explicitChildNodes: true, + label: semanticLabel, + child: child, + ), ), ), ); @@ -577,6 +585,7 @@ class _PopupMenuRoute extends PopupRoute { this.elevation, this.theme, this.barrierLabel, + this.semanticLabel, }); final RelativeRect position; @@ -584,6 +593,7 @@ class _PopupMenuRoute extends PopupRoute { final dynamic initialValue; final double elevation; final ThemeData theme; + final String semanticLabel; @override Animation createAnimation() { @@ -620,7 +630,7 @@ class _PopupMenuRoute extends PopupRoute { } } - Widget menu = new _PopupMenu(route: this); + Widget menu = new _PopupMenu(route: this, semanticLabel: semanticLabel); if (theme != null) menu = new Theme(data: theme, child: menu); @@ -680,7 +690,12 @@ class _PopupMenuRoute extends PopupRoute { /// The `context` argument is used to look up the [Navigator] and [Theme] for /// the menu. It is only used when the method is called. Its corresponding /// widget can be safely removed from the tree before the popup menu is closed. -/// +/// +/// The `semanticLabel` argument is used by accessibility frameworks to +/// announce screen transitions when the menu is opened and closed. If this +/// label is not provided, it will default to +/// [MaterialLocalizations.popupMenuLabel]. +/// /// See also: /// /// * [PopupMenuItem], a popup menu entry for a single value. @@ -688,20 +703,34 @@ class _PopupMenuRoute extends PopupRoute { /// * [CheckedPopupMenuItem], a popup menu item with a checkmark. /// * [PopupMenuButton], which provides an [IconButton] that shows a menu by /// calling this method automatically. +/// * [SemanticsConfiguration.namesRoute], for a description of edge triggered +/// semantics. Future showMenu({ @required BuildContext context, RelativeRect position, @required List> items, T initialValue, double elevation: 8.0, + String semanticLabel, }) { assert(context != null); assert(items != null && items.isNotEmpty); + String label = semanticLabel; + switch (defaultTargetPlatform) { + case TargetPlatform.iOS: + label = semanticLabel; + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + label = semanticLabel ?? MaterialLocalizations.of(context)?.popupMenuLabel; + } + return Navigator.push(context, new _PopupMenuRoute( position: position, items: items, initialValue: initialValue, elevation: elevation, + semanticLabel: label, theme: Theme.of(context, shadowThemeOnly: true), barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, )); diff --git a/packages/flutter/lib/src/rendering/custom_paint.dart b/packages/flutter/lib/src/rendering/custom_paint.dart index 1b6b6d04f3b..f0ac0eebadb 100644 --- a/packages/flutter/lib/src/rendering/custom_paint.dart +++ b/packages/flutter/lib/src/rendering/custom_paint.dart @@ -837,6 +837,12 @@ class RenderCustomPaint extends RenderProxyBox { if (properties.header != null) { config.isHeader = properties.header; } + if (properties.scopesRoute != null) { + config.scopesRoute = properties.scopesRoute; + } + if (properties.namesRoute != null) { + config.namesRoute = properties.namesRoute; + } if (properties.label != null) { config.label = properties.label; } diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index d37ccfdfdfb..24710f6a40e 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -3019,6 +3019,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { bool focused, bool inMutuallyExclusiveGroup, bool obscured, + bool scopesRoute, + bool namesRoute, String label, String value, String increasedValue, @@ -3054,6 +3056,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { _focused = focused, _inMutuallyExclusiveGroup = inMutuallyExclusiveGroup, _obscured = obscured, + _scopesRoute = scopesRoute, + _namesRoute = namesRoute, _label = label, _value = value, _increasedValue = increasedValue, @@ -3213,6 +3217,26 @@ class RenderSemanticsAnnotations extends RenderProxyBox { markNeedsSemanticsUpdate(); } + /// If non-null, sets the [SemanticsNode.scopesRoute] semantic to the give value. + bool get scopesRoute => _scopesRoute; + bool _scopesRoute; + set scopesRoute(bool value) { + if (scopesRoute == value) + return; + _scopesRoute = value; + markNeedsSemanticsUpdate(); + } + + /// If non-null, sets the [SemanticsNode.namesRoute] semantic to the give value. + bool get namesRoute => _namesRoute; + bool _namesRoute; + set namesRoute(bool value) { + if (_namesRoute == value) + return; + _namesRoute = value; + markNeedsSemanticsUpdate(); + } + /// If non-null, sets the [SemanticsNode.label] semantic to the given value. /// /// The reading direction is given by [textDirection]. @@ -3633,6 +3657,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { super.describeSemanticsConfiguration(config); config.isSemanticBoundary = container; config.explicitChildNodes = explicitChildNodes; + assert((scopesRoute == true && explicitChildNodes == true) || scopesRoute != true, + 'explicitChildNodes must be set to true if scopes route is true'); if (enabled != null) config.isEnabled = enabled; @@ -3662,6 +3688,10 @@ class RenderSemanticsAnnotations extends RenderProxyBox { config.decreasedValue = decreasedValue; if (hint != null) config.hint = hint; + if (scopesRoute != null) + config.scopesRoute = scopesRoute; + if (namesRoute != null) + config.namesRoute = namesRoute; if (textDirection != null) config.textDirection = textDirection; if (sortKey != null) diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index 41e7c0f6b19..a4170e27fdb 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -320,6 +320,8 @@ class SemanticsProperties extends DiagnosticableTree { this.focused, this.inMutuallyExclusiveGroup, this.obscured, + this.scopesRoute, + this.namesRoute, this.label, this.value, this.increasedValue, @@ -407,6 +409,25 @@ class SemanticsProperties extends DiagnosticableTree { /// Doing so instructs screen readers to not read out the [value]. final bool obscured; + /// If non-null, whether the node corresponds to the root of a subtree for + /// which a route name should be announced. + /// + /// Generally, this is set in combination with [explicitChildNodes], since + /// nodes with this flag are not considered focusable by Android or iOS. + /// + /// See also: + /// + /// * [SemanticsFlag.scopesRoute] for a description of how the announced + /// value is selected. + final bool scopesRoute; + + /// If non-null, whether the node contains the semantic label for a route. + /// + /// See also: + /// + /// * [SemanticsFlag.namesRoute] for a description of how the name is used. + final bool namesRoute; + /// Provides a textual description of the widget. /// /// If a label is provided, there must either by an ambient [Directionality] @@ -2336,6 +2357,25 @@ class SemanticsConfiguration { _hasBeenAnnotated = true; } + /// Whether the semantics node is the root of a subtree for which values + /// should be announced. + /// + /// See also: + /// * [SemanticsFlag.scopesRoute], for a full description of route scoping. + bool get scopesRoute => _hasFlag(SemanticsFlag.scopesRoute); + set scopesRoute(bool value) { + _setFlag(SemanticsFlag.scopesRoute, value); + } + + /// Whether the semantics node contains the label of a route. + /// + /// See also: + /// * [SemanticsFlag.namesRoute], for a full description of route naming. + bool get namesRoute => _hasFlag(SemanticsFlag.namesRoute); + set namesRoute(bool value) { + _setFlag(SemanticsFlag.namesRoute, value); + } + /// The reading direction for the text in [label], [value], [hint], /// [increasedValue], and [decreasedValue]. TextDirection get textDirection => _textDirection; diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 0a1cbade97d..68571fb05ee 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -4896,6 +4896,8 @@ class Semantics extends SingleChildRenderObjectWidget { bool focused, bool inMutuallyExclusiveGroup, bool obscured, + bool scopesRoute, + bool namesRoute, String label, String value, String increasedValue, @@ -4934,6 +4936,8 @@ class Semantics extends SingleChildRenderObjectWidget { focused: focused, inMutuallyExclusiveGroup: inMutuallyExclusiveGroup, obscured: obscured, + scopesRoute: scopesRoute, + namesRoute: namesRoute, label: label, value: value, increasedValue: increasedValue, @@ -4995,6 +4999,10 @@ class Semantics extends SingleChildRenderObjectWidget { /// information to the semantic tree is to introduce new explicit /// [SemanticNode]s to the tree. /// + /// If the semantics properties of this node include + /// [SemanticsProperties.scopesRoute] set to true, then [explicitChildNodes] + /// must be true also. + /// /// This setting is often used in combination with [SemanticsConfiguration.isSemanticBoundary] /// to create semantic boundaries that are either writable or not for children. final bool explicitChildNodes; @@ -5013,6 +5021,8 @@ class Semantics extends SingleChildRenderObjectWidget { focused: properties.focused, inMutuallyExclusiveGroup: properties.inMutuallyExclusiveGroup, obscured: properties.obscured, + scopesRoute: properties.scopesRoute, + namesRoute: properties.namesRoute, label: properties.label, value: properties.value, increasedValue: properties.increasedValue, @@ -5064,6 +5074,8 @@ class Semantics extends SingleChildRenderObjectWidget { ..increasedValue = properties.increasedValue ..decreasedValue = properties.decreasedValue ..hint = properties.hint + ..scopesRoute = properties.scopesRoute + ..namesRoute = properties.namesRoute ..textDirection = _getTextDirection(context) ..sortKey = properties.sortKey ..onTap = properties.onTap diff --git a/packages/flutter/test/material/date_picker_test.dart b/packages/flutter/test/material/date_picker_test.dart index 4cba66ab763..0f1579f4191 100644 --- a/packages/flutter/test/material/date_picker_test.dart +++ b/packages/flutter/test/material/date_picker_test.dart @@ -387,6 +387,9 @@ void _tests() { final SemanticsTester semantics = new SemanticsTester(tester); await preparePicker(tester, (Future date) async { final TestSemantics expected = new TestSemantics( + flags: [ + SemanticsFlag.scopesRoute, + ], children: [ new TestSemantics( actions: [SemanticsAction.tap], @@ -616,7 +619,7 @@ void _tests() { ); expect(semantics, hasSemantics( - expected, + new TestSemantics.root(children: [expected]), ignoreId: true, ignoreTransform: true, ignoreRect: true, diff --git a/packages/flutter/test/material/dialog_test.dart b/packages/flutter/test/material/dialog_test.dart index c2b44acf855..199c5c44fde 100644 --- a/packages/flutter/test/material/dialog_test.dart +++ b/packages/flutter/test/material/dialog_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:matcher/matcher.dart'; @@ -332,4 +334,51 @@ void main() { new Rect.fromLTRB(40.0, 24.0, 800.0 - 40.0, 600.0 - 24.0), ); }); + + testWidgets('Dialog widget contains route semantics from title', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + await tester.pumpWidget( + new MaterialApp( + home: new Material( + child: new Builder( + builder: (BuildContext context) { + return new Center( + child: new RaisedButton( + child: const Text('X'), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return const AlertDialog( + title: const Text('Title'), + content: const Text('Y'), + actions: const [], + ); + }, + ); + }, + ), + ); + }, + ), + ), + ), + ); + + expect(semantics, isNot(includesNodeWith( + label: 'Title', + flags: [SemanticsFlag.namesRoute] + ))); + + await tester.tap(find.text('X')); + await tester.pump(); // start animation + await tester.pump(const Duration(seconds: 1)); + + expect(semantics, includesNodeWith( + label: 'Title', + flags: [SemanticsFlag.namesRoute], + )); + + semantics.dispose(); + }); } diff --git a/packages/flutter/test/material/floating_action_button_test.dart b/packages/flutter/test/material/floating_action_button_test.dart index 44e26c51123..d51810afb93 100644 --- a/packages/flutter/test/material/floating_action_button_test.dart +++ b/packages/flutter/test/material/floating_action_button_test.dart @@ -281,14 +281,21 @@ void main() { expect(semantics, hasSemantics(new TestSemantics.root( children: [ new TestSemantics.rootChild( - label: 'Add Photo', - actions: [ - SemanticsAction.tap - ], flags: [ - SemanticsFlag.isButton, - SemanticsFlag.hasEnabledState, - SemanticsFlag.isEnabled, + SemanticsFlag.scopesRoute, + ], + children: [ + new TestSemantics( + label: 'Add Photo', + actions: [ + SemanticsAction.tap + ], + flags: [ + SemanticsFlag.isButton, + SemanticsFlag.hasEnabledState, + SemanticsFlag.isEnabled, + ], + ), ], ), ], diff --git a/packages/flutter/test/material/popup_menu_test.dart b/packages/flutter/test/material/popup_menu_test.dart index 8410efa8621..5be9fc605f4 100644 --- a/packages/flutter/test/material/popup_menu_test.dart +++ b/packages/flutter/test/material/popup_menu_test.dart @@ -2,11 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show window; +import 'dart:ui' show window, SemanticsFlag; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; +import '../widgets/semantics_tester.dart'; + void main() { testWidgets('Navigator.push works within a PopupMenuButton', (WidgetTester tester) async { final Key targetKey = new UniqueKey(); @@ -427,6 +429,39 @@ void main() { expect(MediaQuery.of(popupContext).padding, EdgeInsets.zero); }); + + testWidgets('PopupMenu includes route semantics', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + await tester.pumpWidget(new MaterialApp( + home: new Material( + child: new PopupMenuButton( + itemBuilder: (BuildContext context) { + return >[ + const PopupMenuItem(value: 2, child: const Text('2')), + const PopupMenuItem(value: 3, child: const Text('3')), + ]; + }, + child: const SizedBox( + height: 100.0, + width: 100.0, + child: const Text('XXX'), + ), + ), + ), + )); + await tester.tap(find.text('XXX')); + await tester.pumpAndSettle(); + + expect(semantics, includesNodeWith( + label: 'Popup menu', + flags: [ + SemanticsFlag.namesRoute, + SemanticsFlag.scopesRoute, + ], + )); + + semantics.dispose(); + }); } class TestApp extends StatefulWidget { diff --git a/packages/flutter/test/material/tooltip_test.dart b/packages/flutter/test/material/tooltip_test.dart index f57625521ec..064e0f994af 100644 --- a/packages/flutter/test/material/tooltip_test.dart +++ b/packages/flutter/test/material/tooltip_test.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + // Copyright 2015 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. @@ -486,12 +488,13 @@ void main() { ); final TestSemantics expected = new TestSemantics.root( - children: [ - new TestSemantics.rootChild( - id: 1, - label: tooltipText, - ), - ] + children: [ + new TestSemantics.rootChild( + id: 1, + label: 'TIP', + textDirection: TextDirection.ltr, + ), + ] ); expect(semantics, hasSemantics(expected, ignoreTransform: true, ignoreRect: true)); @@ -619,8 +622,13 @@ void main() { expect(semantics, hasSemantics(new TestSemantics.root( children: [ new TestSemantics.rootChild( - label: 'Foo\nBar', - textDirection: TextDirection.ltr, + flags: [SemanticsFlag.scopesRoute], + children: [ + new TestSemantics( + label: 'Foo\nBar', + textDirection: TextDirection.ltr, + ) + ], ), ], ), ignoreRect: true, ignoreId: true, ignoreTransform: true)); @@ -646,8 +654,13 @@ void main() { expect(semantics, hasSemantics(new TestSemantics.root( children: [ new TestSemantics.rootChild( - label: 'Bar', - textDirection: TextDirection.ltr, + flags: [SemanticsFlag.scopesRoute], + children: [ + new TestSemantics( + label: 'Bar', + textDirection: TextDirection.ltr, + ) + ], ), ], ), ignoreRect: true, ignoreId: true, ignoreTransform: true)); diff --git a/packages/flutter/test/material/user_accounts_drawer_header_test.dart b/packages/flutter/test/material/user_accounts_drawer_header_test.dart index 1bf1c75dfd2..2a5f0d78435 100644 --- a/packages/flutter/test/material/user_accounts_drawer_header_test.dart +++ b/packages/flutter/test/material/user_accounts_drawer_header_test.dart @@ -335,26 +335,31 @@ void main() { new TestSemantics( children: [ new TestSemantics( - label: 'Signed in\nname\nemail', - textDirection: TextDirection.ltr, + flags: [SemanticsFlag.scopesRoute], children: [ new TestSemantics( - label: r'B', - textDirection: TextDirection.ltr, - ), - new TestSemantics( - label: r'C', - textDirection: TextDirection.ltr, - ), - new TestSemantics( - label: r'D', - textDirection: TextDirection.ltr, - ), - new TestSemantics( - flags: [SemanticsFlag.isButton], - actions: [SemanticsAction.tap], - label: r'Show accounts', + label: 'Signed in\nname\nemail', textDirection: TextDirection.ltr, + children: [ + new TestSemantics( + label: r'B', + textDirection: TextDirection.ltr, + ), + new TestSemantics( + label: r'C', + textDirection: TextDirection.ltr, + ), + new TestSemantics( + label: r'D', + textDirection: TextDirection.ltr, + ), + new TestSemantics( + flags: [SemanticsFlag.isButton], + actions: [SemanticsAction.tap], + label: r'Show accounts', + textDirection: TextDirection.ltr, + ), + ], ), ], ), @@ -382,20 +387,25 @@ void main() { new TestSemantics( children: [ new TestSemantics( - label: 'Signed in', - textDirection: TextDirection.ltr, + flags: [SemanticsFlag.scopesRoute], children: [ new TestSemantics( - label: r'B', - textDirection: TextDirection.ltr, - ), - new TestSemantics( - label: r'C', - textDirection: TextDirection.ltr, - ), - new TestSemantics( - label: r'D', + label: 'Signed in', textDirection: TextDirection.ltr, + children: [ + new TestSemantics( + label: r'B', + textDirection: TextDirection.ltr, + ), + new TestSemantics( + label: r'C', + textDirection: TextDirection.ltr, + ), + new TestSemantics( + label: r'D', + textDirection: TextDirection.ltr, + ), + ], ), ], ), diff --git a/packages/flutter/test/widgets/custom_painter_test.dart b/packages/flutter/test/widgets/custom_painter_test.dart index 6aca623822e..e50730c0e2d 100644 --- a/packages/flutter/test/widgets/custom_painter_test.dart +++ b/packages/flutter/test/widgets/custom_painter_test.dart @@ -416,15 +416,13 @@ void _defineTests() { inMutuallyExclusiveGroup: true, header: true, obscured: true, + scopesRoute: true, + namesRoute: true, ), ), ), )); - // TODO(jonahwilliams): remove when rolling edge semantic support for framework. - final List flags = SemanticsFlag.values.values - .where((SemanticsFlag flag) => flag != SemanticsFlag.scopesRoute && flag != SemanticsFlag.namesRoute) - .toList(); final TestSemantics expectedSemantics = new TestSemantics.root( children: [ new TestSemantics.rootChild( @@ -433,7 +431,7 @@ void _defineTests() { new TestSemantics.rootChild( id: 2, rect: TestSemantics.fullScreen, - flags: flags, + flags: SemanticsFlag.values.values.toList(), ), ] ), diff --git a/packages/flutter/test/widgets/drawer_test.dart b/packages/flutter/test/widgets/drawer_test.dart index 3b8b202ef51..a8f12eda705 100644 --- a/packages/flutter/test/widgets/drawer_test.dart +++ b/packages/flutter/test/widgets/drawer_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui'; + import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -284,4 +286,40 @@ void main() { semantics.dispose(); }); + + testWidgets('Drawer contains route semantics flags', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + final GlobalKey scaffoldKey = new GlobalKey(); + + await tester.pumpWidget( + new MaterialApp( + home: new Builder( + builder: (BuildContext context) { + return new Scaffold( + key: scaffoldKey, + drawer: const Drawer(), + body: new Container(), + ); + }, + ), + ), + ); + + // Open the drawer. + scaffoldKey.currentState.openDrawer(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + expect(semantics, includesNodeWith( + label: 'Navigation menu', + flags: [ + SemanticsFlag.scopesRoute, + SemanticsFlag.namesRoute, + ], + )); + + semantics.dispose(); + }); } + + diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 4a8eead9939..dfcb066bdf7 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -593,12 +593,17 @@ void main() { expect(semantics, hasSemantics(new TestSemantics( children: [ - new TestSemantics( - flags: [SemanticsFlag.isTextField, SemanticsFlag.isObscured], - value: expectedValue, - textDirection: TextDirection.ltr, - nextNodeId: -1, - previousNodeId: -1, + new TestSemantics.rootChild( + flags: [SemanticsFlag.scopesRoute], + children: [ + new TestSemantics( + flags: [SemanticsFlag.isTextField, SemanticsFlag.isObscured], + value: expectedValue, + textDirection: TextDirection.ltr, + nextNodeId: -1, + previousNodeId: -1, + ), + ], ), ], ), ignoreTransform: true, ignoreRect: true, ignoreId: true)); @@ -713,26 +718,32 @@ void main() { await tester.pump(); final SemanticsOwner owner = tester.binding.pipelineOwner.semanticsOwner; - const int expectedNodeId = 2; + const int expectedNodeId = 3; expect(semantics, hasSemantics(new TestSemantics.root( children: [ new TestSemantics.rootChild( - id: expectedNodeId, - flags: [ - SemanticsFlag.isTextField, - SemanticsFlag.isFocused + id: 1, + flags: [SemanticsFlag.scopesRoute], + children: [ + new TestSemantics.rootChild( + id: expectedNodeId, + flags: [ + SemanticsFlag.isTextField, + SemanticsFlag.isFocused + ], + actions: [ + SemanticsAction.moveCursorBackwardByCharacter, + SemanticsAction.setSelection, + SemanticsAction.copy, + SemanticsAction.cut, + SemanticsAction.paste + ], + value: 'test', + textSelection: new TextSelection.collapsed(offset: controller.text.length), + textDirection: TextDirection.ltr, + ), ], - actions: [ - SemanticsAction.moveCursorBackwardByCharacter, - SemanticsAction.setSelection, - SemanticsAction.copy, - SemanticsAction.cut, - SemanticsAction.paste - ], - value: 'test', - textSelection: new TextSelection.collapsed(offset: controller.text.length), - textDirection: TextDirection.ltr, ), ], ), ignoreRect: true, ignoreTransform: true)); diff --git a/packages/flutter/test/widgets/navigator_test.dart b/packages/flutter/test/widgets/navigator_test.dart index e30e270a678..0557a3d6e9e 100644 --- a/packages/flutter/test/widgets/navigator_test.dart +++ b/packages/flutter/test/widgets/navigator_test.dart @@ -2,9 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui'; + import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; +import 'semantics_tester.dart'; + class FirstWidget extends StatelessWidget { @override Widget build(BuildContext context) { @@ -769,4 +773,50 @@ void main() { await tester.pumpAndSettle(const Duration(milliseconds: 10)); expect(log, ['building B', 'building C', 'found C', 'building D', 'building C', 'found C']); }); + + testWidgets('route semantics', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + final Map routes = { + '/': (BuildContext context) => new OnTapPage(id: '1', onTap: () { Navigator.pushNamed(context, '/A'); }), + '/A': (BuildContext context) => new OnTapPage(id: '2', onTap: () { Navigator.pushNamed(context, '/B/C'); }), + '/B/C': (BuildContext context) => const OnTapPage(id: '3'), + }; + + await tester.pumpWidget(new MaterialApp(routes: routes)); + + expect(semantics, includesNodeWith( + flags: [SemanticsFlag.scopesRoute], + )); + expect(semantics, includesNodeWith( + label: 'Page 1', + flags: [SemanticsFlag.namesRoute], + )); + + await tester.tap(find.text('1')); // pushNamed('/A') + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(semantics, includesNodeWith( + flags: [SemanticsFlag.scopesRoute], + )); + expect(semantics, includesNodeWith( + label: 'Page 2', + flags: [SemanticsFlag.namesRoute], + )); + + await tester.tap(find.text('2')); // pushNamed('/B/C') + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(semantics, includesNodeWith( + flags: [SemanticsFlag.scopesRoute], + )); + expect(semantics, includesNodeWith( + label: 'Page 3', + flags: [SemanticsFlag.namesRoute], + )); + + + semantics.dispose(); + }); } diff --git a/packages/flutter/test/widgets/semantics_test.dart b/packages/flutter/test/widgets/semantics_test.dart index 5ae16403ade..730124e1b34 100644 --- a/packages/flutter/test/widgets/semantics_test.dart +++ b/packages/flutter/test/widgets/semantics_test.dart @@ -471,17 +471,17 @@ void main() { inMutuallyExclusiveGroup: true, header: true, obscured: true, + scopesRoute: true, + namesRoute: true, + explicitChildNodes: true, ) ); - // TODO(jonahwilliams): remove when adding engine support for edge semantics - final List flags = SemanticsFlag.values.values - .where((SemanticsFlag flag) => flag != SemanticsFlag.scopesRoute && flag != SemanticsFlag.namesRoute) - .toList(); + final TestSemantics expectedSemantics = new TestSemantics.root( children: [ new TestSemantics.rootChild( rect: TestSemantics.fullScreen, - flags: flags, + flags: SemanticsFlag.values.values.toList(), ), ], ); diff --git a/packages/flutter/test/widgets/semantics_tester_generateTestSemanticsExpressionForCurrentSemanticsTree_test.dart b/packages/flutter/test/widgets/semantics_tester_generateTestSemanticsExpressionForCurrentSemanticsTree_test.dart index f11252e1ef2..85fd95053ad 100644 --- a/packages/flutter/test/widgets/semantics_tester_generateTestSemanticsExpressionForCurrentSemanticsTree_test.dart +++ b/packages/flutter/test/widgets/semantics_tester_generateTestSemanticsExpressionForCurrentSemanticsTree_test.dart @@ -106,29 +106,35 @@ void _tests() { children: [ new TestSemantics( id: 1, + flags: [SemanticsFlag.scopesRoute], children: [ new TestSemantics( - id: 4, + id: 2, children: [ new TestSemantics( - id: 2, - tags: [const SemanticsTag('RenderViewport.twoPane')], - label: 'Plain text', - textDirection: TextDirection.ltr, - nextNodeId: 3, - ), - new TestSemantics( - id: 3, - tags: [const SemanticsTag('RenderViewport.twoPane')], - flags: [SemanticsFlag.hasCheckedState, SemanticsFlag.isChecked, SemanticsFlag.isSelected], - actions: [SemanticsAction.tap, SemanticsAction.decrease], - label: '‪Interactive text‬', - value: 'test-value', - increasedValue: 'test-increasedValue', - decreasedValue: 'test-decreasedValue', - hint: 'test-hint', - textDirection: TextDirection.rtl, - previousNodeId: 2, + id: 5, + children: [ + new TestSemantics( + id: 3, + tags: [const SemanticsTag('RenderViewport.twoPane')], + label: 'Plain text', + textDirection: TextDirection.ltr, + nextNodeId: 4, + ), + new TestSemantics( + id: 4, + tags: [const SemanticsTag('RenderViewport.twoPane')], + flags: [SemanticsFlag.hasCheckedState, SemanticsFlag.isChecked, SemanticsFlag.isSelected], + actions: [SemanticsAction.tap, SemanticsAction.decrease], + label: '‪Interactive text‬', + value: 'test-value', + increasedValue: 'test-increasedValue', + decreasedValue: 'test-decreasedValue', + hint: 'test-hint', + textDirection: TextDirection.rtl, + previousNodeId: 3, + ), + ], ), ], ), diff --git a/packages/flutter/test/widgets/simple_semantics_test.dart b/packages/flutter/test/widgets/simple_semantics_test.dart index 9f4afe7723e..92398d4e8ce 100644 --- a/packages/flutter/test/widgets/simple_semantics_test.dart +++ b/packages/flutter/test/widgets/simple_semantics_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -53,12 +55,19 @@ void main() { expect(semantics, hasSemantics(new TestSemantics.root( children: [ new TestSemantics.rootChild( + rect: new Rect.fromLTWH(0.0, 0.0, 800.0, 600.0), id: 2, - label: 'Hello!', - textDirection: TextDirection.ltr, - rect: new Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), - transform: new Matrix4.translationValues(395.0, 295.0, 0.0), - ) + flags: [SemanticsFlag.scopesRoute], + children: [ + new TestSemantics( + id: 3, + label: 'Hello!', + textDirection: TextDirection.ltr, + rect: new Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), + transform: new Matrix4.translationValues(395.0, 295.0, 0.0), + ) + ], + ), ], ))); diff --git a/packages/flutter/test/widgets/sliver_semantics_test.dart b/packages/flutter/test/widgets/sliver_semantics_test.dart index 8c788f8622c..5e8bbcead27 100644 --- a/packages/flutter/test/widgets/sliver_semantics_test.dart +++ b/packages/flutter/test/widgets/sliver_semantics_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui'; + import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -72,6 +74,7 @@ void main() { ), new TestSemantics( id: 4, + flags: [SemanticsFlag.namesRoute], label: r'Semantics Test with Slivers', textDirection: TextDirection.ltr, nextNodeId: 2, @@ -126,6 +129,7 @@ void main() { ), new TestSemantics( id: 4, + flags: [SemanticsFlag.namesRoute], label: r'Semantics Test with Slivers', textDirection: TextDirection.ltr, previousNodeId: 5, @@ -176,6 +180,7 @@ void main() { ), new TestSemantics( id: 4, + flags: [SemanticsFlag.namesRoute], label: r'Semantics Test with Slivers', textDirection: TextDirection.ltr, nextNodeId: 2, @@ -412,6 +417,7 @@ void main() { new TestSemantics( id: 22, rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.0), + flags: [SemanticsFlag.namesRoute], tags: [RenderViewport.excludeFromScrolling], label: 'AppBar', previousNodeId: 23, @@ -502,6 +508,7 @@ void main() { id: 28, previousNodeId: 29, rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.0), + flags: [SemanticsFlag.namesRoute], tags: [RenderViewport.excludeFromScrolling], label: 'AppBar' ), @@ -594,6 +601,7 @@ void main() { previousNodeId: 35, rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.0), transform: new Matrix4.translation(new Vector3(0.0, 544.0, 0.0)), + flags: [SemanticsFlag.namesRoute], tags: [RenderViewport.excludeFromScrolling], label: 'AppBar' ), @@ -685,6 +693,7 @@ void main() { previousNodeId: 41, rect: new Rect.fromLTRB(0.0, 0.0, 120.0, 20.0), transform: new Matrix4.translation(new Vector3(0.0, 544.0, 0.0)), + flags: [SemanticsFlag.namesRoute], tags: [RenderViewport.excludeFromScrolling], label: 'AppBar' ),