Fix dropdown button semantics (#19932)

diff --git a/packages/flutter/lib/src/material/dropdown.dart b/packages/flutter/lib/src/material/dropdown.dart
index c5c6ddc..1dea06d 100644
--- a/packages/flutter/lib/src/material/dropdown.dart
+++ b/packages/flutter/lib/src/material/dropdown.dart
@@ -139,6 +139,7 @@
     //
     // When the menu is dismissed we just fade the entire thing out
     // in the first 0.25s.
+    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
     final _DropdownRoute<T> route = widget.route;
     final double unit = 0.5 / (route.items.length + 1.5);
     final List<Widget> children = <Widget>[];
@@ -175,24 +176,30 @@
           selectedIndex: route.selectedIndex,
           resize: _resize,
         ),
-        child: new Material(
-          type: MaterialType.transparency,
-          textStyle: route.style,
-          child: new ScrollConfiguration(
-            behavior: const _DropdownScrollBehavior(),
-            child: new Scrollbar(
-              child: new ListView(
-                controller: widget.route.scrollController,
-                padding: kMaterialListPadding,
-                itemExtent: _kMenuItemHeight,
-                shrinkWrap: true,
-                children: children,
+        child: new Semantics(
+          scopesRoute: true,
+          namesRoute: true,
+          explicitChildNodes: true,
+          label: localizations.popupMenuLabel,
+          child: new Material(
+              type: MaterialType.transparency,
+              textStyle: route.style,
+              child: new ScrollConfiguration(
+                behavior: const _DropdownScrollBehavior(),
+                child: new Scrollbar(
+                  child: new ListView(
+                    controller: widget.route.scrollController,
+                    padding: kMaterialListPadding,
+                    itemExtent: _kMenuItemHeight,
+                    shrinkWrap: true,
+                    children: children,
+                  ),
+                ),
               ),
             ),
           ),
         ),
-      ),
-    );
+      );
   }
 }
 
@@ -627,6 +634,7 @@
         style: _textStyle.copyWith(color: Theme.of(context).hintColor),
         child: new IgnorePointer(
           child: widget.hint,
+          ignoringSemantics: false,
         ),
       ));
     }
@@ -681,10 +689,13 @@
       );
     }
 
-    return new GestureDetector(
-      onTap: _handleTap,
-      behavior: HitTestBehavior.opaque,
-      child: result
+    return new Semantics(
+      button: true,
+      child: new GestureDetector(
+        onTap: _handleTap,
+        behavior: HitTestBehavior.opaque,
+        child: result
+      ),
     );
   }
 }
diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart
index c8d4f43..83d90a9 100644
--- a/packages/flutter/lib/src/semantics/semantics.dart
+++ b/packages/flutter/lib/src/semantics/semantics.dart
@@ -365,7 +365,7 @@
       scrollExtentMax,
       scrollExtentMin,
       transform,
-      customSemanticsActionIds,
+      ui.hashList(customSemanticsActionIds),
     );
   }
 
diff --git a/packages/flutter/test/material/dropdown_test.dart b/packages/flutter/test/material/dropdown_test.dart
index f6b40c3..ecffb4f 100644
--- a/packages/flutter/test/material/dropdown_test.dart
+++ b/packages/flutter/test/material/dropdown_test.dart
@@ -580,4 +580,102 @@
 
     semantics.dispose();
   });
+
+  testWidgets('Dropdown button includes semantics', (WidgetTester tester) async {
+    final SemanticsHandle handle = tester.ensureSemantics();
+    const Key key = const Key('test');
+    await tester.pumpWidget(buildFrame(
+      buttonKey: key,
+      value: null,
+      items: menuItems,
+      onChanged: (String _) {},
+      hint: const Text('test'),
+    ));
+
+    // By default the hint contributes the label.
+    expect(tester.getSemanticsData(find.byKey(key)), matchesSemanticsData(
+      isButton: true,
+      label: 'test',
+      hasTapAction: true,
+    ));
+
+    await tester.pumpWidget(buildFrame(
+      buttonKey: key,
+      value: 'three',
+      items: menuItems,
+      onChanged: null,
+      hint: const Text('test'),
+    ));
+
+    // Displays label of select item and is no longer tappable.
+    expect(tester.getSemanticsData(find.byKey(key)), matchesSemanticsData(
+      isButton: true,
+      label: 'three',
+      hasTapAction: true,
+    ));
+    handle.dispose();
+  });
+
+  testWidgets('Dropdown menu includes semantics', (WidgetTester tester) async {
+    final SemanticsTester semantics = new SemanticsTester(tester);
+    const Key key = const Key('test');
+    await tester.pumpWidget(buildFrame(
+      buttonKey: key,
+      value: null,
+      items: menuItems,
+    ));
+    await tester.tap(find.byKey(key));
+    await tester.pumpAndSettle();
+
+    expect(semantics, hasSemantics(new TestSemantics.root(
+      children: <TestSemantics>[
+        new TestSemantics.rootChild(
+          children: <TestSemantics>[
+            new TestSemantics(
+              flags: <SemanticsFlag>[
+                SemanticsFlag.scopesRoute,
+                SemanticsFlag.namesRoute,
+              ],
+              label: 'Popup menu',
+              children: <TestSemantics>[
+                new TestSemantics(
+                  children: <TestSemantics>[
+                    new TestSemantics(
+                      children: <TestSemantics>[
+                        new TestSemantics(
+                          label: 'one',
+                          textDirection: TextDirection.ltr,
+                          tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
+                          actions: <SemanticsAction>[SemanticsAction.tap],
+                        ),
+                        new TestSemantics(
+                          label: 'two',
+                          textDirection: TextDirection.ltr,
+                          tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
+                          actions: <SemanticsAction>[SemanticsAction.tap],
+                        ),
+                        new TestSemantics(
+                          label: 'three',
+                          textDirection: TextDirection.ltr,
+                          tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
+                          actions: <SemanticsAction>[SemanticsAction.tap],
+                        ),
+                        new TestSemantics(
+                          label: 'four',
+                          textDirection: TextDirection.ltr,
+                          tags: <SemanticsTag>[const SemanticsTag('RenderViewport.twoPane')],
+                          actions: <SemanticsAction>[SemanticsAction.tap],
+                        ),
+                      ],
+                    ),
+                  ],
+                ),
+              ],
+            ),
+          ],
+        ),
+      ],
+    ), ignoreId: true, ignoreRect: true, ignoreTransform: true));
+    semantics.dispose();
+  });
 }