Allow specifying and a11y label for Icon widget (#12475)

* Allow specifying and a11y label for Icon widget
diff --git a/packages/flutter/lib/src/widgets/icon.dart b/packages/flutter/lib/src/widgets/icon.dart
index f2cef83..c400239 100644
--- a/packages/flutter/lib/src/widgets/icon.dart
+++ b/packages/flutter/lib/src/widgets/icon.dart
@@ -35,7 +35,8 @@
   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 @@
   /// ```
   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 @@
     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 @@
     );
   }
 
+  /// 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 3ad61de..3f95d75 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 @@
     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')));
+  });
 }