Fix NavigationRail examples overflow alignment (#159937)

Fix https://github.com/flutter/flutter/issues/160270

Fix the NavigationRail examples overflow alignment by replacing it with
the SegmentedButton widget.

### Before
Example 1:
<img
src="https://github.com/user-attachments/assets/b9f54cd2-b2d1-44ee-a159-f1f04ed011e2"
alt="NavigationRail Example 1 - Before" height="450">

Example: 2:
<img
src="https://github.com/user-attachments/assets/be78bee7-f03d-40ff-ae36-679416c9c3d2"
alt="NavigationRail Example 2 - Before" height="450">

### After
Example 1:
<img
src="https://github.com/user-attachments/assets/c6a3f060-dc9a-44d3-9ab3-3eea5f6183d6"
alt="NavigationRail Example 1 - After" height="450">

Example 2:
<img
src="https://github.com/user-attachments/assets/6b35753b-e2ab-4053-8dd7-d983531b2c74"
alt="NavigationRail Example 2 - After" height="450">




## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [X] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Taha Tesser <tessertaha@gmail.com>
This commit is contained in:
Christofer
2025-01-31 07:29:25 -08:00
committed by GitHub
parent 683096002d
commit 1416381ecc
5 changed files with 142 additions and 448 deletions

View File

@@ -34,151 +34,127 @@ class _NavRailExampleState extends State<NavRailExample> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: <Widget>[
NavigationRail(
selectedIndex: _selectedIndex,
groupAlignment: groupAlignment,
onDestinationSelected: (int index) {
setState(() {
_selectedIndex = index;
});
},
labelType: labelType,
leading:
showLeading
? FloatingActionButton(
elevation: 0,
onPressed: () {
// Add your onPressed code here!
},
child: const Icon(Icons.add),
)
: const SizedBox(),
trailing:
showTrailing
? IconButton(
onPressed: () {
// Add your onPressed code here!
},
icon: const Icon(Icons.more_horiz_rounded),
)
: const SizedBox(),
destinations: const <NavigationRailDestination>[
NavigationRailDestination(
icon: Icon(Icons.favorite_border),
selectedIcon: Icon(Icons.favorite),
label: Text('First'),
),
NavigationRailDestination(
icon: Icon(Icons.bookmark_border),
selectedIcon: Icon(Icons.book),
label: Text('Second'),
),
NavigationRailDestination(
icon: Icon(Icons.star_border),
selectedIcon: Icon(Icons.star),
label: Text('Third'),
),
],
),
const VerticalDivider(thickness: 1, width: 1),
// This is the main content.
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('selectedIndex: $_selectedIndex'),
const SizedBox(height: 20),
Text('Label type: ${labelType.name}'),
const SizedBox(height: 10),
OverflowBar(
spacing: 10.0,
children: <Widget>[
ElevatedButton(
onPressed: () {
setState(() {
labelType = NavigationRailLabelType.none;
});
},
child: const Text('None'),
),
ElevatedButton(
onPressed: () {
setState(() {
labelType = NavigationRailLabelType.selected;
});
},
child: const Text('Selected'),
),
ElevatedButton(
onPressed: () {
setState(() {
labelType = NavigationRailLabelType.all;
});
},
child: const Text('All'),
),
],
body: SafeArea(
child: Row(
children: <Widget>[
NavigationRail(
selectedIndex: _selectedIndex,
groupAlignment: groupAlignment,
onDestinationSelected: (int index) {
setState(() {
_selectedIndex = index;
});
},
labelType: labelType,
leading:
showLeading
? FloatingActionButton(
elevation: 0,
onPressed: () {
// Add your onPressed code here!
},
child: const Icon(Icons.add),
)
: const SizedBox(),
trailing:
showTrailing
? IconButton(
onPressed: () {
// Add your onPressed code here!
},
icon: const Icon(Icons.more_horiz_rounded),
)
: const SizedBox(),
destinations: const <NavigationRailDestination>[
NavigationRailDestination(
icon: Icon(Icons.favorite_border),
selectedIcon: Icon(Icons.favorite),
label: Text('First'),
),
const SizedBox(height: 20),
Text('Group alignment: $groupAlignment'),
const SizedBox(height: 10),
OverflowBar(
spacing: 10.0,
children: <Widget>[
ElevatedButton(
onPressed: () {
setState(() {
groupAlignment = -1.0;
});
},
child: const Text('Top'),
),
ElevatedButton(
onPressed: () {
setState(() {
groupAlignment = 0.0;
});
},
child: const Text('Center'),
),
ElevatedButton(
onPressed: () {
setState(() {
groupAlignment = 1.0;
});
},
child: const Text('Bottom'),
),
],
NavigationRailDestination(
icon: Badge(child: Icon(Icons.bookmark_border)),
selectedIcon: Badge(child: Icon(Icons.book)),
label: Text('Second'),
),
const SizedBox(height: 20),
OverflowBar(
spacing: 10.0,
children: <Widget>[
ElevatedButton(
onPressed: () {
setState(() {
showLeading = !showLeading;
});
},
child: Text(showLeading ? 'Hide Leading' : 'Show Leading'),
),
ElevatedButton(
onPressed: () {
setState(() {
showTrailing = !showTrailing;
});
},
child: Text(showTrailing ? 'Hide Trailing' : 'Show Trailing'),
),
],
NavigationRailDestination(
icon: Badge(label: Text('4'), child: Icon(Icons.star_border)),
selectedIcon: Badge(label: Text('4'), child: Icon(Icons.star)),
label: Text('Third'),
),
],
),
),
],
const VerticalDivider(thickness: 1, width: 1),
// This is the main content.
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('selectedIndex: $_selectedIndex'),
const SizedBox(height: 20),
Text('Label type: ${labelType.name}'),
const SizedBox(height: 10),
SegmentedButton<NavigationRailLabelType>(
segments: const <ButtonSegment<NavigationRailLabelType>>[
ButtonSegment<NavigationRailLabelType>(
value: NavigationRailLabelType.none,
label: Text('None'),
),
ButtonSegment<NavigationRailLabelType>(
value: NavigationRailLabelType.selected,
label: Text('Selected'),
),
ButtonSegment<NavigationRailLabelType>(
value: NavigationRailLabelType.all,
label: Text('All'),
),
],
selected: <NavigationRailLabelType>{labelType},
onSelectionChanged: (Set<NavigationRailLabelType> newSelection) {
setState(() {
labelType = newSelection.first;
});
},
),
const SizedBox(height: 20),
Text('Group alignment: $groupAlignment'),
const SizedBox(height: 10),
SegmentedButton<double>(
segments: const <ButtonSegment<double>>[
ButtonSegment<double>(value: -1.0, label: Text('Top')),
ButtonSegment<double>(value: 0.0, label: Text('Center')),
ButtonSegment<double>(value: 1.0, label: Text('Bottom')),
],
selected: <double>{groupAlignment},
onSelectionChanged: (Set<double> newSelection) {
setState(() {
groupAlignment = newSelection.first;
});
},
),
const SizedBox(height: 20),
SwitchListTile(
title: Text(showLeading ? 'Hide Leading' : 'Show Leading'),
value: showLeading,
onChanged: (bool value) {
setState(() {
showLeading = value;
});
},
),
SwitchListTile(
title: Text(showTrailing ? 'Hide Trailing' : 'Show Trailing'),
value: showTrailing,
onChanged: (bool value) {
setState(() {
showTrailing = value;
});
},
),
],
),
),
],
),
),
);
}

View File

@@ -1,187 +0,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 'package:flutter/material.dart';
/// Flutter code sample for [NavigationRail].
void main() => runApp(const NavigationRailExampleApp());
class NavigationRailExampleApp extends StatelessWidget {
const NavigationRailExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(theme: ThemeData(useMaterial3: true), home: const NavRailExample());
}
}
class NavRailExample extends StatefulWidget {
const NavRailExample({super.key});
@override
State<NavRailExample> createState() => _NavRailExampleState();
}
class _NavRailExampleState extends State<NavRailExample> {
int _selectedIndex = 0;
NavigationRailLabelType labelType = NavigationRailLabelType.all;
bool showLeading = false;
bool showTrailing = false;
double groupAlignment = -1.0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Row(
children: <Widget>[
NavigationRail(
selectedIndex: _selectedIndex,
groupAlignment: groupAlignment,
onDestinationSelected: (int index) {
setState(() {
_selectedIndex = index;
});
},
labelType: labelType,
leading:
showLeading
? FloatingActionButton(
elevation: 0,
onPressed: () {
// Add your onPressed code here!
},
child: const Icon(Icons.add),
)
: const SizedBox(),
trailing:
showTrailing
? IconButton(
onPressed: () {
// Add your onPressed code here!
},
icon: const Icon(Icons.more_horiz_rounded),
)
: const SizedBox(),
destinations: const <NavigationRailDestination>[
NavigationRailDestination(
icon: Icon(Icons.favorite_border),
selectedIcon: Icon(Icons.favorite),
label: Text('First'),
),
NavigationRailDestination(
icon: Badge(child: Icon(Icons.bookmark_border)),
selectedIcon: Badge(child: Icon(Icons.book)),
label: Text('Second'),
),
NavigationRailDestination(
icon: Badge(label: Text('4'), child: Icon(Icons.star_border)),
selectedIcon: Badge(label: Text('4'), child: Icon(Icons.star)),
label: Text('Third'),
),
],
),
const VerticalDivider(thickness: 1, width: 1),
// This is the main content.
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('selectedIndex: $_selectedIndex'),
const SizedBox(height: 20),
Text('Label type: ${labelType.name}'),
const SizedBox(height: 10),
OverflowBar(
spacing: 10.0,
children: <Widget>[
ElevatedButton(
onPressed: () {
setState(() {
labelType = NavigationRailLabelType.none;
});
},
child: const Text('None'),
),
ElevatedButton(
onPressed: () {
setState(() {
labelType = NavigationRailLabelType.selected;
});
},
child: const Text('Selected'),
),
ElevatedButton(
onPressed: () {
setState(() {
labelType = NavigationRailLabelType.all;
});
},
child: const Text('All'),
),
],
),
const SizedBox(height: 20),
Text('Group alignment: $groupAlignment'),
const SizedBox(height: 10),
OverflowBar(
spacing: 10.0,
children: <Widget>[
ElevatedButton(
onPressed: () {
setState(() {
groupAlignment = -1.0;
});
},
child: const Text('Top'),
),
ElevatedButton(
onPressed: () {
setState(() {
groupAlignment = 0.0;
});
},
child: const Text('Center'),
),
ElevatedButton(
onPressed: () {
setState(() {
groupAlignment = 1.0;
});
},
child: const Text('Bottom'),
),
],
),
const SizedBox(height: 20),
OverflowBar(
spacing: 10.0,
children: <Widget>[
ElevatedButton(
onPressed: () {
setState(() {
showLeading = !showLeading;
});
},
child: Text(showLeading ? 'Hide Leading' : 'Show Leading'),
),
ElevatedButton(
onPressed: () {
setState(() {
showTrailing = !showTrailing;
});
},
child: Text(showTrailing ? 'Hide Trailing' : 'Show Trailing'),
),
],
),
],
),
),
],
),
),
);
}
}

View File

@@ -7,7 +7,7 @@ import 'package:flutter_api_samples/material/navigation_rail/navigation_rail.0.d
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Navigation rail updates destination on tap', (WidgetTester tester) async {
testWidgets('NavigationRail updates destination on tap', (WidgetTester tester) async {
await tester.pumpWidget(const example.NavigationRailExampleApp());
final NavigationRail navigationRailWidget = tester.firstWidget(find.byType(NavigationRail));
@@ -30,19 +30,19 @@ void main() {
expect(find.text('selectedIndex: 2'), findsOneWidget);
});
testWidgets('Navigation rail updates label type', (WidgetTester tester) async {
testWidgets('NavigationRail updates label type', (WidgetTester tester) async {
await tester.pumpWidget(const example.NavigationRailExampleApp());
// initial label type set to all.
expect(find.text('Label type: all'), findsOneWidget);
// switch to selected label type
await tester.tap(find.widgetWithText(ElevatedButton, 'Selected'));
await tester.tap(find.text('Selected'));
await tester.pumpAndSettle();
expect(find.text('Label type: selected'), findsOneWidget);
// switch to none label type
await tester.tap(find.widgetWithText(ElevatedButton, 'None'));
await tester.tap(find.text('None'));
await tester.pumpAndSettle();
expect(find.text('Label type: none'), findsOneWidget);
});
@@ -54,17 +54,17 @@ void main() {
expect(find.text('Group alignment: -1.0'), findsOneWidget);
// switch to center alignment
await tester.tap(find.widgetWithText(ElevatedButton, 'Center'));
await tester.tap(find.text('Center'));
await tester.pumpAndSettle();
expect(find.text('Group alignment: 0.0'), findsOneWidget);
// switch to bottom alignment
await tester.tap(find.widgetWithText(ElevatedButton, 'Bottom'));
await tester.tap(find.text('Bottom'));
await tester.pumpAndSettle();
expect(find.text('Group alignment: 1.0'), findsOneWidget);
});
testWidgets('Navigation rail shows leading/trailing widgets', (WidgetTester tester) async {
testWidgets('NavigationRail shows leading/trailing widgets', (WidgetTester tester) async {
await tester.pumpWidget(const example.NavigationRailExampleApp());
// Initially leading/trailing widgets are hidden.
@@ -72,15 +72,31 @@ void main() {
expect(find.byType(IconButton), findsNothing);
// Tap to show leading Widget.
await tester.tap(find.widgetWithText(ElevatedButton, 'Show Leading'));
await tester.tap(find.text('Show Leading'));
await tester.pumpAndSettle();
expect(find.byType(FloatingActionButton), findsOneWidget);
expect(find.byType(IconButton), findsNothing);
// Tap to show trailing Widget.
await tester.tap(find.widgetWithText(ElevatedButton, 'Show Trailing'));
await tester.tap(find.text('Show Trailing'));
await tester.pumpAndSettle();
expect(find.byType(FloatingActionButton), findsOneWidget);
expect(find.byType(IconButton), findsOneWidget);
});
testWidgets('Destinations have badge', (WidgetTester tester) async {
await tester.pumpWidget(const example.NavigationRailExampleApp());
// Test badge without label.
final Badge notificationBadge = tester.firstWidget(
find.ancestor(of: find.byIcon(Icons.bookmark_border), matching: find.byType(Badge)),
);
expect(notificationBadge.label, null);
// Test badge with label.
final Badge messagesBadge = tester.firstWidget(
find.ancestor(of: find.byIcon(Icons.star_border), matching: find.byType(Badge)),
);
expect(messagesBadge.label, isNotNull);
});
}

View File

@@ -1,102 +0,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 'package:flutter/material.dart';
import 'package:flutter_api_samples/material/navigation_rail/navigation_rail.1.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Navigation rail updates destination on tap', (WidgetTester tester) async {
await tester.pumpWidget(const example.NavigationRailExampleApp());
final NavigationRail navigationRailWidget = tester.firstWidget(find.byType(NavigationRail));
/// NavigationRailDestinations must be rendered
expect(find.text('First'), findsOneWidget);
expect(find.text('Second'), findsOneWidget);
expect(find.text('Third'), findsOneWidget);
/// initial index must be zero
expect(navigationRailWidget.selectedIndex, 0);
/// switch to second tab
await tester.tap(find.text('Second'));
await tester.pumpAndSettle();
expect(find.text('selectedIndex: 1'), findsOneWidget);
/// switch to third tab
await tester.tap(find.text('Third'));
await tester.pumpAndSettle();
expect(find.text('selectedIndex: 2'), findsOneWidget);
});
testWidgets('Navigation rail updates label type', (WidgetTester tester) async {
await tester.pumpWidget(const example.NavigationRailExampleApp());
// initial label type set to all.
expect(find.text('Label type: all'), findsOneWidget);
// switch to selected label type
await tester.tap(find.widgetWithText(ElevatedButton, 'Selected'));
await tester.pumpAndSettle();
expect(find.text('Label type: selected'), findsOneWidget);
// switch to none label type
await tester.tap(find.widgetWithText(ElevatedButton, 'None'));
await tester.pumpAndSettle();
expect(find.text('Label type: none'), findsOneWidget);
});
testWidgets('Navigation rail updates group alignment', (WidgetTester tester) async {
await tester.pumpWidget(const example.NavigationRailExampleApp());
// initial group alignment set top top.
expect(find.text('Group alignment: -1.0'), findsOneWidget);
// switch to center alignment
await tester.tap(find.widgetWithText(ElevatedButton, 'Center'));
await tester.pumpAndSettle();
expect(find.text('Group alignment: 0.0'), findsOneWidget);
// switch to bottom alignment
await tester.tap(find.widgetWithText(ElevatedButton, 'Bottom'));
await tester.pumpAndSettle();
expect(find.text('Group alignment: 1.0'), findsOneWidget);
});
testWidgets('Navigation rail shows leading/trailing widgets', (WidgetTester tester) async {
await tester.pumpWidget(const example.NavigationRailExampleApp());
// Initially leading/trailing widgets are hidden.
expect(find.byType(FloatingActionButton), findsNothing);
expect(find.byType(IconButton), findsNothing);
// Tap to show leading Widget.
await tester.tap(find.widgetWithText(ElevatedButton, 'Show Leading'));
await tester.pumpAndSettle();
expect(find.byType(FloatingActionButton), findsOneWidget);
expect(find.byType(IconButton), findsNothing);
// Tap to show trailing Widget.
await tester.tap(find.widgetWithText(ElevatedButton, 'Show Trailing'));
await tester.pumpAndSettle();
expect(find.byType(FloatingActionButton), findsOneWidget);
expect(find.byType(IconButton), findsOneWidget);
});
testWidgets('Destinations have badge', (WidgetTester tester) async {
await tester.pumpWidget(const example.NavigationRailExampleApp());
// Test badge without label.
final Badge notificationBadge = tester.firstWidget(
find.ancestor(of: find.byIcon(Icons.bookmark_border), matching: find.byType(Badge)),
);
expect(notificationBadge.label, null);
// Test badge with label.
final Badge messagesBadge = tester.firstWidget(
find.ancestor(of: find.byIcon(Icons.star_border), matching: find.byType(Badge)),
);
expect(messagesBadge.label, isNotNull);
});
}

View File

@@ -49,19 +49,10 @@ const double _kIndicatorHeight = 32;
/// for an example.
///
/// {@tool dartpad}
/// This example shows a [NavigationRail] used within a Scaffold with 3
/// [NavigationRailDestination]s. The main content is separated by a divider
/// (although elevation on the navigation rail can be used instead). The
/// `_selectedIndex` is updated by the `onDestinationSelected` callback.
///
/// ** See code in examples/api/lib/material/navigation_rail/navigation_rail.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This sample shows the creation of [NavigationRail] widget used within a Scaffold with 3
/// [NavigationRailDestination]s, as described in: https://m3.material.io/components/navigation-rail/overview
///
/// ** See code in examples/api/lib/material/navigation_rail/navigation_rail.1.dart **
/// ** See code in examples/api/lib/material/navigation_rail/navigation_rail.0.dart **
/// {@end-tool}
///
/// See also: