Semantics for CupertinoTabBar (#19924)
This commit is contained in:
parent
5b30b393a8
commit
cb03ca1d9a
@ -156,25 +156,32 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
final List<Widget> result = <Widget>[];
|
final List<Widget> result = <Widget>[];
|
||||||
|
|
||||||
for (int index = 0; index < items.length; index += 1) {
|
for (int index = 0; index < items.length; index += 1) {
|
||||||
|
final bool active = index == currentIndex;
|
||||||
result.add(
|
result.add(
|
||||||
_wrapActiveItem(
|
_wrapActiveItem(
|
||||||
new Expanded(
|
new Expanded(
|
||||||
child: new GestureDetector(
|
child: new Semantics(
|
||||||
behavior: HitTestBehavior.opaque,
|
selected: active,
|
||||||
onTap: onTap == null ? null : () { onTap(index); },
|
// TODO(https://github.com/flutter/flutter/issues/13452):
|
||||||
child: new Padding(
|
// This needs localization support.
|
||||||
padding: const EdgeInsets.only(bottom: 4.0),
|
hint: 'tab, ${index + 1} of ${items.length}',
|
||||||
child: new Column(
|
child: new GestureDetector(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
behavior: HitTestBehavior.opaque,
|
||||||
children: <Widget> [
|
onTap: onTap == null ? null : () { onTap(index); },
|
||||||
new Expanded(child: new Center(child: items[index].icon)),
|
child: new Padding(
|
||||||
items[index].title,
|
padding: const EdgeInsets.only(bottom: 4.0),
|
||||||
],
|
child: new Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: <Widget> [
|
||||||
|
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.
|
/// 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)
|
if (!active)
|
||||||
return item;
|
return item;
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import 'package:flutter/cupertino.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import '../painting/mocks_for_image_cache.dart';
|
import '../painting/mocks_for_image_cache.dart';
|
||||||
|
import '../widgets/semantics_tester.dart';
|
||||||
|
|
||||||
Future<Null> pumpWidgetWithBoilerplate(WidgetTester tester, Widget widget) async {
|
Future<Null> pumpWidgetWithBoilerplate(WidgetTester tester, Widget widget) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@ -169,4 +170,39 @@ void main() {
|
|||||||
await tester.tap(find.text('Tab 1'));
|
await tester.tap(find.text('Tab 1'));
|
||||||
expect(callbackTab, 0);
|
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 <BottomNavigationBarItem>[
|
||||||
|
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>[SemanticsFlag.isSelected],
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(semantics, includesNodeWith(
|
||||||
|
label: 'Tab 2',
|
||||||
|
hint: 'tab, 2 of 2',
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
));
|
||||||
|
|
||||||
|
semantics.dispose();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -384,6 +384,7 @@ class SemanticsTester {
|
|||||||
Iterable<SemanticsNode> nodesWith({
|
Iterable<SemanticsNode> nodesWith({
|
||||||
String label,
|
String label,
|
||||||
String value,
|
String value,
|
||||||
|
String hint,
|
||||||
TextDirection textDirection,
|
TextDirection textDirection,
|
||||||
List<SemanticsAction> actions,
|
List<SemanticsAction> actions,
|
||||||
List<SemanticsFlag> flags,
|
List<SemanticsFlag> flags,
|
||||||
@ -397,6 +398,8 @@ class SemanticsTester {
|
|||||||
return false;
|
return false;
|
||||||
if (value != null && node.value != value)
|
if (value != null && node.value != value)
|
||||||
return false;
|
return false;
|
||||||
|
if (hint != null && node.hint != hint)
|
||||||
|
return false;
|
||||||
if (textDirection != null && node.textDirection != textDirection)
|
if (textDirection != null && node.textDirection != textDirection)
|
||||||
return false;
|
return false;
|
||||||
if (actions != null) {
|
if (actions != null) {
|
||||||
@ -636,6 +639,7 @@ class _IncludesNodeWith extends Matcher {
|
|||||||
const _IncludesNodeWith({
|
const _IncludesNodeWith({
|
||||||
this.label,
|
this.label,
|
||||||
this.value,
|
this.value,
|
||||||
|
this.hint,
|
||||||
this.textDirection,
|
this.textDirection,
|
||||||
this.actions,
|
this.actions,
|
||||||
this.flags,
|
this.flags,
|
||||||
@ -646,6 +650,7 @@ class _IncludesNodeWith extends Matcher {
|
|||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
final String value;
|
final String value;
|
||||||
|
final String hint;
|
||||||
final TextDirection textDirection;
|
final TextDirection textDirection;
|
||||||
final List<SemanticsAction> actions;
|
final List<SemanticsAction> actions;
|
||||||
final List<SemanticsFlag> flags;
|
final List<SemanticsFlag> flags;
|
||||||
@ -658,6 +663,7 @@ class _IncludesNodeWith extends Matcher {
|
|||||||
return item.nodesWith(
|
return item.nodesWith(
|
||||||
label: label,
|
label: label,
|
||||||
value: value,
|
value: value,
|
||||||
|
hint: hint,
|
||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
actions: actions,
|
actions: actions,
|
||||||
flags: flags,
|
flags: flags,
|
||||||
@ -683,6 +689,8 @@ class _IncludesNodeWith extends Matcher {
|
|||||||
strings.add('label "$label"');
|
strings.add('label "$label"');
|
||||||
if (value != null)
|
if (value != null)
|
||||||
strings.add('value "$value"');
|
strings.add('value "$value"');
|
||||||
|
if (hint != null)
|
||||||
|
strings.add('hint "$hint"');
|
||||||
if (textDirection != null)
|
if (textDirection != null)
|
||||||
strings.add(' (${describeEnum(textDirection)})');
|
strings.add(' (${describeEnum(textDirection)})');
|
||||||
if (actions != null)
|
if (actions != null)
|
||||||
@ -706,6 +714,7 @@ class _IncludesNodeWith extends Matcher {
|
|||||||
Matcher includesNodeWith({
|
Matcher includesNodeWith({
|
||||||
String label,
|
String label,
|
||||||
String value,
|
String value,
|
||||||
|
String hint,
|
||||||
TextDirection textDirection,
|
TextDirection textDirection,
|
||||||
List<SemanticsAction> actions,
|
List<SemanticsAction> actions,
|
||||||
List<SemanticsFlag> flags,
|
List<SemanticsFlag> flags,
|
||||||
@ -716,6 +725,7 @@ Matcher includesNodeWith({
|
|||||||
return new _IncludesNodeWith(
|
return new _IncludesNodeWith(
|
||||||
label: label,
|
label: label,
|
||||||
value: value,
|
value: value,
|
||||||
|
hint: hint,
|
||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
actions: actions,
|
actions: actions,
|
||||||
flags: flags,
|
flags: flags,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user