diff --git a/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart b/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart index 7504629d0a..3879d88215 100644 --- a/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart +++ b/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart @@ -156,25 +156,32 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { final List result = []; for (int index = 0; index < items.length; index += 1) { + final bool active = index == currentIndex; result.add( _wrapActiveItem( new Expanded( - child: new GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onTap == null ? null : () { onTap(index); }, - child: new Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: new Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - new Expanded(child: new Center(child: items[index].icon)), - items[index].title, - ], + child: new Semantics( + selected: active, + // TODO(https://github.com/flutter/flutter/issues/13452): + // This needs localization support. + hint: 'tab, ${index + 1} of ${items.length}', + child: new GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap == null ? null : () { onTap(index); }, + child: new Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: new Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + new Expanded(child: new Center(child: items[index].icon)), + items[index].title, + ], + ), ), ), ), ), - active: index == currentIndex, + active: active, ), ); } @@ -183,7 +190,7 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { } /// Change the active tab item's icon and title colors to active. - Widget _wrapActiveItem(Widget item, { bool active }) { + Widget _wrapActiveItem(Widget item, { @required bool active }) { if (!active) return item; diff --git a/packages/flutter/test/cupertino/bottom_tab_bar_test.dart b/packages/flutter/test/cupertino/bottom_tab_bar_test.dart index e951cdf0c6..d2f94744a5 100644 --- a/packages/flutter/test/cupertino/bottom_tab_bar_test.dart +++ b/packages/flutter/test/cupertino/bottom_tab_bar_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; import '../painting/mocks_for_image_cache.dart'; +import '../widgets/semantics_tester.dart'; Future pumpWidgetWithBoilerplate(WidgetTester tester, Widget widget) async { await tester.pumpWidget( @@ -169,4 +170,39 @@ void main() { await tester.tap(find.text('Tab 1')); expect(callbackTab, 0); }); + + testWidgets('tabs announce semantics', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await pumpWidgetWithBoilerplate(tester, new MediaQuery( + data: const MediaQueryData(), + child: new CupertinoTabBar( + items: const [ + const BottomNavigationBarItem( + icon: const ImageIcon(const TestImageProvider(24, 24)), + title: const Text('Tab 1'), + ), + const BottomNavigationBarItem( + icon: const ImageIcon(const TestImageProvider(24, 24)), + title: const Text('Tab 2'), + ), + ], + ), + )); + + expect(semantics, includesNodeWith( + label: 'Tab 1', + hint: 'tab, 1 of 2', + flags: [SemanticsFlag.isSelected], + textDirection: TextDirection.ltr, + )); + + expect(semantics, includesNodeWith( + label: 'Tab 2', + hint: 'tab, 2 of 2', + textDirection: TextDirection.ltr, + )); + + semantics.dispose(); + }); } diff --git a/packages/flutter/test/widgets/semantics_tester.dart b/packages/flutter/test/widgets/semantics_tester.dart index f183870e08..4a3c276b52 100644 --- a/packages/flutter/test/widgets/semantics_tester.dart +++ b/packages/flutter/test/widgets/semantics_tester.dart @@ -384,6 +384,7 @@ class SemanticsTester { Iterable nodesWith({ String label, String value, + String hint, TextDirection textDirection, List actions, List flags, @@ -397,6 +398,8 @@ class SemanticsTester { return false; if (value != null && node.value != value) return false; + if (hint != null && node.hint != hint) + return false; if (textDirection != null && node.textDirection != textDirection) return false; if (actions != null) { @@ -636,6 +639,7 @@ class _IncludesNodeWith extends Matcher { const _IncludesNodeWith({ this.label, this.value, + this.hint, this.textDirection, this.actions, this.flags, @@ -646,6 +650,7 @@ class _IncludesNodeWith extends Matcher { final String label; final String value; + final String hint; final TextDirection textDirection; final List actions; final List flags; @@ -658,6 +663,7 @@ class _IncludesNodeWith extends Matcher { return item.nodesWith( label: label, value: value, + hint: hint, textDirection: textDirection, actions: actions, flags: flags, @@ -683,6 +689,8 @@ class _IncludesNodeWith extends Matcher { strings.add('label "$label"'); if (value != null) strings.add('value "$value"'); + if (hint != null) + strings.add('hint "$hint"'); if (textDirection != null) strings.add(' (${describeEnum(textDirection)})'); if (actions != null) @@ -706,6 +714,7 @@ class _IncludesNodeWith extends Matcher { Matcher includesNodeWith({ String label, String value, + String hint, TextDirection textDirection, List actions, List flags, @@ -716,6 +725,7 @@ Matcher includesNodeWith({ return new _IncludesNodeWith( label: label, value: value, + hint: hint, textDirection: textDirection, actions: actions, flags: flags,