Unverified Commit cb988c7b authored by Taha Tesser's avatar Taha Tesser Committed by GitHub

Add `indicatorColor` & `indicatorShape` to `NavigationRail`,...

Add `indicatorColor` & `indicatorShape` to `NavigationRail`, `NavigationDrawer` and move these properties from destination to `NavigationBar` (#117049)
parent 32da2505
......@@ -93,6 +93,8 @@ class NavigationBar extends StatelessWidget {
this.elevation,
this.shadowColor,
this.surfaceTintColor,
this.indicatorColor,
this.indicatorShape,
this.height,
this.labelBehavior,
}) : assert(destinations != null && destinations.length >= 2),
......@@ -158,6 +160,20 @@ class NavigationBar extends StatelessWidget {
/// overlay is applied.
final Color? surfaceTintColor;
/// The color of the [indicatorShape] when this destination is selected.
///
/// If null, [NavigationBarThemeData.indicatorColor] is used. If that
/// is also null and [ThemeData.useMaterial3] is true, [ColorScheme.secondaryContainer]
/// is used. Otherwise, [ColorScheme.secondary] with an opacity of 0.24 is used.
final Color? indicatorColor;
/// The shape of the selected inidicator.
///
/// If null, [NavigationBarThemeData.indicatorShape] is used. If that
/// is also null and [ThemeData.useMaterial3] is true, [StadiumBorder] is used.
/// Otherwise, [RoundedRectangleBorder] with a circular border radius of 16 is used.
final ShapeBorder? indicatorShape;
/// The height of the [NavigationBar] itself.
///
/// If this is used in [Scaffold.bottomNavigationBar] and the scaffold is
......@@ -224,6 +240,8 @@ class NavigationBar extends StatelessWidget {
totalNumberOfDestinations: destinations.length,
selectedAnimation: animation,
labelBehavior: effectiveLabelBehavior,
indicatorColor: indicatorColor,
indicatorShape: indicatorShape,
onTap: _handleTap(i),
child: destinations[i],
);
......@@ -274,8 +292,6 @@ class NavigationDestination extends StatelessWidget {
super.key,
required this.icon,
this.selectedIcon,
this.indicatorColor,
this.indicatorShape,
required this.label,
this.tooltip,
});
......@@ -300,12 +316,6 @@ class NavigationDestination extends StatelessWidget {
/// would use a size of 24.0 and [ColorScheme.onSurface].
final Widget? selectedIcon;
/// The color of the [indicatorShape] when this destination is selected.
final Color? indicatorColor;
/// The shape of the selected inidicator.
final ShapeBorder? indicatorShape;
/// The text label that appears below the icon of this
/// [NavigationDestination].
///
......@@ -324,12 +334,13 @@ class NavigationDestination extends StatelessWidget {
@override
Widget build(BuildContext context) {
final _NavigationDestinationInfo info = _NavigationDestinationInfo.of(context);
const Set<MaterialState> selectedState = <MaterialState>{MaterialState.selected};
const Set<MaterialState> unselectedState = <MaterialState>{};
final NavigationBarThemeData navigationBarTheme = NavigationBarTheme.of(context);
final NavigationBarThemeData defaults = _defaultsFor(context);
final Animation<double> animation = _NavigationDestinationInfo.of(context).selectedAnimation;
final Animation<double> animation = info.selectedAnimation;
return _NavigationDestinationBuilder(
label: label,
......@@ -351,8 +362,8 @@ class NavigationDestination extends StatelessWidget {
children: <Widget>[
NavigationIndicator(
animation: animation,
color: indicatorColor ?? navigationBarTheme.indicatorColor ?? defaults.indicatorColor!,
shape: indicatorShape ?? navigationBarTheme.indicatorShape ?? defaults.indicatorShape!
color: info.indicatorColor ?? navigationBarTheme.indicatorColor ?? defaults.indicatorColor!,
shape: info.indicatorShape ?? navigationBarTheme.indicatorShape ?? defaults.indicatorShape!
),
_StatusTransitionWidgetBuilder(
animation: animation,
......@@ -532,6 +543,8 @@ class _NavigationDestinationInfo extends InheritedWidget {
required this.totalNumberOfDestinations,
required this.selectedAnimation,
required this.labelBehavior,
required this.indicatorColor,
required this.indicatorShape,
required this.onTap,
required super.child,
});
......@@ -588,6 +601,16 @@ class _NavigationDestinationInfo extends InheritedWidget {
/// label, or hide all labels.
final NavigationDestinationLabelBehavior labelBehavior;
/// The color of the selection indicator.
///
/// This is used by destinations to override the indicator color.
final Color? indicatorColor;
/// The shape of the selection indicator.
///
/// This is used by destinations to override the indicator shape.
final ShapeBorder? indicatorShape;
/// The callback that should be called when this destination is tapped.
///
/// This is computed by calling [NavigationBar.onDestinationSelected]
......
......@@ -57,6 +57,8 @@ class NavigationDrawer extends StatelessWidget {
this.shadowColor,
this.surfaceTintColor,
this.elevation,
this.indicatorColor,
this.indicatorShape,
this.onDestinationSelected,
this.selectedIndex = 0,
});
......@@ -90,6 +92,18 @@ class NavigationDrawer extends StatelessWidget {
/// is also null, it will be 1.0.
final double? elevation;
/// The color of the [indicatorShape] when this destination is selected.
///
/// If this is null, [NavigationDrawerThemeData.indicatorColor] is used.
/// If that is also null, defaults to [ColorScheme.secondaryContainer].
final Color? indicatorColor;
/// The shape of the selected inidicator.
///
/// If this is null, [NavigationDrawerThemeData.indicatorShape] is used.
/// If that is also null, defaults to [StadiumBorder].
final ShapeBorder? indicatorShape;
/// Defines the appearance of the items within the navigation drawer.
///
/// The list contains [NavigationDrawerDestination] widgets and/or customized
......@@ -125,6 +139,8 @@ class NavigationDrawer extends StatelessWidget {
index: index,
totalNumberOfDestinations: totalNumberOfDestinations,
selectedAnimation: animation,
indicatorColor: indicatorColor,
indicatorShape: indicatorShape,
onTap: () {
if (onDestinationSelected != null) {
onDestinationSelected!(index);
......@@ -315,9 +331,9 @@ class _NavigationDestinationBuilder extends StatelessWidget {
alignment: Alignment.center,
children: <Widget>[
NavigationIndicator(
animation: _NavigationDrawerDestinationInfo.of(context).selectedAnimation,
color: navigationDrawerTheme.indicatorColor ?? defaults.indicatorColor!,
shape: navigationDrawerTheme.indicatorShape ?? defaults.indicatorShape!,
animation: info.selectedAnimation,
color: info.indicatorColor ?? navigationDrawerTheme.indicatorColor ?? defaults.indicatorColor!,
shape: info.indicatorShape ?? navigationDrawerTheme.indicatorShape ?? defaults.indicatorShape!,
width: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).width,
height: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).height,
),
......@@ -433,6 +449,8 @@ class _NavigationDrawerDestinationInfo extends InheritedWidget {
required this.index,
required this.totalNumberOfDestinations,
required this.selectedAnimation,
required this.indicatorColor,
required this.indicatorShape,
required this.onTap,
required super.child,
});
......@@ -478,6 +496,16 @@ class _NavigationDrawerDestinationInfo extends InheritedWidget {
/// to 1 (selected).
final Animation<double> selectedAnimation;
/// The color of the indicator.
///
/// This is used by destinations to override the indicator color.
final Color? indicatorColor;
/// The shape of the indicator.
///
/// This is used by destinations to override the indicator shape.
final ShapeBorder? indicatorShape;
/// The callback that should be called when this destination is tapped.
///
/// This is computed by calling [NavigationDrawer.onDestinationSelected]
......
......@@ -110,6 +110,7 @@ class NavigationRail extends StatefulWidget {
this.minExtendedWidth,
this.useIndicator,
this.indicatorColor,
this.indicatorShape,
}) : assert(destinations != null && destinations.length >= 2),
assert(selectedIndex == null || (0 <= selectedIndex && selectedIndex < destinations.length)),
assert(elevation == null || elevation > 0),
......@@ -306,8 +307,18 @@ class NavigationRail extends StatefulWidget {
/// Overrides the default value of [NavigationRail]'s selection indicator color,
/// when [useIndicator] is true.
///
/// If this is null, [NavigationRailThemeData.indicatorColor] is used. If
/// that is null, defaults to [ColorScheme.secondaryContainer].
final Color? indicatorColor;
/// Overrides the default value of [NavigationRail]'s selection indicator shape,
/// when [useIndicator] is true.
///
/// If this is null, [NavigationRailThemeData.indicatorShape] is used. If
/// that is null, defaults to [StadiumBorder].
final ShapeBorder? indicatorShape;
/// Returns the animation that controls the [NavigationRail.extended] state.
///
/// This can be used to synchronize animations in the [leading] or [trailing]
......@@ -396,7 +407,7 @@ class _NavigationRailState extends State<NavigationRail> with TickerProviderStat
final NavigationRailLabelType labelType = widget.labelType ?? navigationRailTheme.labelType ?? defaults.labelType!;
final bool useIndicator = widget.useIndicator ?? navigationRailTheme.useIndicator ?? defaults.useIndicator!;
final Color? indicatorColor = widget.indicatorColor ?? navigationRailTheme.indicatorColor ?? defaults.indicatorColor;
final ShapeBorder? indicatorShape = navigationRailTheme.indicatorShape ?? defaults.indicatorShape;
final ShapeBorder? indicatorShape = widget.indicatorShape ?? navigationRailTheme.indicatorShape ?? defaults.indicatorShape;
// For backwards compatibility, in M2 the opacity of the unselected icons needs
// to be set to the default if it isn't in the given theme. This can be removed
......@@ -900,6 +911,8 @@ class NavigationRailDestination {
const NavigationRailDestination({
required this.icon,
Widget? selectedIcon,
this.indicatorColor,
this.indicatorShape,
required this.label,
this.padding,
}) : selectedIcon = selectedIcon ?? icon,
......@@ -933,6 +946,12 @@ class NavigationRailDestination {
/// icons.
final Widget selectedIcon;
/// The color of the [indicatorShape] when this destination is selected.
final Color? indicatorColor;
/// The shape of the selection inidicator.
final ShapeBorder? indicatorShape;
/// The label for the destination.
///
/// The label must be provided when used with the [NavigationRail]. When the
......
......@@ -263,8 +263,8 @@ void main() {
expect(_getMaterial(tester).surfaceTintColor, null);
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)));
expect(_getIndicatorDecoration(tester)?.color, const Color(0x3d2196f3));
expect(_getIndicatorDecoration(tester)?.shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)));
// M3 settings from the token database.
await tester.pumpWidget(
......@@ -292,8 +292,8 @@ void main() {
expect(_getMaterial(tester).surfaceTintColor, ThemeData().colorScheme.surfaceTint);
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());
expect(_getIndicatorDecoration(tester)?.color, const Color(0xff2196f3));
expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder());
});
testWidgets('NavigationBar shows tooltips with text scaling ', (WidgetTester tester) async {
......@@ -807,21 +807,21 @@ void main() {
testWidgets('Navigation destination updates indicator color and shape', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
const Color color = Color(0xff0000ff);
const ShapeBorder shape = CircleBorder();
const ShapeBorder shape = RoundedRectangleBorder();
Widget buildNaviagationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) {
Widget buildNavigationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) {
return MaterialApp(
theme: theme,
home: Scaffold(
bottomNavigationBar: NavigationBar(
destinations: <Widget>[
indicatorColor: indicatorColor,
indicatorShape: indicatorShape,
destinations: const <Widget>[
NavigationDestination(
icon: const Icon(Icons.ac_unit),
icon: Icon(Icons.ac_unit),
label: 'AC',
indicatorColor: indicatorColor,
indicatorShape: indicatorShape,
),
const NavigationDestination(
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
......@@ -832,17 +832,17 @@ void main() {
);
}
await tester.pumpWidget(buildNaviagationBar());
await tester.pumpWidget(buildNavigationBar());
// Test default indicator color and shape.
expect(_indicator(tester)?.color, theme.colorScheme.secondaryContainer);
expect(_indicator(tester)?.shape, const StadiumBorder());
expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer);
expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder());
await tester.pumpWidget(buildNaviagationBar(indicatorColor: color, indicatorShape: shape));
await tester.pumpWidget(buildNavigationBar(indicatorColor: color, indicatorShape: shape));
// Test custom indicator color and shape.
expect(_indicator(tester)?.color, color);
expect(_indicator(tester)?.shape, shape);
expect(_getIndicatorDecoration(tester)?.color, color);
expect(_getIndicatorDecoration(tester)?.shape, shape);
});
group('Material 2', () {
......@@ -852,21 +852,21 @@ void main() {
testWidgets('Navigation destination updates indicator color and shape', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: false);
const Color color = Color(0xff0000ff);
const ShapeBorder shape = CircleBorder();
const ShapeBorder shape = RoundedRectangleBorder();
Widget buildNaviagationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) {
Widget buildNavigationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) {
return MaterialApp(
theme: theme,
home: Scaffold(
bottomNavigationBar: NavigationBar(
destinations: <Widget>[
indicatorColor: indicatorColor,
indicatorShape: indicatorShape,
destinations: const <Widget>[
NavigationDestination(
icon: const Icon(Icons.ac_unit),
icon: Icon(Icons.ac_unit),
label: 'AC',
indicatorColor: indicatorColor,
indicatorShape: indicatorShape,
),
const NavigationDestination(
NavigationDestination(
icon: Icon(Icons.access_alarm),
label: 'Alarm',
),
......@@ -877,20 +877,20 @@ void main() {
);
}
await tester.pumpWidget(buildNaviagationBar());
await tester.pumpWidget(buildNavigationBar());
// Test default indicator color and shape.
expect(_indicator(tester)?.color, theme.colorScheme.secondary.withOpacity(0.24));
expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondary.withOpacity(0.24));
expect(
_indicator(tester)?.shape,
_getIndicatorDecoration(tester)?.shape,
const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
);
await tester.pumpWidget(buildNaviagationBar(indicatorColor: color, indicatorShape: shape));
await tester.pumpWidget(buildNavigationBar(indicatorColor: color, indicatorShape: shape));
// Test custom indicator color and shape.
expect(_indicator(tester)?.color, color);
expect(_indicator(tester)?.shape, shape);
expect(_getIndicatorDecoration(tester)?.color, color);
expect(_getIndicatorDecoration(tester)?.shape, shape);
});
});
}
......@@ -912,7 +912,7 @@ Material _getMaterial(WidgetTester tester) {
);
}
ShapeDecoration? _indicator(WidgetTester tester) {
ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) {
return tester.firstWidget<Container>(
find.descendant(
of: find.byType(FadeTransition),
......
......@@ -23,7 +23,7 @@ void main() {
),
NavigationDrawerDestination(
icon: Icon(Icons.access_alarm, color: theme.iconTheme.color),
label: Text('Alarm',style: theme.textTheme.bodySmall),
label: Text('Alarm', style: theme.textTheme.bodySmall),
),
],
onDestinationSelected: (int i) {
......@@ -68,7 +68,7 @@ void main() {
),
NavigationDrawerDestination(
icon: Icon(Icons.access_alarm, color: theme.iconTheme.color),
label: Text('Alarm',style: theme.textTheme.bodySmall),
label: Text('Alarm', style: theme.textTheme.bodySmall),
),
],
onDestinationSelected: (int i) {},
......@@ -97,7 +97,7 @@ void main() {
),
NavigationDrawerDestination(
icon: Icon(Icons.access_alarm, color: theme.iconTheme.color),
label: Text('Alarm',style: theme.textTheme.bodySmall),
label: Text('Alarm', style: theme.textTheme.bodySmall),
),
],
);
......@@ -134,7 +134,7 @@ void main() {
),
NavigationDrawerDestination(
icon: Icon(Icons.access_alarm, color: theme.iconTheme.color),
label: Text('Alarm',style: theme.textTheme.bodySmall),
label: Text('Alarm', style: theme.textTheme.bodySmall),
),
],
onDestinationSelected: (int i) {},
......@@ -149,8 +149,8 @@ void main() {
expect(_getMaterial(tester).surfaceTintColor,
ThemeData().colorScheme.surfaceTint);
expect(_getMaterial(tester).elevation, 1);
expect(_indicator(tester)?.color, const Color(0xff2196f3));
expect(_indicator(tester)?.shape, const StadiumBorder());
expect(_getIndicatorDecoration(tester)?.color, const Color(0xff2196f3));
expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder());
});
testWidgets('Navigation drawer semantics', (WidgetTester tester) async {
......@@ -169,7 +169,7 @@ void main() {
),
NavigationDrawerDestination(
icon: Icon(Icons.access_alarm, color: theme.iconTheme.color),
label: Text('Alarm',style: theme.textTheme.bodySmall),
label: Text('Alarm', style: theme.textTheme.bodySmall),
),
],
),
......@@ -222,6 +222,53 @@ void main() {
),
);
});
testWidgets('Navigation destination updates indicator color and shape', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final ThemeData theme = ThemeData(useMaterial3: true);
const Color color = Color(0xff0000ff);
const ShapeBorder shape = RoundedRectangleBorder();
Widget buildNavigationDrawer({Color? indicatorColor, ShapeBorder? indicatorShape}) {
return MaterialApp(
theme: theme,
home: Scaffold(
key: scaffoldKey,
drawer: NavigationDrawer(
indicatorColor: indicatorColor,
indicatorShape: indicatorShape,
children: <Widget>[
Text('Headline', style: theme.textTheme.bodyLarge),
const NavigationDrawerDestination(
icon: Icon(Icons.ac_unit),
label: Text('AC'),
),
const NavigationDrawerDestination(
icon: Icon(Icons.access_alarm),
label: Text('Alarm'),
),
],
onDestinationSelected: (int i) { },
),
body: Container(),
),
);
}
await tester.pumpWidget(buildNavigationDrawer());
scaffoldKey.currentState!.openDrawer();
await tester.pumpAndSettle();
// Test default indicator color and shape.
expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer);
expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder());
await tester.pumpWidget(buildNavigationDrawer(indicatorColor: color, indicatorShape: shape));
// Test custom indicator color and shape.
expect(_getIndicatorDecoration(tester)?.color, color);
expect(_getIndicatorDecoration(tester)?.shape, shape);
});
}
Widget _buildWidget(GlobalKey<ScaffoldState> scaffoldKey, Widget child) {
......@@ -242,7 +289,7 @@ Material _getMaterial(WidgetTester tester) {
);
}
ShapeDecoration? _indicator(WidgetTester tester) {
ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) {
return tester
.firstWidget<Container>(
find.descendant(
......
......@@ -2846,6 +2846,50 @@ void main() {
expect(transform.getColumn(0)[0], 1.0);
});
testWidgets('Navigation destination updates indicator color and shape', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
const Color color = Color(0xff0000ff);
const ShapeBorder shape = RoundedRectangleBorder();
Widget buildNavigationRail({Color? indicatorColor, ShapeBorder? indicatorShape}) {
return MaterialApp(
theme: theme,
home: Builder(
builder: (BuildContext context) {
return Scaffold(
body: Row(
children: <Widget>[
NavigationRail(
useIndicator: true,
indicatorColor: indicatorColor,
indicatorShape: indicatorShape,
selectedIndex: 0,
destinations: _destinations(),
),
const Expanded(
child: Text('body'),
),
],
),
);
},
),
);
}
await tester.pumpWidget(buildNavigationRail());
// Test default indicator color and shape.
expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer);
expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder());
await tester.pumpWidget(buildNavigationRail(indicatorColor: color, indicatorShape: shape));
// Test custom indicator color and shape.
expect(_getIndicatorDecoration(tester)?.color, color);
expect(_getIndicatorDecoration(tester)?.shape, shape);
});
group('Material 2', () {
// Original Material 2 tests. Remove this group after `useMaterial3` has been deprecated.
testWidgets('Renders at the correct default width - [labelType]=none (default)', (WidgetTester tester) async {
......@@ -4655,7 +4699,6 @@ void main() {
final double updatedWidthRTL = tester.getSize(find.byType(NavigationRail)).width;
expect(updatedWidthRTL, defaultWidth + safeAreaPadding);
});
}); // End Material 2 group
}
......@@ -4878,3 +4921,12 @@ Widget _buildWidget(Widget child, {bool useMaterial3 = true, bool isRTL = false}
),
);
}
ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) {
return tester.firstWidget<Container>(
find.descendant(
of: find.byType(FadeTransition),
matching: find.byType(Container),
),
).decoration as ShapeDecoration?;
}
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