diff --git a/packages/flutter/lib/src/widgets/icon.dart b/packages/flutter/lib/src/widgets/icon.dart index f2cef83edb..c400239f8a 100644 --- a/packages/flutter/lib/src/widgets/icon.dart +++ b/packages/flutter/lib/src/widgets/icon.dart @@ -35,7 +35,8 @@ class Icon extends StatelessWidget { const Icon(this.icon, { Key key, this.size, - this.color + this.color, + this.semanticLabel, }) : super(key: key); /// The icon to display. The available icons are described in [Icons]. @@ -83,6 +84,14 @@ class Icon extends StatelessWidget { /// ``` final Color color; + /// Semantic label for the icon. + /// + /// This would be read out in accessibility modes (e.g TalkBack/VoiceOver). + /// This label does not show in the UI. + /// + /// See [Semantics.label]; + final String semanticLabel; + @override Widget build(BuildContext context) { assert(debugCheckHasDirectionality(context)); @@ -93,27 +102,29 @@ class Icon extends StatelessWidget { final double iconSize = size ?? iconTheme.size; if (icon == null) - return new SizedBox(width: iconSize, height: iconSize); + return _wrapWithSemantics(new SizedBox(width: iconSize, height: iconSize)); final double iconOpacity = iconTheme.opacity; Color iconColor = color ?? iconTheme.color; if (iconOpacity != 1.0) iconColor = iconColor.withOpacity(iconColor.opacity * iconOpacity); - return new ExcludeSemantics( - child: new SizedBox( - width: iconSize, - height: iconSize, - child: new Center( - child: new RichText( - textDirection: textDirection, // Since we already fetched it for the assert... - text: new TextSpan( - text: new String.fromCharCode(icon.codePoint), - style: new TextStyle( - inherit: false, - color: iconColor, - fontSize: iconSize, - fontFamily: icon.fontFamily, + return _wrapWithSemantics( + new ExcludeSemantics( + child: new SizedBox( + width: iconSize, + height: iconSize, + child: new Center( + child: new RichText( + textDirection: textDirection, // Since we already fetched it for the assert... + text: new TextSpan( + text: new String.fromCharCode(icon.codePoint), + style: new TextStyle( + inherit: false, + color: iconColor, + fontSize: iconSize, + fontFamily: icon.fontFamily, + ), ), ), ), @@ -122,6 +133,17 @@ class Icon extends StatelessWidget { ); } + /// Wraps the widget with a Semantics widget if [semanticLabel] is set. + Widget _wrapWithSemantics(Widget widget) { + if (semanticLabel == null) + return widget; + + return new Semantics( + child: widget, + label: semanticLabel, + ); + } + @override void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); diff --git a/packages/flutter/test/widgets/icon_test.dart b/packages/flutter/test/widgets/icon_test.dart index 3ad61deebb..3f95d7557b 100644 --- a/packages/flutter/test/widgets/icon_test.dart +++ b/packages/flutter/test/widgets/icon_test.dart @@ -2,9 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; +import 'semantics_tester.dart'; + void main() { testWidgets('Can set opacity for an Icon', (WidgetTester tester) async { await tester.pumpWidget( @@ -122,4 +125,40 @@ void main() { final RichText richText = tester.firstWidget(find.byType(RichText)); expect(richText.text.style.fontFamily, equals('Roboto')); }); + + testWidgets('Icon with semantic label', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: const Center( + child: const Icon( + Icons.title, + semanticLabel: 'a label', + ), + ), + ), + ); + + expect(semantics, hasSemantics(new TestSemantics.root(label: 'a label'))); + }); + + testWidgets('Null icon with semantic label', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: const Center( + child: const Icon( + null, + semanticLabel: 'a label', + ), + ), + ), + ); + + expect(semantics, hasSemantics(new TestSemantics.root(label: 'a label'))); + }); }