Unverified Commit bee9522f authored by MH Johnson's avatar MH Johnson Committed by GitHub

[Text Scaling][Material] Update BottomNavigationBar to show tooltips on long press. (#59127)

parent 6f0cfc95
...@@ -58,15 +58,15 @@ class CupertinoNavigationDemo extends StatelessWidget { ...@@ -58,15 +58,15 @@ class CupertinoNavigationDemo extends StatelessWidget {
items: const <BottomNavigationBarItem>[ items: const <BottomNavigationBarItem>[
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(CupertinoIcons.home), icon: Icon(CupertinoIcons.home),
title: Text('Home'), label: 'Home',
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(CupertinoIcons.conversation_bubble), icon: Icon(CupertinoIcons.conversation_bubble),
title: Text('Support'), label: 'Support',
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(CupertinoIcons.profile_circled), icon: Icon(CupertinoIcons.profile_circled),
title: Text('Profile'), label: 'Profile',
), ),
], ],
), ),
......
...@@ -19,7 +19,7 @@ class NavigationIconView { ...@@ -19,7 +19,7 @@ class NavigationIconView {
item = BottomNavigationBarItem( item = BottomNavigationBarItem(
icon: icon, icon: icon,
activeIcon: activeIcon, activeIcon: activeIcon,
title: Text(title), label: title,
backgroundColor: color, backgroundColor: color,
), ),
controller = AnimationController( controller = AnimationController(
......
...@@ -258,6 +258,7 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget { ...@@ -258,6 +258,7 @@ class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
child: Center(child: active ? item.activeIcon : item.icon), child: Center(child: active ? item.activeIcon : item.icon),
), ),
if (item.title != null) item.title, if (item.title != null) item.title,
if (item.label != null) Text(item.label),
]; ];
} }
......
...@@ -19,6 +19,7 @@ import 'material.dart'; ...@@ -19,6 +19,7 @@ import 'material.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
import 'text_theme.dart'; import 'text_theme.dart';
import 'theme.dart'; import 'theme.dart';
import 'tooltip.dart';
/// Defines the layout and behavior of a [BottomNavigationBar]. /// Defines the layout and behavior of a [BottomNavigationBar].
/// ///
...@@ -112,15 +113,15 @@ enum BottomNavigationBarType { ...@@ -112,15 +113,15 @@ enum BottomNavigationBarType {
/// items: const <BottomNavigationBarItem>[ /// items: const <BottomNavigationBarItem>[
/// BottomNavigationBarItem( /// BottomNavigationBarItem(
/// icon: Icon(Icons.home), /// icon: Icon(Icons.home),
/// title: Text('Home'), /// label: 'Home',
/// ), /// ),
/// BottomNavigationBarItem( /// BottomNavigationBarItem(
/// icon: Icon(Icons.business), /// icon: Icon(Icons.business),
/// title: Text('Business'), /// label: 'Business',
/// ), /// ),
/// BottomNavigationBarItem( /// BottomNavigationBarItem(
/// icon: Icon(Icons.school), /// icon: Icon(Icons.school),
/// title: Text('School'), /// label: 'School',
/// ), /// ),
/// ], /// ],
/// currentIndex: _selectedIndex, /// currentIndex: _selectedIndex,
...@@ -194,8 +195,9 @@ class BottomNavigationBar extends StatefulWidget { ...@@ -194,8 +195,9 @@ class BottomNavigationBar extends StatefulWidget {
}) : assert(items != null), }) : assert(items != null),
assert(items.length >= 2), assert(items.length >= 2),
assert( assert(
items.every((BottomNavigationBarItem item) => item.title != null) == true, items.every((BottomNavigationBarItem item) => item.title != null) ||
'Every item must have a non-null title', items.every((BottomNavigationBarItem item) => item.label != null),
'Every item must have a non-null title or label',
), ),
assert(0 <= currentIndex && currentIndex < items.length), assert(0 <= currentIndex && currentIndex < items.length),
assert(elevation == null || elevation >= 0.0), assert(elevation == null || elevation >= 0.0),
...@@ -458,52 +460,65 @@ class _BottomNavigationTile extends StatelessWidget { ...@@ -458,52 +460,65 @@ class _BottomNavigationTile extends StatelessWidget {
break; break;
} }
return Expanded( Widget result = InkResponse(
flex: size, onTap: onTap,
child: Semantics( mouseCursor: mouseCursor,
container: true, child: Padding(
selected: selected, padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding),
child: Stack( child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
InkResponse( _TileIcon(
onTap: onTap, colorTween: colorTween,
mouseCursor: mouseCursor, animation: animation,
child: Padding( iconSize: iconSize,
padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding), selected: selected,
child: Column( item: item,
crossAxisAlignment: CrossAxisAlignment.center, selectedIconTheme: selectedIconTheme ?? bottomTheme.selectedIconTheme,
mainAxisAlignment: MainAxisAlignment.spaceBetween, unselectedIconTheme: unselectedIconTheme ?? bottomTheme.unselectedIconTheme,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_TileIcon(
colorTween: colorTween,
animation: animation,
iconSize: iconSize,
selected: selected,
item: item,
selectedIconTheme: selectedIconTheme ?? bottomTheme.selectedIconTheme,
unselectedIconTheme: unselectedIconTheme ?? bottomTheme.unselectedIconTheme,
),
_Label(
colorTween: colorTween,
animation: animation,
item: item,
selectedLabelStyle: selectedLabelStyle ?? bottomTheme.selectedLabelStyle,
unselectedLabelStyle: unselectedLabelStyle ?? bottomTheme.unselectedLabelStyle,
showSelectedLabels: showSelectedLabels ?? bottomTheme.showUnselectedLabels,
showUnselectedLabels: showUnselectedLabels ?? bottomTheme.showUnselectedLabels,
),
],
),
),
), ),
Semantics( _Label(
label: indexLabel, colorTween: colorTween,
animation: animation,
item: item,
selectedLabelStyle: selectedLabelStyle ?? bottomTheme.selectedLabelStyle,
unselectedLabelStyle: unselectedLabelStyle ?? bottomTheme.unselectedLabelStyle,
showSelectedLabels: showSelectedLabels ?? bottomTheme.showUnselectedLabels,
showUnselectedLabels: showUnselectedLabels ?? bottomTheme.showUnselectedLabels,
), ),
], ],
), ),
), ),
); );
if (item.label != null) {
result = Tooltip(
message: item.label,
preferBelow: false,
verticalOffset: selectedIconSize + selectedFontSize,
child: result,
);
}
result = Semantics(
selected: selected,
container: true,
child: Stack(
children: <Widget>[
result,
Semantics(
label: indexLabel,
),
],
),
);
return Expanded(
flex: size,
child: result,
);
} }
} }
...@@ -611,7 +626,7 @@ class _Label extends StatelessWidget { ...@@ -611,7 +626,7 @@ class _Label extends StatelessWidget {
), ),
), ),
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: item.title, child: item.title ?? Text(item.label),
), ),
); );
...@@ -638,11 +653,25 @@ class _Label extends StatelessWidget { ...@@ -638,11 +653,25 @@ class _Label extends StatelessWidget {
); );
} }
return Align( text = Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
heightFactor: 1.0, heightFactor: 1.0,
child: Container(child: text), child: Container(child: text),
); );
if (item.label != null) {
// Do not grow text in bottom navigation bar when we can show a tooltip
// instead.
final MediaQueryData mediaQueryData = MediaQuery.of(context);
text = MediaQuery(
data: mediaQueryData.copyWith(
textScaleFactor: math.min(1.0, mediaQueryData.textScaleFactor),
),
child: text,
);
}
return text;
} }
} }
...@@ -893,6 +922,7 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr ...@@ -893,6 +922,7 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> with TickerPr
assert(debugCheckHasDirectionality(context)); assert(debugCheckHasDirectionality(context));
assert(debugCheckHasMaterialLocalizations(context)); assert(debugCheckHasMaterialLocalizations(context));
assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMediaQuery(context));
assert(Overlay.of(context, debugRequiredFor: widget) != null);
final BottomNavigationBarThemeData bottomTheme = BottomNavigationBarTheme.of(context); final BottomNavigationBarThemeData bottomTheme = BottomNavigationBarTheme.of(context);
......
...@@ -26,10 +26,16 @@ class BottomNavigationBarItem { ...@@ -26,10 +26,16 @@ class BottomNavigationBarItem {
/// The argument [icon] should not be null and the argument [title] should not be null when used in a Material Design's [BottomNavigationBar]. /// The argument [icon] should not be null and the argument [title] should not be null when used in a Material Design's [BottomNavigationBar].
const BottomNavigationBarItem({ const BottomNavigationBarItem({
@required this.icon, @required this.icon,
@Deprecated(
'Use "label" instead, as it allows for an improved text-scaling experience. '
'This feature was deprecated after v1.19.0.'
)
this.title, this.title,
this.label,
Widget activeIcon, Widget activeIcon,
this.backgroundColor, this.backgroundColor,
}) : activeIcon = activeIcon ?? icon, }) : activeIcon = activeIcon ?? icon,
assert(label == null || title == null),
assert(icon != null); assert(icon != null);
/// The icon of the item. /// The icon of the item.
...@@ -63,8 +69,21 @@ class BottomNavigationBarItem { ...@@ -63,8 +69,21 @@ class BottomNavigationBarItem {
final Widget activeIcon; final Widget activeIcon;
/// The title of the item. If the title is not provided only the icon will be shown when not used in a Material Design [BottomNavigationBar]. /// The title of the item. If the title is not provided only the icon will be shown when not used in a Material Design [BottomNavigationBar].
///
/// This field is deprecated, use [label] instead.
@Deprecated(
'Use "label" instead, as it allows for an improved text-scaling experience. '
'This feature was deprecated after v1.19.0.'
)
final Widget title; final Widget title;
/// The text label for this [BottomNavigationBarItem].
///
/// This will be used to create a [Text] widget to put in the bottom navigation bar,
/// and in Material Design [BottomNavigationBar]s, this will be used to display
/// a tooltip on long press of an item in the [BottomNavigationBar].
final String label;
/// The color of the background radial animation for material [BottomNavigationBar]. /// The color of the background radial animation for material [BottomNavigationBar].
/// ///
/// If the navigation bar's type is [BottomNavigationBarType.shifting], then /// If the navigation bar's type is [BottomNavigationBarType.shifting], then
......
...@@ -195,11 +195,11 @@ class PageStorageBucket { ...@@ -195,11 +195,11 @@ class PageStorageBucket {
/// items: <BottomNavigationBarItem>[ /// items: <BottomNavigationBarItem>[
/// BottomNavigationBarItem( /// BottomNavigationBarItem(
/// icon: Icon(Icons.home), /// icon: Icon(Icons.home),
/// title: Text('page 1'), /// label: 'page 1',
/// ), /// ),
/// BottomNavigationBarItem( /// BottomNavigationBarItem(
/// icon: Icon(Icons.settings), /// icon: Icon(Icons.settings),
/// title: Text('page2'), /// label: 'page2',
/// ), /// ),
/// ], /// ],
/// ), /// ),
......
...@@ -78,6 +78,29 @@ void main() { ...@@ -78,6 +78,29 @@ void main() {
expect(actualActive.text.style.color, const Color(0xFF123456)); expect(actualActive.text.style.color, const Color(0xFF123456));
}); });
testWidgets('BottomNavigationBar.label will create a text widget', (WidgetTester tester) async {
await pumpWidgetWithBoilerplate(tester, MediaQuery(
data: const MediaQueryData(),
child: CupertinoTabBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: ImageIcon(TestImageProvider(24, 24)),
label: 'Tab 1',
),
BottomNavigationBarItem(
icon: ImageIcon(TestImageProvider(24, 24)),
label: 'Tab 2',
),
],
currentIndex: 1,
),
));
expect(find.text('Tab 1'), findsOneWidget);
expect(find.text('Tab 2'), findsOneWidget);
});
testWidgets('Active and inactive colors dark mode', (WidgetTester tester) async { testWidgets('Active and inactive colors dark mode', (WidgetTester tester) async {
const CupertinoDynamicColor dynamicActiveColor = CupertinoDynamicColor.withBrightness( const CupertinoDynamicColor dynamicActiveColor = CupertinoDynamicColor.withBrightness(
color: Color(0xFF000000), color: Color(0xFF000000),
......
...@@ -952,7 +952,6 @@ void main() { ...@@ -952,7 +952,6 @@ void main() {
expect(builderIconSize, 12.0); expect(builderIconSize, 12.0);
}); });
testWidgets('BottomNavigationBar responds to textScaleFactor', (WidgetTester tester) async { testWidgets('BottomNavigationBar responds to textScaleFactor', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
...@@ -1026,6 +1025,133 @@ void main() { ...@@ -1026,6 +1025,133 @@ void main() {
expect(box.size.height, equals(66.0)); expect(box.size.height, equals(66.0));
}); });
testWidgets('BottomNavigationBar does not grow with textScaleFactor when labels are provided', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
label: 'A',
icon: Icon(Icons.ac_unit),
),
BottomNavigationBarItem(
label: 'B',
icon: Icon(Icons.battery_alert),
),
],
),
),
),
);
final RenderBox defaultBox = tester.renderObject(find.byType(BottomNavigationBar));
expect(defaultBox.size.height, equals(kBottomNavigationBarHeight));
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.shifting,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
label: 'A',
icon: Icon(Icons.ac_unit),
),
BottomNavigationBarItem(
label: 'B',
icon: Icon(Icons.battery_alert),
),
],
),
),
),
);
final RenderBox shiftingBox = tester.renderObject(find.byType(BottomNavigationBar));
expect(shiftingBox.size.height, equals(kBottomNavigationBarHeight));
await tester.pumpWidget(
MaterialApp(
home: MediaQuery(
data: const MediaQueryData(textScaleFactor: 2.0),
child: Scaffold(
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
label: 'A',
icon: Icon(Icons.ac_unit),
),
BottomNavigationBarItem(
label: 'B',
icon: Icon(Icons.battery_alert),
),
],
),
),
),
),
);
final RenderBox box = tester.renderObject(find.byType(BottomNavigationBar));
expect(box.size.height, equals(kBottomNavigationBarHeight));
});
testWidgets('BottomNavigationBar shows tool tips with text scaling on long press when labels are provided', (WidgetTester tester) async {
const String label = 'Foo';
Widget buildApp({ double textScaleFactor }) {
return MediaQuery(
data: MediaQueryData(textScaleFactor: textScaleFactor),
child: Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
builder: (BuildContext context) {
return Scaffold(
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
label: label,
icon: Icon(Icons.ac_unit),
),
BottomNavigationBarItem(
label: 'B',
icon: Icon(Icons.battery_alert),
),
],
),
);
}
);
},
),
),
),
);
}
await tester.pumpWidget(buildApp(textScaleFactor: 1.0));
expect(find.text(label), findsOneWidget);
await tester.longPress(find.text(label));
expect(find.text(label), findsNWidgets(2));
expect(tester.getSize(find.text(label).last), equals(const Size(42.0, 14.0)));
await tester.pumpAndSettle(const Duration(seconds: 2));
await tester.pumpWidget(buildApp(textScaleFactor: 4.0));
expect(find.text(label), findsOneWidget);
await tester.longPress(find.text(label));
expect(tester.getSize(find.text(label).last), equals(const Size(168.0, 56.0)));
});
testWidgets('BottomNavigationBar limits width of tiles with long titles', (WidgetTester tester) async { testWidgets('BottomNavigationBar limits width of tiles with long titles', (WidgetTester tester) async {
final Text longTextA = Text(''.padLeft(100, 'A')); final Text longTextA = Text(''.padLeft(100, 'A'));
final Text longTextB = Text(''.padLeft(100, 'B')); final Text longTextB = Text(''.padLeft(100, 'B'));
...@@ -1639,7 +1765,7 @@ void main() { ...@@ -1639,7 +1765,7 @@ void main() {
await gesture.addPointer(location: tester.getCenter(find.text('AC'))); await gesture.addPointer(location: tester.getCenter(find.text('AC')));
addTearDown(gesture.removePointer); addTearDown(gesture.removePointer);
await tester.pump(); await tester.pumpAndSettle();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
...@@ -1666,19 +1792,21 @@ void main() { ...@@ -1666,19 +1792,21 @@ void main() {
Widget boilerplate({ Widget bottomNavigationBar, @required TextDirection textDirection }) { Widget boilerplate({ Widget bottomNavigationBar, @required TextDirection textDirection }) {
assert(textDirection != null); assert(textDirection != null);
return Localizations( return MaterialApp(
locale: const Locale('en', 'US'), home: Localizations(
delegates: const <LocalizationsDelegate<dynamic>>[ locale: const Locale('en', 'US'),
DefaultMaterialLocalizations.delegate, delegates: const <LocalizationsDelegate<dynamic>>[
DefaultWidgetsLocalizations.delegate, DefaultMaterialLocalizations.delegate,
], DefaultWidgetsLocalizations.delegate,
child: Directionality( ],
textDirection: textDirection, child: Directionality(
child: MediaQuery( textDirection: textDirection,
data: const MediaQueryData(), child: MediaQuery(
child: Material( data: const MediaQueryData(),
child: Scaffold( child: Material(
bottomNavigationBar: bottomNavigationBar, child: Scaffold(
bottomNavigationBar: bottomNavigationBar,
),
), ),
), ),
), ),
......
...@@ -1117,17 +1117,25 @@ void main() { ...@@ -1117,17 +1117,25 @@ void main() {
Scaffold( Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
body: const Placeholder(), body: const Placeholder(),
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: Navigator(
items: const <BottomNavigationBarItem>[ onGenerateRoute: (RouteSettings settings) {
BottomNavigationBarItem( return MaterialPageRoute<void>(
icon: Icon(Icons.add), builder: (BuildContext context) {
title: Text('test'), return BottomNavigationBar(
), items: const <BottomNavigationBarItem>[
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.add), icon: Icon(Icons.add),
title: Text('test'), title: Text('test'),
), ),
], BottomNavigationBarItem(
icon: Icon(Icons.add),
title: Text('test'),
),
],
);
},
);
},
), ),
), ),
); );
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment