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 {
required this.icon,
this.selectedIcon,
required this.label,
this.enabled = true,
});
/// Sets the color of the [Material] that holds all of the [Drawer]'s
......@@ -229,12 +230,20 @@ class NavigationDrawerDestination extends StatelessWidget {
/// text style would use [TextTheme.labelLarge] with [ColorScheme.onSurfaceVariant].
final Widget label;
/// Indicates that this destination is selectable.
///
/// Defaults to true.
final bool enabled;
@override
Widget build(BuildContext context) {
const Set<MaterialState> selectedState = <MaterialState>{
MaterialState.selected
};
const Set<MaterialState> unselectedState = <MaterialState>{};
const Set<MaterialState> disabledState = <MaterialState>{
MaterialState.disabled
};
final NavigationDrawerThemeData navigationDrawerTheme =
NavigationDrawerTheme.of(context);
......@@ -247,13 +256,13 @@ class NavigationDrawerDestination extends StatelessWidget {
return _NavigationDestinationBuilder(
buildIcon: (BuildContext context) {
final Widget selectedIconWidget = IconTheme.merge(
data: navigationDrawerTheme.iconTheme?.resolve(selectedState) ??
defaults.iconTheme!.resolve(selectedState)!,
data: navigationDrawerTheme.iconTheme?.resolve(enabled ? selectedState : disabledState) ??
defaults.iconTheme!.resolve(enabled ? selectedState : disabledState)!,
child: selectedIcon ?? icon,
);
final Widget unselectedIconWidget = IconTheme.merge(
data: navigationDrawerTheme.iconTheme?.resolve(unselectedState) ??
defaults.iconTheme!.resolve(unselectedState)!,
data: navigationDrawerTheme.iconTheme?.resolve(enabled ? unselectedState : disabledState) ??
defaults.iconTheme!.resolve(enabled ? unselectedState : disabledState)!,
child: icon,
);
......@@ -263,11 +272,12 @@ class NavigationDrawerDestination extends StatelessWidget {
},
buildLabel: (BuildContext context) {
final TextStyle? effectiveSelectedLabelTextStyle =
navigationDrawerTheme.labelTextStyle?.resolve(selectedState) ??
defaults.labelTextStyle!.resolve(selectedState);
navigationDrawerTheme.labelTextStyle?.resolve(enabled ? selectedState : disabledState) ??
defaults.labelTextStyle!.resolve(enabled ? selectedState : disabledState);
final TextStyle? effectiveUnselectedLabelTextStyle =
navigationDrawerTheme.labelTextStyle?.resolve(unselectedState) ??
defaults.labelTextStyle!.resolve(unselectedState);
navigationDrawerTheme.labelTextStyle?.resolve(enabled ? unselectedState : disabledState) ??
defaults.labelTextStyle!.resolve(enabled ? unselectedState : disabledState);
return DefaultTextStyle(
style: _isForwardOrCompleted(animation)
? effectiveSelectedLabelTextStyle!
......@@ -275,6 +285,7 @@ class NavigationDrawerDestination extends StatelessWidget {
child: label,
);
},
enabled: enabled,
);
}
}
......@@ -296,6 +307,7 @@ class _NavigationDestinationBuilder extends StatelessWidget {
const _NavigationDestinationBuilder({
required this.buildIcon,
required this.buildLabel,
this.enabled = true,
});
/// Builds the icon for a destination in a [NavigationDrawer].
......@@ -322,12 +334,26 @@ class _NavigationDestinationBuilder extends StatelessWidget {
/// animation is decreasing or dismissed.
final WidgetBuilder buildLabel;
/// Indicates that this destination is selectable.
///
/// Defaults to true.
final bool enabled;
@override
Widget build(BuildContext context) {
final _NavigationDrawerDestinationInfo info = _NavigationDrawerDestinationInfo.of(context);
final NavigationDrawerThemeData navigationDrawerTheme = NavigationDrawerTheme.of(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(
padding: info.tilePadding,
child: _NavigationDestinationSemantics(
......@@ -335,7 +361,7 @@ class _NavigationDestinationBuilder extends StatelessWidget {
height: navigationDrawerTheme.tileHeight ?? defaults.tileHeight,
child: InkWell(
highlightColor: Colors.transparent,
onTap: info.onTap,
onTap: enabled ? info.onTap : null,
customBorder: info.indicatorShape ?? navigationDrawerTheme.indicatorShape ?? defaults.indicatorShape!,
child: Stack(
alignment: Alignment.center,
......@@ -347,14 +373,7 @@ class _NavigationDestinationBuilder extends StatelessWidget {
width: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).width,
height: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).height,
),
Row(
children: <Widget>[
const SizedBox(width: 16),
buildIcon(context),
const SizedBox(width: 12),
buildLabel(context),
],
),
destinationBody
],
),
),
......@@ -702,7 +721,9 @@ class _NavigationDrawerDefaultsM3 extends NavigationDrawerThemeData {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
return IconThemeData(
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.onSurfaceVariant,
);
......@@ -714,7 +735,9 @@ class _NavigationDrawerDefaultsM3 extends NavigationDrawerThemeData {
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
final TextStyle style = _textTheme.labelLarge!;
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.onSurfaceVariant,
);
......
......@@ -395,6 +395,57 @@ void main() {
final NavigationDrawer drawer = tester.widget(find.byType(NavigationDrawer));
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 }) {
......
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