diff --git a/examples/api/lib/material/navigation_rail/navigation_rail.0.dart b/examples/api/lib/material/navigation_rail/navigation_rail.0.dart index 9c4ffa0622..0dabb14dff 100644 --- a/examples/api/lib/material/navigation_rail/navigation_rail.0.dart +++ b/examples/api/lib/material/navigation_rail/navigation_rail.0.dart @@ -11,26 +11,27 @@ void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); - static const String _title = 'Flutter Code Sample'; - @override Widget build(BuildContext context) { return const MaterialApp( - title: _title, - home: MyStatefulWidget(), + home: NavRailExample(), ); } } -class MyStatefulWidget extends StatefulWidget { - const MyStatefulWidget({Key? key}) : super(key: key); +class NavRailExample extends StatefulWidget { + const NavRailExample({Key? key}) : super(key: key); @override - State createState() => _MyStatefulWidgetState(); + State createState() => _NavRailExampleState(); } -class _MyStatefulWidgetState extends State { +class _NavRailExampleState extends State { int _selectedIndex = 0; + NavigationRailLabelType labelType = NavigationRailLabelType.all; + bool showLeading = false; + bool showTrailing = false; + double groupAligment = -1.0; @override Widget build(BuildContext context) { @@ -39,12 +40,26 @@ class _MyStatefulWidgetState extends State { children: [ NavigationRail( selectedIndex: _selectedIndex, + groupAlignment: groupAligment, onDestinationSelected: (int index) { setState(() { _selectedIndex = index; }); }, - labelType: NavigationRailLabelType.selected, + 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( icon: Icon(Icons.favorite_border), @@ -65,11 +80,100 @@ class _MyStatefulWidgetState extends State { ), const VerticalDivider(thickness: 1, width: 1), // This is the main content. - Expanded( - child: Center( - child: Text('selectedIndex: $_selectedIndex'), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('selectedIndex: $_selectedIndex'), + const SizedBox(height: 20), + Text('Label type: ${labelType.name}'), + const SizedBox(height: 10), + OverflowBar( + spacing: 10.0, + children: [ + 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: $groupAligment'), + const SizedBox(height: 10), + OverflowBar( + spacing: 10.0, + children: [ + ElevatedButton( + onPressed: () { + setState(() { + groupAligment = -1.0; + }); + }, + child: const Text('Top'), + ), + ElevatedButton( + onPressed: () { + setState(() { + groupAligment = 0.0; + }); + }, + child: const Text('Center'), + ), + ElevatedButton( + onPressed: () { + setState(() { + groupAligment = 1.0; + }); + }, + child: const Text('Bottom'), + ), + ], + ), + const SizedBox(height: 20), + OverflowBar( + spacing: 10.0, + children: [ + ElevatedButton( + onPressed: () { + setState(() { + showLeading = !showLeading; + }); + }, + child: Text(showLeading ? 'Hide Leading' : 'Show Leading'), + ), + ElevatedButton( + onPressed: () { + setState(() { + showTrailing = !showTrailing; + }); + }, + child: Text(showTrailing ? 'Hide Trailing' : 'Show Trailing'), + ), + ], + ), + ], + ), ), - ) ], ), ); diff --git a/examples/api/lib/material/navigation_rail/navigation_rail.1.dart b/examples/api/lib/material/navigation_rail/navigation_rail.1.dart new file mode 100644 index 0000000000..c3b886fecc --- /dev/null +++ b/examples/api/lib/material/navigation_rail/navigation_rail.1.dart @@ -0,0 +1,184 @@ +// 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. + +// Flutter code sample for NavigationRail + +import 'package:flutter/material.dart'; + +void main() => runApp(const MyApp()); + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData(colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true), + home: const NavRailExample(), + ); + } +} + +class NavRailExample extends StatefulWidget { + const NavRailExample({Key? key}) : super(key: key); + + @override + State createState() => _NavRailExampleState(); +} + +class _NavRailExampleState extends State { + int _selectedIndex = 0; + NavigationRailLabelType labelType = NavigationRailLabelType.all; + bool showLeading = false; + bool showTrailing = false; + double groupAligment = -1.0; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Row( + children: [ + NavigationRail( + selectedIndex: _selectedIndex, + groupAlignment: groupAligment, + 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( + 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: [ + Text('selectedIndex: $_selectedIndex'), + const SizedBox(height: 20), + Text('Label type: ${labelType.name}'), + const SizedBox(height: 10), + OverflowBar( + spacing: 10.0, + children: [ + 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: $groupAligment'), + const SizedBox(height: 10), + OverflowBar( + spacing: 10.0, + children: [ + ElevatedButton( + onPressed: () { + setState(() { + groupAligment = -1.0; + }); + }, + child: const Text('Top'), + ), + ElevatedButton( + onPressed: () { + setState(() { + groupAligment = 0.0; + }); + }, + child: const Text('Center'), + ), + ElevatedButton( + onPressed: () { + setState(() { + groupAligment = 1.0; + }); + }, + child: const Text('Bottom'), + ), + ], + ), + const SizedBox(height: 20), + OverflowBar( + spacing: 10.0, + children: [ + ElevatedButton( + onPressed: () { + setState(() { + showLeading = !showLeading; + }); + }, + child: Text(showLeading ? 'Hide Leading' : 'Show Leading'), + ), + ElevatedButton( + onPressed: () { + setState(() { + showTrailing = !showTrailing; + }); + }, + child: Text(showTrailing ? 'Hide Trailing' : 'Show Trailing'), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/examples/api/lib/material/navigation_rail/navigation_rail.extended_animation.0.dart b/examples/api/lib/material/navigation_rail/navigation_rail.extended_animation.0.dart index 2f7e8ab2b4..6197e944f6 100644 --- a/examples/api/lib/material/navigation_rail/navigation_rail.extended_animation.0.dart +++ b/examples/api/lib/material/navigation_rail/navigation_rail.extended_animation.0.dart @@ -13,15 +13,11 @@ void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); - static const String _title = 'NavigationRail.extendedAnimation Sample'; - @override Widget build(BuildContext context) { - return MaterialApp( - title: _title, + return const MaterialApp( home: Scaffold( - appBar: AppBar(title: const Text(_title)), - body: const MyNavigationRail(), + body: MyNavigationRail(), ), ); } @@ -78,7 +74,14 @@ class _MyNavigationRailState extends State { // This is the main content. Expanded( child: Center( - child: Text('selectedIndex: $_selectedIndex'), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Tap on FloatingActionButton to expand'), + const SizedBox(height: 20), + Text('selectedIndex: $_selectedIndex'), + ], + ), ), ) ], diff --git a/examples/api/test/material/navigation_rail/navigation_rail.0_test.dart b/examples/api/test/material/navigation_rail/navigation_rail.0_test.dart new file mode 100644 index 0000000000..165c0c0874 --- /dev/null +++ b/examples/api/test/material/navigation_rail/navigation_rail.0_test.dart @@ -0,0 +1,97 @@ +// 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.0.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.MyApp(), + ); + 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.MyApp(), + ); + + // 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.MyApp(), + ); + + // 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.MyApp(), + ); + + // 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); + }); +} diff --git a/examples/api/test/material/navigation_rail/navigation_rail.1_test.dart b/examples/api/test/material/navigation_rail/navigation_rail.1_test.dart new file mode 100644 index 0000000000..ea266fbba0 --- /dev/null +++ b/examples/api/test/material/navigation_rail/navigation_rail.1_test.dart @@ -0,0 +1,97 @@ +// 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.MyApp(), + ); + 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.MyApp(), + ); + + // 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.MyApp(), + ); + + // 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.MyApp(), + ); + + // 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); + }); +} diff --git a/packages/flutter/lib/src/material/navigation_rail.dart b/packages/flutter/lib/src/material/navigation_rail.dart index b5cbabaa43..6743915ca1 100644 --- a/packages/flutter/lib/src/material/navigation_rail.dart +++ b/packages/flutter/lib/src/material/navigation_rail.dart @@ -46,6 +46,13 @@ import 'theme.dart'; /// ** 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 ** +/// {@end-tool} +/// /// See also: /// /// * [Scaffold], which can display the navigation rail within a [Row] of the