diff --git a/packages/flutter/lib/src/material/dropdown.dart b/packages/flutter/lib/src/material/dropdown.dart index c5c6ddcd9ba..1dea06d31c4 100644 --- a/packages/flutter/lib/src/material/dropdown.dart +++ b/packages/flutter/lib/src/material/dropdown.dart @@ -139,6 +139,7 @@ class _DropdownMenuState extends State<_DropdownMenu> { // // 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 route = widget.route; final double unit = 0.5 / (route.items.length + 1.5); final List children = []; @@ -175,24 +176,30 @@ class _DropdownMenuState extends State<_DropdownMenu> { 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 @@ class _DropdownButtonState extends State> with WidgetsBindi style: _textStyle.copyWith(color: Theme.of(context).hintColor), child: new IgnorePointer( child: widget.hint, + ignoringSemantics: false, ), )); } @@ -681,10 +689,13 @@ class _DropdownButtonState extends State> with WidgetsBindi ); } - 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 c8d4f439af5..83d90a914bc 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -365,7 +365,7 @@ class SemanticsData extends Diagnosticable { 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 f6b40c35009..ecffb4f5562 100644 --- a/packages/flutter/test/material/dropdown_test.dart +++ b/packages/flutter/test/material/dropdown_test.dart @@ -580,4 +580,102 @@ void main() { 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: [ + new TestSemantics.rootChild( + children: [ + new TestSemantics( + flags: [ + SemanticsFlag.scopesRoute, + SemanticsFlag.namesRoute, + ], + label: 'Popup menu', + children: [ + new TestSemantics( + children: [ + new TestSemantics( + children: [ + new TestSemantics( + label: 'one', + textDirection: TextDirection.ltr, + tags: [const SemanticsTag('RenderViewport.twoPane')], + actions: [SemanticsAction.tap], + ), + new TestSemantics( + label: 'two', + textDirection: TextDirection.ltr, + tags: [const SemanticsTag('RenderViewport.twoPane')], + actions: [SemanticsAction.tap], + ), + new TestSemantics( + label: 'three', + textDirection: TextDirection.ltr, + tags: [const SemanticsTag('RenderViewport.twoPane')], + actions: [SemanticsAction.tap], + ), + new TestSemantics( + label: 'four', + textDirection: TextDirection.ltr, + tags: [const SemanticsTag('RenderViewport.twoPane')], + actions: [SemanticsAction.tap], + ), + ], + ), + ], + ), + ], + ), + ], + ), + ], + ), ignoreId: true, ignoreRect: true, ignoreTransform: true)); + semantics.dispose(); + }); }