Unverified Commit 034a663d authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Semantics object support for edge triggered semantics (#16081)

Semantics object support for edge triggered semantics 
parent d05bc9c0
......@@ -96,7 +96,7 @@ class _RecipeGridPageState extends State<RecipeGridPage> {
_buildBody(context, statusBarHeight),
],
),
)
),
);
}
......@@ -215,30 +215,33 @@ class _PestoLogoState extends State<PestoLogo> {
@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: <Widget>[
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: <Widget>[
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),
),
),
),
],
],
),
),
),
);
......
......@@ -237,7 +237,11 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> 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(
......
......@@ -383,6 +383,15 @@ class _AppBarState extends State<AppBar> {
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,
......
......@@ -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<Widget> 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<Widget> children = <Widget>[];
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<Widget> body = <Widget>[];
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<T> extends PopupRoute<T> {
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<T> showDialog<T>({
) 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<T>(
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,
......
......@@ -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,
),
),
);
}
......
......@@ -141,6 +141,19 @@ class _FlexibleSpaceBarState extends State<FlexibleSpaceBar> {
}
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<FlexibleSpaceBar> {
transform: scaleTransform,
child: new Align(
alignment: titleAlignment,
child: new DefaultTextStyle(style: titleStyle, child: widget.title)
child: new DefaultTextStyle(
style: titleStyle,
child: title,
)
)
)
));
......
......@@ -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<T> extends PageRoute<T> {
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> 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(
......
......@@ -417,10 +417,12 @@ class _CheckedPopupMenuItemState<T> extends PopupMenuItemState<T, CheckedPopupMe
class _PopupMenu<T> extends StatelessWidget {
const _PopupMenu({
Key key,
this.route
this.route,
this.semanticLabel,
}) : super(key: key);
final _PopupMenuRoute<T> route;
final String semanticLabel;
@override
Widget build(BuildContext context) {
......@@ -479,7 +481,13 @@ class _PopupMenu<T> 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<T> extends PopupRoute<T> {
this.elevation,
this.theme,
this.barrierLabel,
this.semanticLabel,
});
final RelativeRect position;
......@@ -584,6 +593,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
final dynamic initialValue;
final double elevation;
final ThemeData theme;
final String semanticLabel;
@override
Animation<double> createAnimation() {
......@@ -620,7 +630,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
}
}
Widget menu = new _PopupMenu<T>(route: this);
Widget menu = new _PopupMenu<T>(route: this, semanticLabel: semanticLabel);
if (theme != null)
menu = new Theme(data: theme, child: menu);
......@@ -680,7 +690,12 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
/// 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<T> extends PopupRoute<T> {
/// * [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<T> showMenu<T>({
@required BuildContext context,
RelativeRect position,
@required List<PopupMenuEntry<T>> 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<T>(
position: position,
items: items,
initialValue: initialValue,
elevation: elevation,
semanticLabel: label,
theme: Theme.of(context, shadowThemeOnly: true),
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
));
......
......@@ -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;
}
......
......@@ -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)
......
......@@ -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;
......
......@@ -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
......
......@@ -387,6 +387,9 @@ void _tests() {
final SemanticsTester semantics = new SemanticsTester(tester);
await preparePicker(tester, (Future<DateTime> date) async {
final TestSemantics expected = new TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.scopesRoute,
],
children: <TestSemantics>[
new TestSemantics(
actions: <SemanticsAction>[SemanticsAction.tap],
......@@ -616,7 +619,7 @@ void _tests() {
);
expect(semantics, hasSemantics(
expected,
new TestSemantics.root(children: <TestSemantics>[expected]),
ignoreId: true,
ignoreTransform: true,
ignoreRect: true,
......
......@@ -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<void>(
context: context,
builder: (BuildContext context) {
return const AlertDialog(
title: const Text('Title'),
content: const Text('Y'),
actions: const <Widget>[],
);
},
);
},
),
);
},
),
),
),
);
expect(semantics, isNot(includesNodeWith(
label: 'Title',
flags: <SemanticsFlag>[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>[SemanticsFlag.namesRoute],
));
semantics.dispose();
});
}
......@@ -281,14 +281,21 @@ void main() {
expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
label: 'Add Photo',
actions: <SemanticsAction>[
SemanticsAction.tap
],
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.scopesRoute,
],
children: <TestSemantics>[
new TestSemantics(
label: 'Add Photo',
actions: <SemanticsAction>[
SemanticsAction.tap
],
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
],
),
],
),
],
......
......@@ -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<int>(
itemBuilder: (BuildContext context) {
return <PopupMenuItem<int>>[
const PopupMenuItem<int>(value: 2, child: const Text('2')),
const PopupMenuItem<int>(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>[
SemanticsFlag.namesRoute,
SemanticsFlag.scopesRoute,
],
));
semantics.dispose();
});
}
class TestApp extends StatefulWidget {
......
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: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
label: tooltipText,
),
]
children: <TestSemantics>[
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: <TestSemantics>[
new TestSemantics.rootChild(
label: 'Foo\nBar',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
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: <TestSemantics>[
new TestSemantics.rootChild(
label: 'Bar',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
new TestSemantics(
label: 'Bar',
textDirection: TextDirection.ltr,
)
],
),
],
), ignoreRect: true, ignoreId: true, ignoreTransform: true));
......
......@@ -335,26 +335,31 @@ void main() {
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
label: 'Signed in\nname\nemail',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
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>[SemanticsFlag.isButton],
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'Show accounts',
label: 'Signed in\nname\nemail',
textDirection: TextDirection.ltr,
children: <TestSemantics>[
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>[SemanticsFlag.isButton],
actions: <SemanticsAction>[SemanticsAction.tap],
label: r'Show accounts',
textDirection: TextDirection.ltr,
),
],
),
],
),
......@@ -382,20 +387,25 @@ void main() {
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
label: 'Signed in',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
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: <TestSemantics>[
new TestSemantics(
label: r'B',
textDirection: TextDirection.ltr,
),
new TestSemantics(
label: r'C',
textDirection: TextDirection.ltr,
),
new TestSemantics(
label: r'D',
textDirection: TextDirection.ltr,
),
],
),
],
),
......
......@@ -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<SemanticsFlag> flags = SemanticsFlag.values.values
.where((SemanticsFlag flag) => flag != SemanticsFlag.scopesRoute && flag != SemanticsFlag.namesRoute)
.toList();
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
......@@ -433,7 +431,7 @@ void _defineTests() {
new TestSemantics.rootChild(
id: 2,
rect: TestSemantics.fullScreen,
flags: flags,
flags: SemanticsFlag.values.values.toList(),
),
]
),
......
......@@ -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<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
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>[
SemanticsFlag.scopesRoute,
SemanticsFlag.namesRoute,
],
));
semantics.dispose();
});
}
......@@ -593,12 +593,17 @@ void main() {
expect(semantics, hasSemantics(new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isObscured],
value: expectedValue,
textDirection: TextDirection.ltr,
nextNodeId: -1,
previousNodeId: -1,
new TestSemantics.rootChild(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
new TestSemantics(
flags: <SemanticsFlag>[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: <TestSemantics>[
new TestSemantics.rootChild(
id: expectedNodeId,
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused
],
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
SemanticsAction.copy,
SemanticsAction.cut,
SemanticsAction.paste
id: 1,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
new TestSemantics.rootChild(
id: expectedNodeId,
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused
],
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
SemanticsAction.copy,
SemanticsAction.cut,
SemanticsAction.paste
],
value: 'test',
textSelection: new TextSelection.collapsed(offset: controller.text.length),
textDirection: TextDirection.ltr,
),
],
value: 'test',
textSelection: new TextSelection.collapsed(offset: controller.text.length),
textDirection: TextDirection.ltr,
),
],
), ignoreRect: true, ignoreTransform: true));
......
......@@ -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, <String>['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<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (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>[SemanticsFlag.scopesRoute],
));
expect(semantics, includesNodeWith(
label: 'Page 1',
flags: <SemanticsFlag>[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>[SemanticsFlag.scopesRoute],
));
expect(semantics, includesNodeWith(
label: 'Page 2',
flags: <SemanticsFlag>[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>[SemanticsFlag.scopesRoute],
));
expect(semantics, includesNodeWith(
label: 'Page 3',
flags: <SemanticsFlag>[SemanticsFlag.namesRoute],
));
semantics.dispose();
});
}
......@@ -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<SemanticsFlag> flags = SemanticsFlag.values.values
.where((SemanticsFlag flag) => flag != SemanticsFlag.scopesRoute && flag != SemanticsFlag.namesRoute)
.toList();
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
rect: TestSemantics.fullScreen,
flags: flags,
flags: SemanticsFlag.values.values.toList(),
),
],
);
......
......@@ -106,29 +106,35 @@ void _tests() {
children: <TestSemantics>[
new TestSemantics(
id: 1,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
new TestSemantics(
id: 4,
id: 2,
children: <TestSemantics>[
new TestSemantics(
id: 2,
tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
label: 'Plain text',
textDirection: TextDirection.ltr,
nextNodeId: 3,
),
new TestSemantics(
id: 3,
tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
flags: <SemanticsFlag>[SemanticsFlag.hasCheckedState, SemanticsFlag.isChecked, SemanticsFlag.isSelected],
actions: <SemanticsAction>[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: <TestSemantics>[
new TestSemantics(
id: 3,
tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
label: 'Plain text',
textDirection: TextDirection.ltr,
nextNodeId: 4,
),
new TestSemantics(
id: 4,
tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
flags: <SemanticsFlag>[SemanticsFlag.hasCheckedState, SemanticsFlag.isChecked, SemanticsFlag.isSelected],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.decrease],
label: '‪Interactive text‬',
value: 'test-value',
increasedValue: 'test-increasedValue',
decreasedValue: 'test-decreasedValue',
hint: 'test-hint',
textDirection: TextDirection.rtl,
previousNodeId: 3,
),
],
),
],
),
......
......@@ -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: <TestSemantics>[
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>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
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),
)
],
),
],
)));
......
......@@ -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>[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>[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>[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>[SemanticsFlag.namesRoute],
tags: <SemanticsTag>[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>[SemanticsFlag.namesRoute],
tags: <SemanticsTag>[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>[SemanticsFlag.namesRoute],
tags: <SemanticsTag>[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>[SemanticsFlag.namesRoute],
tags: <SemanticsTag>[RenderViewport.excludeFromScrolling],
label: 'AppBar'
),
......
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