Skip to content

Commit 801a1c9

Browse files
Added option to disable [NavigationDrawerDestination]s (flutter#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 * flutter#132348
1 parent 12d761a commit 801a1c9

File tree

2 files changed

+93
-19
lines changed

2 files changed

+93
-19
lines changed

packages/flutter/lib/src/material/navigation_drawer.dart

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ class NavigationDrawerDestination extends StatelessWidget {
193193
required this.icon,
194194
this.selectedIcon,
195195
required this.label,
196+
this.enabled = true,
196197
});
197198

198199
/// Sets the color of the [Material] that holds all of the [Drawer]'s
@@ -229,12 +230,20 @@ class NavigationDrawerDestination extends StatelessWidget {
229230
/// text style would use [TextTheme.labelLarge] with [ColorScheme.onSurfaceVariant].
230231
final Widget label;
231232

233+
/// Indicates that this destination is selectable.
234+
///
235+
/// Defaults to true.
236+
final bool enabled;
237+
232238
@override
233239
Widget build(BuildContext context) {
234240
const Set<MaterialState> selectedState = <MaterialState>{
235241
MaterialState.selected
236242
};
237243
const Set<MaterialState> unselectedState = <MaterialState>{};
244+
const Set<MaterialState> disabledState = <MaterialState>{
245+
MaterialState.disabled
246+
};
238247

239248
final NavigationDrawerThemeData navigationDrawerTheme =
240249
NavigationDrawerTheme.of(context);
@@ -247,13 +256,13 @@ class NavigationDrawerDestination extends StatelessWidget {
247256
return _NavigationDestinationBuilder(
248257
buildIcon: (BuildContext context) {
249258
final Widget selectedIconWidget = IconTheme.merge(
250-
data: navigationDrawerTheme.iconTheme?.resolve(selectedState) ??
251-
defaults.iconTheme!.resolve(selectedState)!,
259+
data: navigationDrawerTheme.iconTheme?.resolve(enabled ? selectedState : disabledState) ??
260+
defaults.iconTheme!.resolve(enabled ? selectedState : disabledState)!,
252261
child: selectedIcon ?? icon,
253262
);
254263
final Widget unselectedIconWidget = IconTheme.merge(
255-
data: navigationDrawerTheme.iconTheme?.resolve(unselectedState) ??
256-
defaults.iconTheme!.resolve(unselectedState)!,
264+
data: navigationDrawerTheme.iconTheme?.resolve(enabled ? unselectedState : disabledState) ??
265+
defaults.iconTheme!.resolve(enabled ? unselectedState : disabledState)!,
257266
child: icon,
258267
);
259268

@@ -263,18 +272,20 @@ class NavigationDrawerDestination extends StatelessWidget {
263272
},
264273
buildLabel: (BuildContext context) {
265274
final TextStyle? effectiveSelectedLabelTextStyle =
266-
navigationDrawerTheme.labelTextStyle?.resolve(selectedState) ??
267-
defaults.labelTextStyle!.resolve(selectedState);
275+
navigationDrawerTheme.labelTextStyle?.resolve(enabled ? selectedState : disabledState) ??
276+
defaults.labelTextStyle!.resolve(enabled ? selectedState : disabledState);
268277
final TextStyle? effectiveUnselectedLabelTextStyle =
269-
navigationDrawerTheme.labelTextStyle?.resolve(unselectedState) ??
270-
defaults.labelTextStyle!.resolve(unselectedState);
278+
navigationDrawerTheme.labelTextStyle?.resolve(enabled ? unselectedState : disabledState) ??
279+
defaults.labelTextStyle!.resolve(enabled ? unselectedState : disabledState);
280+
271281
return DefaultTextStyle(
272282
style: _isForwardOrCompleted(animation)
273283
? effectiveSelectedLabelTextStyle!
274284
: effectiveUnselectedLabelTextStyle!,
275285
child: label,
276286
);
277287
},
288+
enabled: enabled,
278289
);
279290
}
280291
}
@@ -296,6 +307,7 @@ class _NavigationDestinationBuilder extends StatelessWidget {
296307
const _NavigationDestinationBuilder({
297308
required this.buildIcon,
298309
required this.buildLabel,
310+
this.enabled = true,
299311
});
300312

301313
/// Builds the icon for a destination in a [NavigationDrawer].
@@ -322,20 +334,34 @@ class _NavigationDestinationBuilder extends StatelessWidget {
322334
/// animation is decreasing or dismissed.
323335
final WidgetBuilder buildLabel;
324336

337+
/// Indicates that this destination is selectable.
338+
///
339+
/// Defaults to true.
340+
final bool enabled;
341+
325342
@override
326343
Widget build(BuildContext context) {
327344
final _NavigationDrawerDestinationInfo info = _NavigationDrawerDestinationInfo.of(context);
328345
final NavigationDrawerThemeData navigationDrawerTheme = NavigationDrawerTheme.of(context);
329346
final NavigationDrawerThemeData defaults = _NavigationDrawerDefaultsM3(context);
330347

348+
final Row destinationBody = Row(
349+
children: <Widget>[
350+
const SizedBox(width: 16),
351+
buildIcon(context),
352+
const SizedBox(width: 12),
353+
buildLabel(context),
354+
],
355+
);
356+
331357
return Padding(
332358
padding: info.tilePadding,
333359
child: _NavigationDestinationSemantics(
334360
child: SizedBox(
335361
height: navigationDrawerTheme.tileHeight ?? defaults.tileHeight,
336362
child: InkWell(
337363
highlightColor: Colors.transparent,
338-
onTap: info.onTap,
364+
onTap: enabled ? info.onTap : null,
339365
customBorder: info.indicatorShape ?? navigationDrawerTheme.indicatorShape ?? defaults.indicatorShape!,
340366
child: Stack(
341367
alignment: Alignment.center,
@@ -347,14 +373,7 @@ class _NavigationDestinationBuilder extends StatelessWidget {
347373
width: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).width,
348374
height: (navigationDrawerTheme.indicatorSize ?? defaults.indicatorSize!).height,
349375
),
350-
Row(
351-
children: <Widget>[
352-
const SizedBox(width: 16),
353-
buildIcon(context),
354-
const SizedBox(width: 12),
355-
buildLabel(context),
356-
],
357-
),
376+
destinationBody
358377
],
359378
),
360379
),
@@ -702,7 +721,9 @@ class _NavigationDrawerDefaultsM3 extends NavigationDrawerThemeData {
702721
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
703722
return IconThemeData(
704723
size: 24.0,
705-
color: states.contains(MaterialState.selected)
724+
color: states.contains(MaterialState.disabled)
725+
? _colors.onSurfaceVariant.withOpacity(0.38)
726+
: states.contains(MaterialState.selected)
706727
? _colors.onSecondaryContainer
707728
: _colors.onSurfaceVariant,
708729
);
@@ -714,7 +735,9 @@ class _NavigationDrawerDefaultsM3 extends NavigationDrawerThemeData {
714735
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
715736
final TextStyle style = _textTheme.labelLarge!;
716737
return style.apply(
717-
color: states.contains(MaterialState.selected)
738+
color: states.contains(MaterialState.disabled)
739+
? _colors.onSurfaceVariant.withOpacity(0.38)
740+
: states.contains(MaterialState.selected)
718741
? _colors.onSecondaryContainer
719742
: _colors.onSurfaceVariant,
720743
);

packages/flutter/test/material/navigation_drawer_test.dart

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,57 @@ void main() {
395395
final NavigationDrawer drawer = tester.widget(find.byType(NavigationDrawer));
396396
expect(drawer.tilePadding, const EdgeInsets.symmetric(horizontal: 12.0));
397397
});
398+
399+
testWidgetsWithLeakTracking('Destinations respect their disabled state', (WidgetTester tester) async {
400+
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
401+
int selectedIndex = 0;
402+
403+
widgetSetup(tester, 800);
404+
405+
final Widget widget = _buildWidget(
406+
scaffoldKey,
407+
NavigationDrawer(
408+
children: const <Widget>[
409+
NavigationDrawerDestination(
410+
icon: Icon(Icons.ac_unit),
411+
label: Text('AC'),
412+
),
413+
NavigationDrawerDestination(
414+
icon: Icon(Icons.access_alarm),
415+
label: Text('Alarm'),
416+
),
417+
NavigationDrawerDestination(
418+
icon: Icon(Icons.accessible),
419+
label: Text('Accessible'),
420+
enabled: false,
421+
),
422+
],
423+
onDestinationSelected: (int i) {
424+
selectedIndex = i;
425+
},
426+
),
427+
);
428+
429+
await tester.pumpWidget(widget);
430+
scaffoldKey.currentState!.openDrawer();
431+
await tester.pump();
432+
433+
expect(find.text('AC'), findsOneWidget);
434+
expect(find.text('Alarm'), findsOneWidget);
435+
expect(find.text('Accessible'), findsOneWidget);
436+
437+
await tester.pump(const Duration(seconds: 1));
438+
439+
expect(selectedIndex, 0);
440+
441+
await tester.tap(find.text('Alarm'));
442+
expect(selectedIndex, 1);
443+
444+
await tester.tap(find.text('Accessible'));
445+
expect(selectedIndex, 1);
446+
447+
tester.pumpAndSettle();
448+
});
398449
}
399450

400451
Widget _buildWidget(GlobalKey<ScaffoldState> scaffoldKey, Widget child, { bool? useMaterial3 }) {

0 commit comments

Comments
 (0)