Unverified Commit 801a1c93 authored by Matheus Kirchesch's avatar Matheus Kirchesch Committed by GitHub

Added option to disable [NavigationDrawerDestination]s (#132349)

This PR adds a new option in the NavigationDrawerDestination api allowing it to be disabled, this is very useful for role based access control, especially in the navigation drawer which is used to lay out all the app destinations

* https://github.com/flutter/flutter/issues/132348
parent 12d761a8
...@@ -193,6 +193,7 @@ class NavigationDrawerDestination extends StatelessWidget { ...@@ -193,6 +193,7 @@ class NavigationDrawerDestination extends StatelessWidget {
required this.icon, required this.icon,
this.selectedIcon, this.selectedIcon,
required this.label, required this.label,
this.enabled = true,
}); });
/// Sets the color of the [Material] that holds all of the [Drawer]'s /// Sets the color of the [Material] that holds all of the [Drawer]'s
...@@ -229,12 +230,20 @@ class NavigationDrawerDestination extends StatelessWidget { ...@@ -229,12 +230,20 @@ class NavigationDrawerDestination extends StatelessWidget {
/// text style would use [TextTheme.labelLarge] with [ColorScheme.onSurfaceVariant]. /// text style would use [TextTheme.labelLarge] with [ColorScheme.onSurfaceVariant].
final Widget label; final Widget label;
/// Indicates that this destination is selectable.
///
/// Defaults to true.
final bool enabled;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const Set<MaterialState> selectedState = <MaterialState>{ const Set<MaterialState> selectedState = <MaterialState>{
MaterialState.selected MaterialState.selected
}; };
const Set<MaterialState> unselectedState = <MaterialState>{}; const Set<MaterialState> unselectedState = <MaterialState>{};
const Set<MaterialState> disabledState = <MaterialState>{
MaterialState.disabled
};
final NavigationDrawerThemeData navigationDrawerTheme = final NavigationDrawerThemeData navigationDrawerTheme =
NavigationDrawerTheme.of(context); NavigationDrawerTheme.of(context);
...@@ -247,13 +256,13 @@ class NavigationDrawerDestination extends StatelessWidget { ...@@ -247,13 +256,13 @@ class NavigationDrawerDestination extends StatelessWidget {
return _NavigationDestinationBuilder( return _NavigationDestinationBuilder(
buildIcon: (BuildContext context) { buildIcon: (BuildContext context) {
final Widget selectedIconWidget = IconTheme.merge( final Widget selectedIconWidget = IconTheme.merge(
data: navigationDrawerTheme.iconTheme?.resolve(selectedState) ?? data: navigationDrawerTheme.iconTheme?.resolve(enabled ? selectedState : disabledState) ??
defaults.iconTheme!.resolve(selectedState)!, defaults.iconTheme!.resolve(enabled ? selectedState : disabledState)!,
child: selectedIcon ?? icon, child: selectedIcon ?? icon,
); );
final Widget unselectedIconWidget = IconTheme.merge( final Widget unselectedIconWidget = IconTheme.merge(
data: navigationDrawerTheme.iconTheme?.resolve(unselectedState) ?? data: navigationDrawerTheme.iconTheme?.resolve(enabled ? unselectedState : disabledState) ??
defaults.iconTheme!.resolve(unselectedState)!, defaults.iconTheme!.resolve(enabled ? unselectedState : disabledState)!,
child: icon, child: icon,
); );
...@@ -263,11 +272,12 @@ class NavigationDrawerDestination extends StatelessWidget { ...@@ -263,11 +272,12 @@ class NavigationDrawerDestination extends StatelessWidget {
}, },
buildLabel: (BuildContext context) { buildLabel: (BuildContext context) {
final TextStyle? effectiveSelectedLabelTextStyle = final TextStyle? effectiveSelectedLabelTextStyle =
navigationDrawerTheme.labelTextStyle?.resolve(selectedState) ?? navigationDrawerTheme.labelTextStyle?.resolve(enabled ? selectedState : disabledState) ??
defaults.labelTextStyle!.resolve(selectedState); defaults.labelTextStyle!.resolve(enabled ? selectedState : disabledState);
final TextStyle? effectiveUnselectedLabelTextStyle = final TextStyle? effectiveUnselectedLabelTextStyle =
navigationDrawerTheme.labelTextStyle?.resolve(unselectedState) ?? navigationDrawerTheme.labelTextStyle?.resolve(enabled ? unselectedState : disabledState) ??
defaults.labelTextStyle!.resolve(unselectedState); defaults.labelTextStyle!.resolve(enabled ? unselectedState : disabledState);
return DefaultTextStyle( return DefaultTextStyle(
style: _isForwardOrCompleted(animation) style: _isForwardOrCompleted(animation)
? effectiveSelectedLabelTextStyle! ? effectiveSelectedLabelTextStyle!
...@@ -275,6 +285,7 @@ class NavigationDrawerDestination extends StatelessWidget { ...@@ -275,6 +285,7 @@ class NavigationDrawerDestination extends StatelessWidget {
child: label, child: label,
); );
}, },
enabled: enabled,
); );
} }
} }
...@@ -296,6 +307,7 @@ class _NavigationDestinationBuilder extends StatelessWidget { ...@@ -296,6 +307,7 @@ class _NavigationDestinationBuilder extends StatelessWidget {
const _NavigationDestinationBuilder({ const _NavigationDestinationBuilder({
required this.buildIcon, required this.buildIcon,
required this.buildLabel, required this.buildLabel,
this.enabled = true,
}); });
/// Builds the icon for a destination in a [NavigationDrawer]. /// Builds the icon for a destination in a [NavigationDrawer].
...@@ -322,12 +334,26 @@ class _NavigationDestinationBuilder extends StatelessWidget { ...@@ -322,12 +334,26 @@ class _NavigationDestinationBuilder extends StatelessWidget {
/// animation is decreasing or dismissed. /// animation is decreasing or dismissed.
final WidgetBuilder buildLabel; final WidgetBuilder buildLabel;
/// Indicates that this destination is selectable.
///
/// Defaults to true.
final bool enabled;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final _NavigationDrawerDestinationInfo info = _NavigationDrawerDestinationInfo.of(context); final _NavigationDrawerDestinationInfo info = _NavigationDrawerDestinationInfo.of(context);
final NavigationDrawerThemeData navigationDrawerTheme = NavigationDrawerTheme.of(context); final NavigationDrawerThemeData navigationDrawerTheme = NavigationDrawerTheme.of(context);
final NavigationDrawerThemeData defaults = _NavigationDrawerDefaultsM3(context); final NavigationDrawerThemeData defaults = _NavigationDrawerDefaultsM3(context);
final Row destinationBody = Row(
children: <Widget>[
const SizedBox(width: 16),
buildIcon(context),
const SizedBox(width: 12),
buildLabel(context),
],
);
return Padding( return Padding(
padding: info.tilePadding, padding: info.tilePadding,
child: _NavigationDestinationSemantics( child: _NavigationDestinationSemantics(
...@@ -335,7 +361,7 @@ class _NavigationDestinationBuilder extends StatelessWidget { ...@@ -335,7 +361,7 @@ class _NavigationDestinationBuilder extends StatelessWidget {
height: navigationDrawerTheme.tileHeight ?? defaults.tileHeight, height: navigationDrawerTheme.tileHeight ?? defaults.tileHeight,
child: InkWell( child: InkWell(
highlightColor: Colors.transparent, highlightColor: Colors.transparent,
onTap: info.onTap, onTap: enabled ? info.onTap : null,
customBorder: info.indicatorShape ?? navigationDrawerTheme.indicatorShape ?? defaults.indicatorShape!, customBorder: info.indicatorShape ?? navigationDrawerTheme.indicatorShape ?? defaults.indicatorShape!,
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
...@@ -347,14 +373,7 @@ class _NavigationDestinationBuilder extends StatelessWidget { ...@@ -347,14 +373,7 @@ class _NavigationDestinationBuilder extends StatelessWidget {
width: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).width, width: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).width,
height: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).height, height: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).height,
), ),
Row( destinationBody
children: <Widget>[
const SizedBox(width: 16),
buildIcon(context),
const SizedBox(width: 12),
buildLabel(context),
],
),
], ],
), ),
), ),
...@@ -702,7 +721,9 @@ class _NavigationDrawerDefaultsM3 extends NavigationDrawerThemeData { ...@@ -702,7 +721,9 @@ class _NavigationDrawerDefaultsM3 extends NavigationDrawerThemeData {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) { return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
return IconThemeData( return IconThemeData(
size: 24.0, size: 24.0,
color: states.contains(MaterialState.selected) color: states.contains(MaterialState.disabled)
? _colors.onSurfaceVariant.withOpacity(0.38)
: states.contains(MaterialState.selected)
? _colors.onSecondaryContainer ? _colors.onSecondaryContainer
: _colors.onSurfaceVariant, : _colors.onSurfaceVariant,
); );
...@@ -714,7 +735,9 @@ class _NavigationDrawerDefaultsM3 extends NavigationDrawerThemeData { ...@@ -714,7 +735,9 @@ class _NavigationDrawerDefaultsM3 extends NavigationDrawerThemeData {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) { return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
final TextStyle style = _textTheme.labelLarge!; final TextStyle style = _textTheme.labelLarge!;
return style.apply( return style.apply(
color: states.contains(MaterialState.selected) color: states.contains(MaterialState.disabled)
? _colors.onSurfaceVariant.withOpacity(0.38)
: states.contains(MaterialState.selected)
? _colors.onSecondaryContainer ? _colors.onSecondaryContainer
: _colors.onSurfaceVariant, : _colors.onSurfaceVariant,
); );
......
...@@ -395,6 +395,57 @@ void main() { ...@@ -395,6 +395,57 @@ void main() {
final NavigationDrawer drawer = tester.widget(find.byType(NavigationDrawer)); final NavigationDrawer drawer = tester.widget(find.byType(NavigationDrawer));
expect(drawer.tilePadding, const EdgeInsets.symmetric(horizontal: 12.0)); expect(drawer.tilePadding, const EdgeInsets.symmetric(horizontal: 12.0));
}); });
testWidgetsWithLeakTracking('Destinations respect their disabled state', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
int selectedIndex = 0;
widgetSetup(tester, 800);
final Widget widget = _buildWidget(
scaffoldKey,
NavigationDrawer(
children: const <Widget>[
NavigationDrawerDestination(
icon: Icon(Icons.ac_unit),
label: Text('AC'),
),
NavigationDrawerDestination(
icon: Icon(Icons.access_alarm),
label: Text('Alarm'),
),
NavigationDrawerDestination(
icon: Icon(Icons.accessible),
label: Text('Accessible'),
enabled: false,
),
],
onDestinationSelected: (int i) {
selectedIndex = i;
},
),
);
await tester.pumpWidget(widget);
scaffoldKey.currentState!.openDrawer();
await tester.pump();
expect(find.text('AC'), findsOneWidget);
expect(find.text('Alarm'), findsOneWidget);
expect(find.text('Accessible'), findsOneWidget);
await tester.pump(const Duration(seconds: 1));
expect(selectedIndex, 0);
await tester.tap(find.text('Alarm'));
expect(selectedIndex, 1);
await tester.tap(find.text('Accessible'));
expect(selectedIndex, 1);
tester.pumpAndSettle();
});
} }
Widget _buildWidget(GlobalKey<ScaffoldState> scaffoldKey, Widget child, { bool? useMaterial3 }) { Widget _buildWidget(GlobalKey<ScaffoldState> scaffoldKey, Widget child, { bool? useMaterial3 }) {
......
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