Unverified Commit ca2a7516 authored by Darren Austin's avatar Darren Austin Committed by GitHub

Migrate NavigationBar to M3 tokens. (#98285)

parent ccaf5156
......@@ -18,6 +18,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:gen_defaults/fab_template.dart';
import 'package:gen_defaults/navigation_bar_template.dart';
import 'package:gen_defaults/typography_template.dart';
Map<String, dynamic> _readTokenFile(String fileName) {
......@@ -64,5 +65,6 @@ Future<void> main(List<String> args) async {
tokens['colorsDark'] = _readTokenFile('color_dark.json');
FABTemplate('$materialLib/floating_action_button.dart', tokens).updateFile();
NavigationBarTemplate('$materialLib/navigation_bar.dart', tokens).updateFile();
TypographyTemplate('$materialLib/typography.dart', tokens).updateFile();
}
......@@ -33,6 +33,10 @@
"bottomRight": 0.0
},
"md.sys.shape.corner.full": {
"family": "SHAPE_FAMILY_CIRCULAR"
},
"md.sys.shape.corner.large": {
"family": "SHAPE_FAMILY_ROUNDED_CORNERS",
"topLeft": 16.0,
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'template.dart';
class NavigationBarTemplate extends TokenTemplate {
const NavigationBarTemplate(String fileName, Map<String, dynamic> tokens) : super(fileName, tokens);
@override
String generate() => '''
// Generated version ${tokens["version"]}
class _TokenDefaultsM3 extends NavigationBarThemeData {
_TokenDefaultsM3(BuildContext context)
: _theme = Theme.of(context),
_colors = Theme.of(context).colorScheme,
super(
height: ${tokens["md.comp.navigation-bar.container.height"]},
elevation: ${elevation("md.comp.navigation-bar.container")},
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
);
final ThemeData _theme;
final ColorScheme _colors;
// With Material 3, the NavigationBar uses an overlay blend for the
// default color regardless of light/dark mode. This should be handled
// in the Material widget based off of elevation, but for now we will do
// it here in the defaults.
@override Color? get backgroundColor => ElevationOverlay.colorWithOverlay(_colors.${color("md.comp.navigation-bar.container")}, _colors.primary, ${elevation("md.comp.navigation-bar.container")});
@override MaterialStateProperty<IconThemeData?>? get iconTheme {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
return IconThemeData(
size: ${tokens["md.comp.navigation-bar.icon.size"]},
color: states.contains(MaterialState.selected)
? _colors.${color("md.comp.navigation-bar.active.icon")}
: _colors.${color("md.comp.navigation-bar.inactive.icon")},
);
});
}
@override Color? get indicatorColor => _colors.${color("md.comp.navigation-bar.active-indicator")};
@override ShapeBorder? get indicatorShape => ${shape("md.comp.navigation-bar.active-indicator")};
@override MaterialStateProperty<TextStyle?>? get labelTextStyle {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
final TextStyle style = _theme.textTheme.${textStyle("md.comp.navigation-bar.label-text")}!;
return style.apply(color: states.contains(MaterialState.selected)
? _colors.${color("md.comp.navigation-bar.active.label-text")}
: _colors.${color("md.comp.navigation-bar.inactive.label-text")}
);
});
}
}
''';
}
......@@ -76,17 +76,24 @@ abstract class TokenTemplate {
/// Generate a shape constant for the given component token.
///
/// Currently only supports "SHAPE_FAMILY_ROUNDED_CORNERS" which it
/// maps to a [RoundedRectangleBorder] expression.
/// Currently supports family:
/// - "SHAPE_FAMILY_ROUNDED_CORNERS" which maps to [RoundedRectangleBorder].
/// - "SHAPE_FAMILY_CIRCULAR" which maps to a [StadiumBorder].
String shape(String componentToken) {
// TODO(darrenaustin): handle more than just rounded rectangle shapes
final Map<String, dynamic> shape = tokens[tokens['$componentToken.shape']!]! as Map<String, dynamic>;
return 'const RoundedRectangleBorder(borderRadius: '
'BorderRadius.only('
'topLeft: Radius.circular(${shape['topLeft']}), '
'topRight: Radius.circular(${shape['topRight']}), '
'bottomLeft: Radius.circular(${shape['bottomLeft']}), '
'bottomRight: Radius.circular(${shape['bottomRight']})))';
switch (shape['family']) {
case 'SHAPE_FAMILY_ROUNDED_CORNERS':
return 'const RoundedRectangleBorder(borderRadius: '
'BorderRadius.only('
'topLeft: Radius.circular(${shape['topLeft']}), '
'topRight: Radius.circular(${shape['topRight']}), '
'bottomLeft: Radius.circular(${shape['bottomLeft']}), '
'bottomRight: Radius.circular(${shape['bottomRight']})))';
case 'SHAPE_FAMILY_CIRCULAR':
return 'const StadiumBorder()';
}
print('Unsupported shape family type: ${shape['family']} for $componentToken');
return '';
}
/// Generate a [TextTheme] text style name for the given component token.
......
......@@ -101,16 +101,21 @@ static final String tokenBar = 'bar';
test('Templates can get proper shapes from given data', () {
const Map<String, dynamic> tokens = <String, dynamic>{
'foo.shape': 'shape.large',
'bar.shape': 'shape.full',
'shape.large': <String, dynamic>{
'family': 'SHAPE_FAMILY_ROUNDED_CORNERS',
'topLeft': 1.0,
'topRight': 2.0,
'bottomLeft': 3.0,
'bottomRight': 4.0,
}
},
'shape.full': <String, dynamic>{
'family': 'SHAPE_FAMILY_CIRCULAR',
},
};
final TestTemplate template = TestTemplate('foobar.dart', tokens);
expect(template.shape('foo'), 'const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(1.0), topRight: Radius.circular(2.0), bottomLeft: Radius.circular(3.0), bottomRight: Radius.circular(4.0)))');
expect(template.shape('bar'), 'const StadiumBorder()');
});
}
......
......@@ -41,7 +41,9 @@ class NavigationBarThemeData with Diagnosticable {
const NavigationBarThemeData({
this.height,
this.backgroundColor,
this.elevation,
this.indicatorColor,
this.indicatorShape,
this.labelTextStyle,
this.iconTheme,
this.labelBehavior,
......@@ -53,9 +55,15 @@ class NavigationBarThemeData with Diagnosticable {
/// Overrides the default value of [NavigationBar.backgroundColor].
final Color? backgroundColor;
/// Overrides the default value of [NavigationBar.elevation].
final double? elevation;
/// Overrides the default value of [NavigationBar]'s selection indicator.
final Color? indicatorColor;
/// Overrides the default shape of the [NavigationBar]'s selection indicator.
final ShapeBorder? indicatorShape;
/// The style to merge with the default text style for
/// [NavigationDestination] labels.
///
......@@ -77,7 +85,9 @@ class NavigationBarThemeData with Diagnosticable {
NavigationBarThemeData copyWith({
double? height,
Color? backgroundColor,
double? elevation,
Color? indicatorColor,
ShapeBorder? indicatorShape,
MaterialStateProperty<TextStyle?>? labelTextStyle,
MaterialStateProperty<IconThemeData?>? iconTheme,
NavigationDestinationLabelBehavior? labelBehavior,
......@@ -85,7 +95,9 @@ class NavigationBarThemeData with Diagnosticable {
return NavigationBarThemeData(
height: height ?? this.height,
backgroundColor: backgroundColor ?? this.backgroundColor,
elevation: elevation ?? this.elevation,
indicatorColor: indicatorColor ?? this.indicatorColor,
indicatorShape: indicatorShape ?? this.indicatorShape,
labelTextStyle: labelTextStyle ?? this.labelTextStyle,
iconTheme: iconTheme ?? this.iconTheme,
labelBehavior: labelBehavior ?? this.labelBehavior,
......@@ -104,7 +116,9 @@ class NavigationBarThemeData with Diagnosticable {
return NavigationBarThemeData(
height: lerpDouble(a?.height, b?.height, t),
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
elevation: lerpDouble(a?.elevation, b?.elevation, t),
indicatorColor: Color.lerp(a?.indicatorColor, b?.indicatorColor, t),
indicatorShape: ShapeBorder.lerp(a?.indicatorShape, b?.indicatorShape, t),
labelTextStyle: _lerpProperties<TextStyle?>(a?.labelTextStyle, b?.labelTextStyle, t, TextStyle.lerp),
iconTheme: _lerpProperties<IconThemeData?>(a?.iconTheme, b?.iconTheme, t, IconThemeData.lerp),
labelBehavior: t < 0.5 ? a?.labelBehavior : b?.labelBehavior,
......@@ -116,7 +130,9 @@ class NavigationBarThemeData with Diagnosticable {
return hashValues(
height,
backgroundColor,
elevation,
indicatorColor,
indicatorShape,
labelTextStyle,
iconTheme,
labelBehavior,
......@@ -132,7 +148,9 @@ class NavigationBarThemeData with Diagnosticable {
return other is NavigationBarThemeData
&& other.height == height
&& other.backgroundColor == backgroundColor
&& other.elevation == elevation
&& other.indicatorColor == indicatorColor
&& other.indicatorShape == indicatorShape
&& other.labelTextStyle == labelTextStyle
&& other.iconTheme == iconTheme
&& other.labelBehavior == labelBehavior;
......@@ -143,7 +161,9 @@ class NavigationBarThemeData with Diagnosticable {
super.debugFillProperties(properties);
properties.add(DoubleProperty('height', height, defaultValue: null));
properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: null));
properties.add(DoubleProperty('elevation', elevation, defaultValue: null));
properties.add(ColorProperty('indicatorColor', indicatorColor, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('indicatorShape', indicatorShape, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<TextStyle?>>('labelTextStyle', labelTextStyle, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<IconThemeData?>>('iconTheme', iconTheme, defaultValue: null));
properties.add(DiagnosticsProperty<NavigationDestinationLabelBehavior>('labelBehavior', labelBehavior, defaultValue: null));
......
......@@ -1150,6 +1150,7 @@ class ThemeData with Diagnosticable {
/// Components that have been migrated to Material 3 are:
///
/// * [FloatingActionButton]
/// * [NavigationBar]
///
/// See also:
///
......
......@@ -63,6 +63,31 @@ void main() {
expect(_getMaterial(tester).color, equals(color));
});
testWidgets('NavigationBar can update elevation', (WidgetTester tester) async {
const double elevation = 42.0;
await tester.pumpWidget(
_buildWidget(
NavigationBar(
elevation: elevation,
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {},
),
),
);
expect(_getMaterial(tester).elevation, equals(elevation));
});
testWidgets('NavigationBar adds bottom padding to height', (WidgetTester tester) async {
const double bottomPadding = 40.0;
......@@ -112,6 +137,61 @@ void main() {
expect(tester.getSize(find.byType(NavigationBar)).height, expectedHeight);
});
testWidgets('NavigationBar uses proper defaults when no parameters are given', (WidgetTester tester) async {
// Pre-M3 settings that were hand coded.
await tester.pumpWidget(
_buildWidget(
NavigationBar(
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {},
),
),
);
expect(_getMaterial(tester).color, const Color(0xffeaeaea));
expect(_getMaterial(tester).elevation, 0);
expect(tester.getSize(find.byType(NavigationBar)).height, 80);
expect(_indicator(tester)?.color, const Color(0x3d2196f3));
expect(_indicator(tester)?.shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)));
// M3 settings from the token database.
await tester.pumpWidget(
_buildWidget(
Theme(
data: ThemeData.light().copyWith(useMaterial3: true),
child: NavigationBar(
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.ac_unit),
label: 'AC',
),
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
],
onDestinationSelected: (int i) {},
),
),
),
);
expect(_getMaterial(tester).color, const Color(0xffecf6fe));
expect(_getMaterial(tester).elevation, 3);
expect(tester.getSize(find.byType(NavigationBar)).height, 80);
expect(_indicator(tester)?.color, const Color(0xff2196f3));
expect(_indicator(tester)?.shape, const StadiumBorder());
});
testWidgets('NavigationBar shows tooltips with text scaling ', (WidgetTester tester) async {
const String label = 'A';
......@@ -390,3 +470,12 @@ Material _getMaterial(WidgetTester tester) {
find.descendant(of: find.byType(NavigationBar), matching: find.byType(Material)),
);
}
ShapeDecoration? _indicator(WidgetTester tester) {
return tester.firstWidget<Container>(
find.descendant(
of: find.byType(FadeTransition),
matching: find.byType(Container),
),
).decoration as ShapeDecoration?;
}
......@@ -29,7 +29,9 @@ void main() {
NavigationBarThemeData(
height: 200.0,
backgroundColor: const Color(0x00000099),
elevation: 20.0,
indicatorColor: const Color(0x00000098),
indicatorShape: const CircleBorder(),
labelTextStyle: MaterialStateProperty.all(const TextStyle(fontSize: 7.0)),
iconTheme: MaterialStateProperty.all(const IconThemeData(color: Color(0x00000097))),
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
......@@ -42,20 +44,24 @@ void main() {
expect(description[0], 'height: 200.0');
expect(description[1], 'backgroundColor: Color(0x00000099)');
expect(description[2], 'indicatorColor: Color(0x00000098)');
expect(description[3], 'labelTextStyle: MaterialStateProperty.all(TextStyle(inherit: true, size: 7.0))');
expect(description[2], 'elevation: 20.0');
expect(description[3], 'indicatorColor: Color(0x00000098)');
expect(description[4], 'indicatorShape: CircleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none))');
expect(description[5], 'labelTextStyle: MaterialStateProperty.all(TextStyle(inherit: true, size: 7.0))');
// Ignore instance address for IconThemeData.
expect(description[4].contains('iconTheme: MaterialStateProperty.all(IconThemeData'), isTrue);
expect(description[4].contains('(color: Color(0x00000097))'), isTrue);
expect(description[6].contains('iconTheme: MaterialStateProperty.all(IconThemeData'), isTrue);
expect(description[6].contains('(color: Color(0x00000097))'), isTrue);
expect(description[5], 'labelBehavior: NavigationDestinationLabelBehavior.alwaysHide');
expect(description[7], 'labelBehavior: NavigationDestinationLabelBehavior.alwaysHide');
});
testWidgets('NavigationBarThemeData values are used when no NavigationBar properties are specified', (WidgetTester tester) async {
const double height = 200.0;
const Color backgroundColor = Color(0x00000001);
const double elevation = 42.0;
const Color indicatorColor = Color(0x00000002);
const ShapeBorder indicatorShape = CircleBorder();
const double selectedIconSize = 25.0;
const double unselectedIconSize = 23.0;
const Color selectedIconColor = Color(0x00000003);
......@@ -73,7 +79,9 @@ void main() {
data: NavigationBarThemeData(
height: height,
backgroundColor: backgroundColor,
elevation: elevation,
indicatorColor: indicatorColor,
indicatorShape: indicatorShape,
iconTheme: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return const IconThemeData(
......@@ -106,7 +114,9 @@ void main() {
expect(_barHeight(tester), height);
expect(_barMaterial(tester).color, backgroundColor);
expect(_barMaterial(tester).elevation, elevation);
expect(_indicator(tester)?.color, indicatorColor);
expect(_indicator(tester)?.shape, indicatorShape);
expect(_selectedIconTheme(tester).size, selectedIconSize);
expect(_selectedIconTheme(tester).color, selectedIconColor);
expect(_selectedIconTheme(tester).opacity, selectedIconOpacity);
......@@ -121,6 +131,7 @@ void main() {
testWidgets('NavigationBar values take priority over NavigationBarThemeData values when both properties are specified', (WidgetTester tester) async {
const double height = 200.0;
const Color backgroundColor = Color(0x00000001);
const double elevation = 42.0;
const NavigationDestinationLabelBehavior labelBehavior = NavigationDestinationLabelBehavior.alwaysShow;
await tester.pumpWidget(
......@@ -129,11 +140,13 @@ void main() {
bottomNavigationBar: NavigationBarTheme(
data: const NavigationBarThemeData(
height: 100.0,
elevation: 18.0,
backgroundColor: Color(0x00000099),
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
),
child: NavigationBar(
height: height,
elevation: elevation,
backgroundColor: backgroundColor,
labelBehavior: labelBehavior,
destinations: _destinations(),
......@@ -145,6 +158,7 @@ void main() {
expect(_barHeight(tester), height);
expect(_barMaterial(tester).color, backgroundColor);
expect(_barMaterial(tester).elevation, elevation);
expect(_labelBehavior(tester), labelBehavior);
});
}
......@@ -179,13 +193,13 @@ Material _barMaterial(WidgetTester tester) {
);
}
BoxDecoration? _indicator(WidgetTester tester) {
ShapeDecoration? _indicator(WidgetTester tester) {
return tester.firstWidget<Container>(
find.descendant(
of: find.byType(FadeTransition),
matching: find.byType(Container),
),
).decoration as BoxDecoration?;
).decoration as ShapeDecoration?;
}
IconThemeData _selectedIconTheme(WidgetTester tester) {
......
......@@ -263,13 +263,13 @@ Material _railMaterial(WidgetTester tester) {
}
BoxDecoration? _indicatorDecoration(WidgetTester tester) {
ShapeDecoration? _indicatorDecoration(WidgetTester tester) {
return tester.firstWidget<Container>(
find.descendant(
of: find.byType(NavigationIndicator),
matching: find.byType(Container),
),
).decoration as BoxDecoration?;
).decoration as ShapeDecoration?;
}
IconThemeData _selectedIconTheme(WidgetTester tester) {
......
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