diff --git a/examples/flutter_gallery/lib/demo/animation/widgets.dart b/examples/flutter_gallery/lib/demo/animation/widgets.dart index c8ca06c5936..08bbfd2b4a0 100644 --- a/examples/flutter_gallery/lib/demo/animation/widgets.dart +++ b/examples/flutter_gallery/lib/demo/animation/widgets.dart @@ -19,23 +19,27 @@ class SectionCard extends StatelessWidget { @override Widget build(BuildContext context) { - return new DecoratedBox( - decoration: new BoxDecoration( - gradient: new LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - section.leftColor, - section.rightColor, - ], + return new Semantics( + label: section.title, + button: true, + child: new DecoratedBox( + decoration: new BoxDecoration( + gradient: new LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + section.leftColor, + section.rightColor, + ], + ), + ), + child: new Image.asset( + section.backgroundAsset, + package: section.backgroundAssetPackage, + color: const Color.fromRGBO(255, 255, 255, 0.075), + colorBlendMode: BlendMode.modulate, + fit: BoxFit.cover, ), - ), - child: new Image.asset( - section.backgroundAsset, - package: section.backgroundAssetPackage, - color: const Color.fromRGBO(255, 255, 255, 0.075), - colorBlendMode: BlendMode.modulate, - fit: BoxFit.cover, ), ); } diff --git a/examples/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart b/examples/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart index 327f1db51f7..9990dc7ce1e 100644 --- a/examples/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart +++ b/examples/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart @@ -107,6 +107,7 @@ class ExitButton extends StatelessWidget { child: const Tooltip( message: 'Back', child: const Text('Exit'), + excludeFromSemantics: true, ), onPressed: () { // The demo is on the root navigator. diff --git a/examples/flutter_gallery/lib/demo/material/buttons_demo.dart b/examples/flutter_gallery/lib/demo/material/buttons_demo.dart index ebf98ff245b..cf58a49052e 100644 --- a/examples/flutter_gallery/lib/demo/material/buttons_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/buttons_demo.dart @@ -218,14 +218,20 @@ class _ButtonsDemoState extends State { mainAxisSize: MainAxisSize.min, children: [ new IconButton( - icon: const Icon(Icons.thumb_up), + icon: const Icon( + Icons.thumb_up, + semanticLabel: 'Thumbs up', + ), onPressed: () { setState(() => iconButtonToggle = !iconButtonToggle); }, color: iconButtonToggle ? Theme.of(context).primaryColor : null, ), const IconButton( - icon: const Icon(Icons.thumb_up), + icon: const Icon( + Icons.thumb_up, + semanticLabel: 'Thumbs up', + ), onPressed: null, ) ] diff --git a/examples/flutter_gallery/lib/demo/material/icons_demo.dart b/examples/flutter_gallery/lib/demo/material/icons_demo.dart index 1b7de49d5e2..07f4dd04028 100644 --- a/examples/flutter_gallery/lib/demo/material/icons_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/icons_demo.dart @@ -108,21 +108,24 @@ class _IconsDemoCard extends StatelessWidget { return new Card( child: new DefaultTextStyle( style: textStyle, - child: new Table( - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: [ - new TableRow( - children: [ - _centeredText('Size'), - _centeredText('Enabled'), - _centeredText('Disabled'), - ] - ), - _buildIconRow(18.0), - _buildIconRow(24.0), - _buildIconRow(36.0), - _buildIconRow(48.0), - ], + child: new Semantics( + explicitChildNodes: true, + child: new Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + new TableRow( + children: [ + _centeredText('Size'), + _centeredText('Enabled'), + _centeredText('Disabled'), + ] + ), + _buildIconRow(18.0), + _buildIconRow(24.0), + _buildIconRow(36.0), + _buildIconRow(48.0), + ], + ), ), ), ); diff --git a/examples/flutter_gallery/lib/demo/material/persistent_bottom_sheet_demo.dart b/examples/flutter_gallery/lib/demo/material/persistent_bottom_sheet_demo.dart index 1f2d55302b2..2fb695a45ed 100644 --- a/examples/flutter_gallery/lib/demo/material/persistent_bottom_sheet_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/persistent_bottom_sheet_demo.dart @@ -76,7 +76,10 @@ class _PersistentBottomSheetDemoState extends State { floatingActionButton: new FloatingActionButton( onPressed: _showMessage, backgroundColor: Colors.redAccent, - child: const Icon(Icons.add) + child: const Icon( + Icons.add, + semanticLabel: 'Add', + ), ), body: new Center( child: new RaisedButton( diff --git a/examples/flutter_gallery/lib/gallery/demo.dart b/examples/flutter_gallery/lib/gallery/demo.dart index a18d0d9c324..c6e477ce610 100644 --- a/examples/flutter_gallery/lib/gallery/demo.dart +++ b/examples/flutter_gallery/lib/gallery/demo.dart @@ -154,7 +154,10 @@ class FullScreenCodeDialogState extends State { return new Scaffold( appBar: new AppBar( leading: new IconButton( - icon: const Icon(Icons.clear), + icon: const Icon( + Icons.clear, + semanticLabel: 'Close', + ), onPressed: () { Navigator.pop(context); } ), title: const Text('Example code') diff --git a/examples/flutter_gallery/lib/gallery/drawer.dart b/examples/flutter_gallery/lib/gallery/drawer.dart index 1089224c504..5a4c0a2ef33 100644 --- a/examples/flutter_gallery/lib/gallery/drawer.dart +++ b/examples/flutter_gallery/lib/gallery/drawer.dart @@ -52,51 +52,54 @@ class _GalleryDrawerHeaderState extends State { Widget build(BuildContext context) { final double systemTopPadding = MediaQuery.of(context).padding.top; - return new DrawerHeader( - decoration: new FlutterLogoDecoration( - margin: new EdgeInsets.fromLTRB(12.0, 12.0 + systemTopPadding, 12.0, 12.0), - style: _logoHasName ? _logoHorizontal ? FlutterLogoStyle.horizontal - : FlutterLogoStyle.stacked - : FlutterLogoStyle.markOnly, - lightColor: _logoColor.shade400, - darkColor: _logoColor.shade900, - textColor: widget.light ? const Color(0xFF616161) : const Color(0xFF9E9E9E), + return new Semantics( + label: 'Flutter', + child: new DrawerHeader( + decoration: new FlutterLogoDecoration( + margin: new EdgeInsets.fromLTRB(12.0, 12.0 + systemTopPadding, 12.0, 12.0), + style: _logoHasName ? _logoHorizontal ? FlutterLogoStyle.horizontal + : FlutterLogoStyle.stacked + : FlutterLogoStyle.markOnly, + lightColor: _logoColor.shade400, + darkColor: _logoColor.shade900, + textColor: widget.light ? const Color(0xFF616161) : const Color(0xFF9E9E9E), + ), + duration: const Duration(milliseconds: 750), + child: new GestureDetector( + onLongPress: () { + setState(() { + _logoHorizontal = !_logoHorizontal; + if (!_logoHasName) + _logoHasName = true; + }); + }, + onTap: () { + setState(() { + _logoHasName = !_logoHasName; + }); + }, + onDoubleTap: () { + setState(() { + final List options = []; + if (_logoColor != Colors.blue) + options.addAll([Colors.blue, Colors.blue, Colors.blue, Colors.blue, Colors.blue, Colors.blue, Colors.blue]); + if (_logoColor != Colors.amber) + options.addAll([Colors.amber, Colors.amber, Colors.amber]); + if (_logoColor != Colors.red) + options.addAll([Colors.red, Colors.red, Colors.red]); + if (_logoColor != Colors.indigo) + options.addAll([Colors.indigo, Colors.indigo, Colors.indigo]); + if (_logoColor != Colors.pink) + options.addAll([Colors.pink]); + if (_logoColor != Colors.purple) + options.addAll([Colors.purple]); + if (_logoColor != Colors.cyan) + options.addAll([Colors.cyan]); + _logoColor = options[new math.Random().nextInt(options.length)]; + }); + } + ), ), - duration: const Duration(milliseconds: 750), - child: new GestureDetector( - onLongPress: () { - setState(() { - _logoHorizontal = !_logoHorizontal; - if (!_logoHasName) - _logoHasName = true; - }); - }, - onTap: () { - setState(() { - _logoHasName = !_logoHasName; - }); - }, - onDoubleTap: () { - setState(() { - final List options = []; - if (_logoColor != Colors.blue) - options.addAll([Colors.blue, Colors.blue, Colors.blue, Colors.blue, Colors.blue, Colors.blue, Colors.blue]); - if (_logoColor != Colors.amber) - options.addAll([Colors.amber, Colors.amber, Colors.amber]); - if (_logoColor != Colors.red) - options.addAll([Colors.red, Colors.red, Colors.red]); - if (_logoColor != Colors.indigo) - options.addAll([Colors.indigo, Colors.indigo, Colors.indigo]); - if (_logoColor != Colors.pink) - options.addAll([Colors.pink]); - if (_logoColor != Colors.purple) - options.addAll([Colors.purple]); - if (_logoColor != Colors.cyan) - options.addAll([Colors.cyan]); - _logoColor = options[new math.Random().nextInt(options.length)]; - }); - } - ) ); } } diff --git a/packages/flutter/lib/src/material/floating_action_button.dart b/packages/flutter/lib/src/material/floating_action_button.dart index 4d9e3f16a54..75fae20700f 100644 --- a/packages/flutter/lib/src/material/floating_action_button.dart +++ b/packages/flutter/lib/src/material/floating_action_button.dart @@ -165,10 +165,14 @@ class _FloatingActionButtonState extends State { child: new Container( width: widget.mini ? _kSizeMini : _kSize, height: widget.mini ? _kSizeMini : _kSize, - child: new InkWell( - onTap: widget.onPressed, - onHighlightChanged: _handleHighlightChanged, - child: result, + child: new Semantics( + button: true, + enabled: widget.onPressed != null, + child: new InkWell( + onTap: widget.onPressed, + onHighlightChanged: _handleHighlightChanged, + child: result, + ), ), ), ); diff --git a/packages/flutter/lib/src/material/icon_button.dart b/packages/flutter/lib/src/material/icon_button.dart index ed7a69ffd4f..fe833d6feaf 100644 --- a/packages/flutter/lib/src/material/icon_button.dart +++ b/packages/flutter/lib/src/material/icon_button.dart @@ -193,6 +193,7 @@ class IconButton extends StatelessWidget { Widget result = new Semantics( button: true, + enabled: onPressed != null, child: new ConstrainedBox( constraints: const BoxConstraints(minWidth: _kMinButtonSize, minHeight: _kMinButtonSize), child: new Padding( diff --git a/packages/flutter/lib/src/material/tooltip.dart b/packages/flutter/lib/src/material/tooltip.dart index fcc2125cdd3..52900a1ee79 100644 --- a/packages/flutter/lib/src/material/tooltip.dart +++ b/packages/flutter/lib/src/material/tooltip.dart @@ -48,12 +48,14 @@ class Tooltip extends StatefulWidget { this.padding: const EdgeInsets.symmetric(horizontal: 16.0), this.verticalOffset: 24.0, this.preferBelow: true, + this.excludeFromSemantics: false, this.child, }) : assert(message != null), assert(height != null), assert(padding != null), assert(verticalOffset != null), assert(preferBelow != null), + assert(excludeFromSemantics != null), super(key: key); /// The text to display in the tooltip. @@ -77,6 +79,10 @@ class Tooltip extends StatefulWidget { /// direction. final bool preferBelow; + /// Whether the tooltip's [message] should be excluded from the semantics + /// tree. + final bool excludeFromSemantics; + /// The widget below this widget in the tree. /// /// {@macro flutter.widgets.child} @@ -191,7 +197,7 @@ class _TooltipState extends State with SingleTickerProviderStateMixin { onLongPress: _handleLongPress, excludeFromSemantics: true, child: new Semantics( - label: widget.message, + label: widget.excludeFromSemantics ? null : widget.message, child: widget.child, ), ); diff --git a/packages/flutter/test/material/date_picker_test.dart b/packages/flutter/test/material/date_picker_test.dart index 7e7f9974d3b..137907d7902 100644 --- a/packages/flutter/test/material/date_picker_test.dart +++ b/packages/flutter/test/material/date_picker_test.dart @@ -573,13 +573,21 @@ void _tests() { ], ), new TestSemantics( - flags: [SemanticsFlag.isButton], + flags: [ + SemanticsFlag.isButton, + SemanticsFlag.hasEnabledState, + SemanticsFlag.isEnabled, + ], actions: [SemanticsAction.tap], label: r'Previous month December 2015', textDirection: TextDirection.ltr, ), new TestSemantics( - flags: [SemanticsFlag.isButton], + flags: [ + SemanticsFlag.isButton, + SemanticsFlag.hasEnabledState, + SemanticsFlag.isEnabled, + ], actions: [SemanticsAction.tap], label: r'Next month February 2016', textDirection: TextDirection.ltr, diff --git a/packages/flutter/test/material/floating_action_button_test.dart b/packages/flutter/test/material/floating_action_button_test.dart index 7471b68b87d..e14884effef 100644 --- a/packages/flutter/test/material/floating_action_button_test.dart +++ b/packages/flutter/test/material/floating_action_button_test.dart @@ -2,9 +2,14 @@ // 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_test/flutter_test.dart'; +import '../widgets/semantics_tester.dart'; + void main() { testWidgets('Floating Action Button control test', (WidgetTester tester) async { bool didPressButton = false; @@ -132,4 +137,68 @@ void main() { await tester.pump(); expect(tester.takeException().toString(), contains('xyzzy')); }); + + testWidgets('Floating Action Button semantics (enabled)', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Center( + child: new FloatingActionButton( + onPressed: () { }, + child: const Icon(Icons.add, semanticLabel: 'Add'), + ), + ), + ), + ); + + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + label: 'Add', + flags: [ + SemanticsFlag.isButton, + SemanticsFlag.hasEnabledState, + SemanticsFlag.isEnabled, + ], + actions: [ + SemanticsAction.tap + ], + ), + ], + ), ignoreTransform: true, ignoreId: true, ignoreRect: true)); + + semantics.dispose(); + }); + + testWidgets('Floating Action Button semantics (disabled)', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: const Center( + child: const FloatingActionButton( + onPressed: null, + child: const Icon(Icons.add, semanticLabel: 'Add'), + ), + ), + ), + ); + + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + label: 'Add', + flags: [ + SemanticsFlag.isButton, + SemanticsFlag.hasEnabledState, + ], + ), + ], + ), ignoreTransform: true, ignoreId: true, ignoreRect: true)); + + semantics.dispose(); + }); } diff --git a/packages/flutter/test/material/icon_button_test.dart b/packages/flutter/test/material/icon_button_test.dart index 7fff066e7a3..53c75e5f5a8 100644 --- a/packages/flutter/test/material/icon_button_test.dart +++ b/packages/flutter/test/material/icon_button_test.dart @@ -275,7 +275,7 @@ void main() { await gesture.up(); }); - testWidgets('IconButton Semantics', (WidgetTester tester) async { + testWidgets('IconButton Semantics (enabled)', (WidgetTester tester) async { final SemanticsTester semantics = new SemanticsTester(tester); await tester.pumpWidget( @@ -291,8 +291,14 @@ void main() { children: [ new TestSemantics.rootChild( rect: new Rect.fromLTRB(0.0, 0.0, 48.0, 48.0), - actions: [SemanticsAction.tap], - flags: [SemanticsFlag.isButton], + actions: [ + SemanticsAction.tap + ], + flags: [ + SemanticsFlag.hasEnabledState, + SemanticsFlag.isEnabled, + SemanticsFlag.isButton + ], label: 'link', ) ] @@ -300,6 +306,34 @@ void main() { semantics.dispose(); }); + + testWidgets('IconButton Semantics (disabled)', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await tester.pumpWidget( + wrap( + child: const IconButton( + onPressed: null, + icon: const Icon(Icons.link, semanticLabel: 'link'), + ), + ), + ); + + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + rect: new Rect.fromLTRB(0.0, 0.0, 48.0, 48.0), + flags: [ + SemanticsFlag.hasEnabledState, + SemanticsFlag.isButton + ], + label: 'link', + ) + ] + ), ignoreId: true, ignoreTransform: true)); + + semantics.dispose(); + }); } Widget wrap({ Widget child }) { diff --git a/packages/flutter/test/material/tooltip_test.dart b/packages/flutter/test/material/tooltip_test.dart index 8b0842b9ed2..67b2d327894 100644 --- a/packages/flutter/test/material/tooltip_test.dart +++ b/packages/flutter/test/material/tooltip_test.dart @@ -602,4 +602,57 @@ void main() { feedback.dispose(); }); + testWidgets('Semantics included', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await tester.pumpWidget( + new MaterialApp( + home: const Center( + child: const Tooltip( + message: 'Foo', + child: const Text('Bar'), + ), + ), + ), + ); + + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + label: 'Foo\nBar', + textDirection: TextDirection.ltr, + ), + ], + ), ignoreRect: true, ignoreId: true, ignoreTransform: true)); + + semantics.dispose(); + }); + + testWidgets('Semantics excluded', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await tester.pumpWidget( + new MaterialApp( + home: const Center( + child: const Tooltip( + message: 'Foo', + child: const Text('Bar'), + excludeFromSemantics: true, + ), + ), + ), + ); + + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + label: 'Bar', + textDirection: TextDirection.ltr, + ), + ], + ), ignoreRect: true, ignoreId: true, ignoreTransform: true)); + + semantics.dispose(); + }); + }