From 57aa49028db77a4edf0fbc6efc70cceea2d2c6ed Mon Sep 17 00:00:00 2001 From: Darren Austin Date: Wed, 9 Mar 2022 16:01:03 -0800 Subject: [PATCH] Migrate NavigationRail to Material 3. (#99171) --- dev/tools/gen_defaults/bin/gen_defaults.dart | 3 + .../lib/navigation_rail_template.dart | 57 + .../lib/src/material/navigation_rail.dart | 224 +- .../src/material/navigation_rail_theme.dart | 25 +- .../flutter/lib/src/material/theme_data.dart | 13 +- .../test/material/navigation_rail_test.dart | 2777 ++++++++++++++--- .../material/navigation_rail_theme_test.dart | 43 + 7 files changed, 2625 insertions(+), 517 deletions(-) create mode 100644 dev/tools/gen_defaults/lib/navigation_rail_template.dart diff --git a/dev/tools/gen_defaults/bin/gen_defaults.dart b/dev/tools/gen_defaults/bin/gen_defaults.dart index 42924544c1a..8769076c2b1 100644 --- a/dev/tools/gen_defaults/bin/gen_defaults.dart +++ b/dev/tools/gen_defaults/bin/gen_defaults.dart @@ -20,6 +20,7 @@ import 'dart:io'; import 'package:gen_defaults/dialog_template.dart'; import 'package:gen_defaults/fab_template.dart'; import 'package:gen_defaults/navigation_bar_template.dart'; +import 'package:gen_defaults/navigation_rail_template.dart'; import 'package:gen_defaults/typography_template.dart'; Map _readTokenFile(String fileName) { @@ -45,6 +46,7 @@ Future main(List args) async { 'input_chip.json', 'motion.json', 'navigation_bar.json', + 'navigation_rail.json', 'outlined_card.json', 'palette.json', 'shape.json', @@ -70,6 +72,7 @@ Future main(List args) async { FABTemplate('$materialLib/floating_action_button.dart', tokens).updateFile(); NavigationBarTemplate('$materialLib/navigation_bar.dart', tokens).updateFile(); + NavigationRailTemplate('$materialLib/navigation_rail.dart', tokens).updateFile(); TypographyTemplate('$materialLib/typography.dart', tokens).updateFile(); DialogTemplate('$materialLib/dialog.dart', tokens).updateFile(); } diff --git a/dev/tools/gen_defaults/lib/navigation_rail_template.dart b/dev/tools/gen_defaults/lib/navigation_rail_template.dart new file mode 100644 index 00000000000..567fb55f549 --- /dev/null +++ b/dev/tools/gen_defaults/lib/navigation_rail_template.dart @@ -0,0 +1,57 @@ +// 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 'template.dart'; + +class NavigationRailTemplate extends TokenTemplate { + const NavigationRailTemplate(String fileName, Map tokens) : super(fileName, tokens); + + @override + String generate() => ''' +// Generated version ${tokens["version"]} +class _TokenDefaultsM3 extends NavigationRailThemeData { + _TokenDefaultsM3(BuildContext context) + : _theme = Theme.of(context), + _colors = Theme.of(context).colorScheme, + super( + elevation: ${elevation("md.comp.navigation-rail.container")}, + groupAlignment: -1, + labelType: NavigationRailLabelType.none, + useIndicator: true, + minWidth: ${tokens["md.comp.navigation-rail.container.width"]}, + minExtendedWidth: 256, + ); + + final ThemeData _theme; + final ColorScheme _colors; + + @override Color? get backgroundColor => _colors.${color("md.comp.navigation-rail.container")}; + + @override TextStyle? get unselectedLabelTextStyle { + return _theme.textTheme.${textStyle("md.comp.navigation-rail.label-text")}!.copyWith(color: _colors.${color("md.comp.navigation-rail.inactive.focus.label-text")}); + } + + @override TextStyle? get selectedLabelTextStyle { + return _theme.textTheme.${textStyle("md.comp.navigation-rail.label-text")}!.copyWith(color: _colors.${color("md.comp.navigation-rail.active.focus.label-text")}); + } + + @override IconThemeData? get unselectedIconTheme { + return IconThemeData( + size: ${tokens["md.comp.navigation-rail.icon.size"]}, + color: _colors.${color("md.comp.navigation-rail.inactive.icon")}, + ); + } + + @override IconThemeData? get selectedIconTheme { + return IconThemeData( + size: ${tokens["md.comp.navigation-rail.icon.size"]}, + color: _colors.${color("md.comp.navigation-rail.active.icon")}, + ); + } + + @override Color? get indicatorColor => _colors.${color("md.comp.navigation-rail.active-indicator")}; + +} +'''; +} diff --git a/packages/flutter/lib/src/material/navigation_rail.dart b/packages/flutter/lib/src/material/navigation_rail.dart index 1833cee2608..84c9ae61c38 100644 --- a/packages/flutter/lib/src/material/navigation_rail.dart +++ b/packages/flutter/lib/src/material/navigation_rail.dart @@ -365,32 +365,29 @@ class _NavigationRailState extends State with TickerProviderStat @override Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); final NavigationRailThemeData navigationRailTheme = NavigationRailTheme.of(context); + final NavigationRailThemeData defaults = Theme.of(context).useMaterial3 ? _TokenDefaultsM3(context) : _DefaultsM2(context); final MaterialLocalizations localizations = MaterialLocalizations.of(context); - final Color backgroundColor = widget.backgroundColor ?? navigationRailTheme.backgroundColor ?? theme.colorScheme.surface; - final double elevation = widget.elevation ?? navigationRailTheme.elevation ?? 0; - final double minWidth = widget.minWidth ?? _minRailWidth; - final double minExtendedWidth = widget.minExtendedWidth ?? _minExtendedRailWidth; - final Color baseSelectedColor = theme.colorScheme.primary; - final Color baseUnselectedColor = theme.colorScheme.onSurface.withOpacity(0.64); - final IconThemeData? defaultUnselectedIconTheme = widget.unselectedIconTheme ?? navigationRailTheme.unselectedIconTheme; - final IconThemeData unselectedIconTheme = IconThemeData( - size: defaultUnselectedIconTheme?.size ?? 24.0, - color: defaultUnselectedIconTheme?.color ?? theme.colorScheme.onSurface, - opacity: defaultUnselectedIconTheme?.opacity ?? 0.64, - ); - final IconThemeData? defaultSelectedIconTheme = widget.selectedIconTheme ?? navigationRailTheme.selectedIconTheme; - final IconThemeData selectedIconTheme = IconThemeData( - size: defaultSelectedIconTheme?.size ?? 24.0, - color: defaultSelectedIconTheme?.color ?? theme.colorScheme.primary, - opacity: defaultSelectedIconTheme?.opacity ?? 1.0, - ); - final TextStyle unselectedLabelTextStyle = theme.textTheme.bodyText1!.copyWith(color: baseUnselectedColor).merge(widget.unselectedLabelTextStyle ?? navigationRailTheme.unselectedLabelTextStyle); - final TextStyle selectedLabelTextStyle = theme.textTheme.bodyText1!.copyWith(color: baseSelectedColor).merge(widget.selectedLabelTextStyle ?? navigationRailTheme.selectedLabelTextStyle); - final double groupAlignment = widget.groupAlignment ?? navigationRailTheme.groupAlignment ?? -1.0; - final NavigationRailLabelType labelType = widget.labelType ?? navigationRailTheme.labelType ?? NavigationRailLabelType.none; + final Color backgroundColor = widget.backgroundColor ?? navigationRailTheme.backgroundColor ?? defaults.backgroundColor!; + final double elevation = widget.elevation ?? navigationRailTheme.elevation ?? defaults.elevation!; + final double minWidth = widget.minWidth ?? navigationRailTheme.minWidth ?? defaults.minWidth!; + final double minExtendedWidth = widget.minExtendedWidth ?? navigationRailTheme.minExtendedWidth ?? defaults.minExtendedWidth!; + final TextStyle unselectedLabelTextStyle = widget.unselectedLabelTextStyle ?? navigationRailTheme.unselectedLabelTextStyle ?? defaults.unselectedLabelTextStyle!; + final TextStyle selectedLabelTextStyle = widget.selectedLabelTextStyle ?? navigationRailTheme.selectedLabelTextStyle ?? defaults.selectedLabelTextStyle!; + final IconThemeData unselectedIconTheme = widget.unselectedIconTheme ?? navigationRailTheme.unselectedIconTheme ?? defaults.unselectedIconTheme!; + final IconThemeData selectedIconTheme = widget.selectedIconTheme ?? navigationRailTheme.selectedIconTheme ?? defaults.selectedIconTheme!; + final double groupAlignment = widget.groupAlignment ?? navigationRailTheme.groupAlignment ?? defaults.groupAlignment!; + final NavigationRailLabelType labelType = widget.labelType ?? navigationRailTheme.labelType ?? defaults.labelType!; + final bool useIndicator = widget.useIndicator ?? navigationRailTheme.useIndicator ?? defaults.useIndicator!; + final Color? indicatorColor = widget.indicatorColor ?? navigationRailTheme.indicatorColor ?? defaults.indicatorColor; + + // For backwards compatibility, in M2 the opacity of the unselected icons needs + // to be set to the default if it isn't in the given theme. This can be removed + // when Material 3 is the default. + final IconThemeData effectiveUnselectedIconTheme = Theme.of(context).useMaterial3 + ? unselectedIconTheme + : unselectedIconTheme.copyWith(opacity: unselectedIconTheme.opacity ?? defaults.unselectedIconTheme!.opacity); return _ExtendedNavigationRailAnimation( animation: _extendedAnimation, @@ -404,12 +401,7 @@ class _NavigationRailState extends State with TickerProviderStat _verticalSpacer, if (widget.leading != null) ...[ - ConstrainedBox( - constraints: BoxConstraints( - minWidth: lerpDouble(minWidth, minExtendedWidth, _extendedAnimation.value)!, - ), - child: widget.leading, - ), + widget.leading!, _verticalSpacer, ], Expanded( @@ -428,11 +420,11 @@ class _NavigationRailState extends State with TickerProviderStat label: widget.destinations[i].label, destinationAnimation: _destinationAnimations[i], labelType: labelType, - iconTheme: widget.selectedIndex == i ? selectedIconTheme : unselectedIconTheme, + iconTheme: widget.selectedIndex == i ? selectedIconTheme : effectiveUnselectedIconTheme, labelTextStyle: widget.selectedIndex == i ? selectedLabelTextStyle : unselectedLabelTextStyle, padding: widget.destinations[i].padding, - useIndicator: widget.useIndicator ?? navigationRailTheme.useIndicator ?? theme.useMaterial3, - indicatorColor: widget.indicatorColor ?? navigationRailTheme.indicatorColor, + useIndicator: useIndicator, + indicatorColor: useIndicator ? indicatorColor : null, onTap: () { if (widget.onDestinationSelected != null) widget.onDestinationSelected!(i); @@ -443,12 +435,7 @@ class _NavigationRailState extends State with TickerProviderStat ), ), if (widget.trailing != null) - ConstrainedBox( - constraints: BoxConstraints( - minWidth: lerpDouble(minWidth, minExtendedWidth, _extendedAnimation.value)!, - ), - child: widget.trailing, - ), + widget.trailing!, ], ), ), @@ -563,6 +550,8 @@ class _RailDestination extends StatelessWidget { '[NavigationRail.indicatorColor] does not have an effect when [NavigationRail.useIndicator] is false', ); + final bool material3 = Theme.of(context).useMaterial3; + final Widget themedIcon = IconTheme( data: iconTheme, child: icon, @@ -576,18 +565,27 @@ class _RailDestination extends StatelessWidget { switch (labelType) { case NavigationRailLabelType.none: - final Widget iconPart = SizedBox( - width: minWidth, - height: minWidth, - child: Center( - child: _AddIndicator( - addIndicator: useIndicator, - indicatorColor: indicatorColor, - isCircular: true, - indicatorAnimation: destinationAnimation, - child: themedIcon, + // Split the destination spacing across the top and bottom to keep the icon centered. + final Widget? spacing = material3 ? const SizedBox(height: _verticalDestinationSpacingM3 / 2) : null; + + final Widget iconPart = Column( + children: [ + if (spacing != null) spacing, + SizedBox( + width: minWidth, + height: material3 ? null : minWidth, + child: Center( + child: _AddIndicator( + addIndicator: useIndicator, + indicatorColor: indicatorColor, + isCircular: !material3, + indicatorAnimation: destinationAnimation, + child: themedIcon, + ), + ), ), - ), + if (spacing != null) spacing, + ], ); if (extendedTransitionAnimation.value == 0) { content = Padding( @@ -643,11 +641,15 @@ class _RailDestination extends StatelessWidget { final double verticalPadding = lerpDouble(_verticalDestinationPaddingNoLabel, _verticalDestinationPaddingWithLabel, appearingAnimationValue)!; final Interval interval = selected ? const Interval(0.25, 0.75) : const Interval(0.75, 1.0); final Animation labelFadeAnimation = destinationAnimation.drive(CurveTween(curve: interval)); + final double minHeight = material3 ? 0 : minWidth; + final Widget topSpacing = SizedBox(height: material3 ? 0 : verticalPadding); + final Widget labelSpacing = SizedBox(height: material3 ? lerpDouble(0, _verticalIconLabelSpacingM3, appearingAnimationValue)! : 0); + final Widget bottomSpacing = SizedBox(height: material3 ? _verticalDestinationSpacingM3 : verticalPadding); content = Container( constraints: BoxConstraints( minWidth: minWidth, - minHeight: minWidth, + minHeight: minHeight, ), padding: padding ?? const EdgeInsets.symmetric(horizontal: _horizontalDestinationPadding), child: ClipRect( @@ -655,7 +657,7 @@ class _RailDestination extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - SizedBox(height: verticalPadding), + topSpacing, _AddIndicator( addIndicator: useIndicator, indicatorColor: indicatorColor, @@ -663,6 +665,7 @@ class _RailDestination extends StatelessWidget { indicatorAnimation: destinationAnimation, child: themedIcon, ), + labelSpacing, Align( alignment: Alignment.topCenter, heightFactor: appearingAnimationValue, @@ -673,22 +676,26 @@ class _RailDestination extends StatelessWidget { child: styledLabel, ), ), - SizedBox(height: verticalPadding), + bottomSpacing, ], ), ), ); break; case NavigationRailLabelType.all: + final double minHeight = material3 ? 0 : minWidth; + final Widget topSpacing = SizedBox(height: material3 ? 0 : _verticalDestinationPaddingWithLabel); + final Widget labelSpacing = SizedBox(height: material3 ? _verticalIconLabelSpacingM3 : 0); + final Widget bottomSpacing = SizedBox(height: material3 ? _verticalDestinationSpacingM3 : _verticalDestinationPaddingWithLabel); content = Container( constraints: BoxConstraints( minWidth: minWidth, - minHeight: minWidth, + minHeight: minHeight, ), padding: padding ?? const EdgeInsets.symmetric(horizontal: _horizontalDestinationPadding), child: Column( children: [ - const SizedBox(height: _verticalDestinationPaddingWithLabel), + topSpacing, _AddIndicator( addIndicator: useIndicator, indicatorColor: indicatorColor, @@ -696,8 +703,9 @@ class _RailDestination extends StatelessWidget { indicatorAnimation: destinationAnimation, child: themedIcon, ), + labelSpacing, styledLabel, - const SizedBox(height: _verticalDestinationPaddingWithLabel), + bottomSpacing, ], ), ); @@ -716,7 +724,7 @@ class _RailDestination extends StatelessWidget { onTap: onTap, onHover: (_) {}, highlightShape: BoxShape.rectangle, - borderRadius: BorderRadius.all(Radius.circular(minWidth / 2.0)), + borderRadius: material3 ? null : BorderRadius.all(Radius.circular(minWidth / 2.0)), containedInkWell: true, splashColor: colors.primary.withOpacity(0.12), hoverColor: colors.primary.withOpacity(0.04), @@ -772,7 +780,7 @@ class _AddIndicator extends StatelessWidget { indicator = NavigationIndicator( animation: indicatorAnimation, width: 56, - borderRadius: BorderRadius.circular(16), + shape: const StadiumBorder(), color: indicatorColor, ); } @@ -880,9 +888,107 @@ class _ExtendedNavigationRailAnimation extends InheritedWidget { bool updateShouldNotify(_ExtendedNavigationRailAnimation old) => animation != old.animation; } -const double _minRailWidth = 72.0; -const double _minExtendedRailWidth = 256.0; +// There don't appear to be tokens for these values, but they are +// shown in the spec. const double _horizontalDestinationPadding = 8.0; const double _verticalDestinationPaddingNoLabel = 24.0; const double _verticalDestinationPaddingWithLabel = 16.0; const Widget _verticalSpacer = SizedBox(height: 8.0); +const double _verticalIconLabelSpacingM3 = 4.0; +const double _verticalDestinationSpacingM3 = 12.0; + +class _DefaultsM2 extends NavigationRailThemeData { + _DefaultsM2(BuildContext context) + : _theme = Theme.of(context), + _colors = Theme.of(context).colorScheme, + super( + elevation: 0, + groupAlignment: -1, + labelType: NavigationRailLabelType.none, + useIndicator: false, + minWidth: 72.0, + minExtendedWidth: 256, + ); + + final ThemeData _theme; + final ColorScheme _colors; + + @override Color? get backgroundColor => _colors.surface; + + @override TextStyle? get unselectedLabelTextStyle { + return _theme.textTheme.bodyText1!.copyWith(color: _colors.onSurface.withOpacity(0.64)); + } + + @override TextStyle? get selectedLabelTextStyle { + return _theme.textTheme.bodyText1!.copyWith(color: _colors.primary); + } + + @override IconThemeData? get unselectedIconTheme { + return IconThemeData( + size: 24.0, + color: _colors.onSurface, + opacity: 0.64, + ); + } + + @override IconThemeData? get selectedIconTheme { + return IconThemeData( + size: 24.0, + color: _colors.primary, + opacity: 1.0, + ); + } +} + +// BEGIN GENERATED TOKEN PROPERTIES + +// Generated code to the end of this file. Do not edit by hand. +// These defaults are generated from the Material Design Token +// database by the script dev/tools/gen_defaults/bin/gen_defaults.dart. + +// Generated version v0_90 +class _TokenDefaultsM3 extends NavigationRailThemeData { + _TokenDefaultsM3(BuildContext context) + : _theme = Theme.of(context), + _colors = Theme.of(context).colorScheme, + super( + elevation: 0.0, + groupAlignment: -1, + labelType: NavigationRailLabelType.none, + useIndicator: true, + minWidth: 80.0, + minExtendedWidth: 256, + ); + + final ThemeData _theme; + final ColorScheme _colors; + + @override Color? get backgroundColor => _colors.surface; + + @override TextStyle? get unselectedLabelTextStyle { + return _theme.textTheme.labelMedium!.copyWith(color: _colors.onSurface); + } + + @override TextStyle? get selectedLabelTextStyle { + return _theme.textTheme.labelMedium!.copyWith(color: _colors.onSurface); + } + + @override IconThemeData? get unselectedIconTheme { + return IconThemeData( + size: 24.0, + color: _colors.onSurfaceVariant, + ); + } + + @override IconThemeData? get selectedIconTheme { + return IconThemeData( + size: 24.0, + color: _colors.onSecondaryContainer, + ); + } + + @override Color? get indicatorColor => _colors.secondaryContainer; + +} + +// END GENERATED TOKEN PROPERTIES diff --git a/packages/flutter/lib/src/material/navigation_rail_theme.dart b/packages/flutter/lib/src/material/navigation_rail_theme.dart index ee9f505b75e..187f95d2cb1 100644 --- a/packages/flutter/lib/src/material/navigation_rail_theme.dart +++ b/packages/flutter/lib/src/material/navigation_rail_theme.dart @@ -46,6 +46,8 @@ class NavigationRailThemeData with Diagnosticable { this.labelType, this.useIndicator, this.indicatorColor, + this.minWidth, + this.minExtendedWidth, }); /// Color to be used for the [NavigationRail]'s background. @@ -86,6 +88,14 @@ class NavigationRailThemeData with Diagnosticable { /// when [useIndicator] is true. final Color? indicatorColor; + /// Overrides the default value of [NavigationRail]'s minimum width when it + /// is not extended. + final double? minWidth; + + /// Overrides the default value of [NavigationRail]'s minimum width when it + /// is extended. + final double? minExtendedWidth; + /// Creates a copy of this object with the given fields replaced with the /// new values. NavigationRailThemeData copyWith({ @@ -99,6 +109,8 @@ class NavigationRailThemeData with Diagnosticable { NavigationRailLabelType? labelType, bool? useIndicator, Color? indicatorColor, + double? minWidth, + double? minExtendedWidth, }) { return NavigationRailThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, @@ -111,6 +123,8 @@ class NavigationRailThemeData with Diagnosticable { labelType: labelType ?? this.labelType, useIndicator: useIndicator ?? this.useIndicator, indicatorColor: indicatorColor ?? this.indicatorColor, + minWidth: minWidth ?? this.minWidth, + minExtendedWidth: minExtendedWidth ?? this.minExtendedWidth, ); } @@ -134,6 +148,9 @@ class NavigationRailThemeData with Diagnosticable { labelType: t < 0.5 ? a?.labelType : b?.labelType, useIndicator: t < 0.5 ? a?.useIndicator : b?.useIndicator, indicatorColor: Color.lerp(a?.indicatorColor, b?.indicatorColor, t), + minWidth: lerpDouble(a?.minWidth, b?.minWidth, t), + minExtendedWidth: lerpDouble(a?.minExtendedWidth, b?.minExtendedWidth, t), + ); } @@ -149,6 +166,8 @@ class NavigationRailThemeData with Diagnosticable { labelType, useIndicator, indicatorColor, + minWidth, + minExtendedWidth, ); @override @@ -167,7 +186,9 @@ class NavigationRailThemeData with Diagnosticable { && other.groupAlignment == groupAlignment && other.labelType == labelType && other.useIndicator == useIndicator - && other.indicatorColor == indicatorColor; + && other.indicatorColor == indicatorColor + && other.minWidth == minWidth + && other.minExtendedWidth == minExtendedWidth; } @override @@ -185,6 +206,8 @@ class NavigationRailThemeData with Diagnosticable { properties.add(DiagnosticsProperty('labelType', labelType, defaultValue: defaultData.labelType)); properties.add(DiagnosticsProperty('useIndicator', useIndicator, defaultValue: defaultData.useIndicator)); properties.add(ColorProperty('indicatorColor', indicatorColor, defaultValue: defaultData.indicatorColor)); + properties.add(DoubleProperty('minWidth', minWidth, defaultValue: defaultData.minWidth)); + properties.add(DoubleProperty('minExtendedWidth', minExtendedWidth, defaultValue: defaultData.minExtendedWidth)); } } diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index f5d2b0085a4..1df87e7d90f 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -1087,8 +1087,12 @@ class ThemeData with Diagnosticable { /// start using new colors, typography and other features of Material 3. /// If false, they will use the Material 2 look and feel. /// - /// If true, the default Typography will be [Typography.material2021], - /// otherwise it will default to [Typography.material2014]. + /// If a [ThemeData] is constructed with [useMaterial3] set to true, + /// the default [typography] will be [Typography.material2021], + /// otherwise it will be [Typography.material2014]. + /// + /// However, just copying a [ThemeData] with [useMaterial3] set to true will + /// not change the typography of the resulting ThemeData. /// /// During the migration to Material 3, turning this on may yield /// inconsistent look and feel in your app. Some components will be migrated @@ -1102,10 +1106,11 @@ class ThemeData with Diagnosticable { /// /// Components that have been migrated to Material 3 are: /// + /// * [AlertDialog] + /// * [Dialog] /// * [FloatingActionButton] /// * [NavigationBar] - /// * [Dialog] - /// * [AlertDialog] + /// * [NavigationRail] /// /// See also: /// diff --git a/packages/flutter/test/material/navigation_rail_test.dart b/packages/flutter/test/material/navigation_rail_test.dart index c50af2df992..4fbccf51da2 100644 --- a/packages/flutter/test/material/navigation_rail_test.dart +++ b/packages/flutter/test/material/navigation_rail_test.dart @@ -115,7 +115,7 @@ void main() { ); final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); - expect(renderBox.size.width, 72.0); + expect(renderBox.size.width, 80.0); }); testWidgets('Renders at the correct default width - [labelType]=selected', (WidgetTester tester) async { @@ -129,7 +129,7 @@ void main() { ); final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); - expect(renderBox.size.width, 72.0); + expect(renderBox.size.width, 80.0); }); testWidgets('Renders at the correct default width - [labelType]=all', (WidgetTester tester) async { @@ -143,7 +143,7 @@ void main() { ); final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); - expect(renderBox.size.width, 72.0); + expect(renderBox.size.width, 80.0); }); testWidgets('Renders wider for a destination with a long label - [labelType]=all', (WidgetTester tester) async { @@ -243,6 +243,15 @@ void main() { }); testWidgets('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { + // Padding at the top of the rail. + const double topPadding = 8.0; + // Width of a destination. + const double destinationWidth = 80.0; + // Height of a destination indicator with icon. + const double destinationHeight = 32.0; + // Space between destinations. + const double destinationPadding = 12.0; + await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -252,57 +261,56 @@ void main() { ); final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); - expect(renderBox.size.width, 72.0); + expect(renderBox.size.width, destinationWidth); - // The first destination is 8 from the top because of the default vertical - // padding at the to of the rail. - double nextDestinationY = 8.0; + // The first destination below the rail top by some padding. + double nextDestinationY = topPadding + destinationPadding / 2; final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); expect( firstIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - firstIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0, + (destinationWidth - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0, ), ), ); - // The second destination is 72 below the first destination. - nextDestinationY += 72.0; + // The second destination is one height below the first destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); expect( secondIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, ), ), ); - // The third destination is 72 below the second destination. - nextDestinationY += 72.0; + // The third destination is one height below the second destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); expect( thirdIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, ), ), ); - // The fourth destination is 72 below the third destination. - nextDestinationY += 72.0; + // The fourth destination is one height below the third destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); expect( fourthIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, ), ), ); @@ -311,6 +319,16 @@ void main() { testWidgets('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=3.0', (WidgetTester tester) async { // Since the rail is icon only, its destinations should not be affected by // textScaleFactor. + + // Padding at the top of the rail. + const double topPadding = 8.0; + // Width of a destination. + const double destinationWidth = 80.0; + // Height of a destination indicator with icon. + const double destinationHeight = 32.0; + // Space between destinations. + const double destinationPadding = 12.0; + await _pumpNavigationRail( tester, textScaleFactor: 3.0, @@ -321,57 +339,56 @@ void main() { ); final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); - expect(renderBox.size.width, 72.0); + expect(renderBox.size.width, destinationWidth); - // The first destination is 8 from the top because of the default vertical - // padding at the to of the rail. - double nextDestinationY = 8.0; + // The first destination below the rail top by some padding. + double nextDestinationY = topPadding + destinationPadding / 2; final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); expect( firstIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - firstIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0, + (destinationWidth - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0, ), ), ); - // The second destination is 72 below the first destination. - nextDestinationY += 72.0; + // The second destination is one height below the first destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); expect( secondIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, ), ), ); - // The third destination is 72 below the second destination. - nextDestinationY += 72.0; + // The third destination is one height below the second destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); expect( thirdIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, ), ), ); - // The fourth destination is 72 below the third destination. - nextDestinationY += 72.0; + // The fourth destination is one height below the third destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); expect( fourthIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, ), ), ); @@ -380,6 +397,16 @@ void main() { testWidgets('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=0.75', (WidgetTester tester) async { // Since the rail is icon only, its destinations should not be affected by // textScaleFactor. + + // Padding at the top of the rail. + const double topPadding = 8.0; + // Width of a destination. + const double destinationWidth = 80.0; + // Height of a destination indicator with icon. + const double destinationHeight = 32.0; + // Space between destinations. + const double destinationPadding = 12.0; + await _pumpNavigationRail( tester, textScaleFactor: 0.75, @@ -390,63 +417,77 @@ void main() { ); final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); - expect(renderBox.size.width, 72.0); + expect(renderBox.size.width, destinationWidth); - // The first destination is 8 from the top because of the default vertical - // padding at the to of the rail. - double nextDestinationY = 8.0; + // The first destination below the rail top by some padding. + double nextDestinationY = topPadding + destinationPadding / 2; final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); expect( firstIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - firstIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0, + (destinationWidth - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0, ), ), ); - // The second destination is 72 below the first destination. - nextDestinationY += 72.0; + // The second destination is one height below the first destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); expect( secondIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, ), ), ); - // The third destination is 72 below the second destination. - nextDestinationY += 72.0; + // The third destination is one height below the second destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); expect( thirdIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, ), ), ); - // The fourth destination is 72 below the third destination. - nextDestinationY += 72.0; + // The fourth destination is one height below the third destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); expect( fourthIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, ), ), ); }); testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { + // Padding at the top of the rail. + const double topPadding = 8.0; + // Width of a destination. + const double destinationWidth = 80.0; + // Height of a destination indicator with icon. + const double destinationHeight = 32.0; + // Space between the indicator and label. + const double destinationLabelSpacing = 4.0; + // Height of the label. + const double labelHeight = 16.0; + // Height of a destination with both icon and label. + const double destinationHeightWithLabel = destinationHeight + destinationLabelSpacing + labelHeight; + // Space between destinations. + const double destinationSpacing = 12.0; + await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -457,19 +498,18 @@ void main() { ); final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); - expect(renderBox.size.width, 72.0); + expect(renderBox.size.width, destinationWidth); - // The first destination is 8 from the top because of the default vertical - // padding at the to of the rail. - double nextDestinationY = 8.0; + // The first destination is topPadding below the rail top. + double nextDestinationY = topPadding; final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc'); expect( firstIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - firstIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - firstIconRenderBox.size.height - firstLabelRenderBox.size.height) / 2.0, + (destinationWidth - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0, ), ), ); @@ -477,53 +517,70 @@ void main() { firstLabelRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - firstLabelRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 + firstIconRenderBox.size.height - firstLabelRenderBox.size.height) / 2.0, + (destinationWidth - firstLabelRenderBox.size.width) / 2.0, + nextDestinationY + destinationHeight + destinationLabelSpacing, ), ), ); - // The second destination is 72 below the first destination. - nextDestinationY += 72.0; + // The second destination is below the first with some spacing. + nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); expect( secondIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, ), ), ); - // The third destination is 72 below the second destination. - nextDestinationY += 72.0; + // The third destination is below the second with some spacing. + nextDestinationY += destinationHeight + destinationSpacing; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); expect( thirdIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, ), ), ); - // The fourth destination is 72 below the third destination. - nextDestinationY += 72.0; + // The fourth destination is below the third with some spacing. + nextDestinationY += destinationHeight + destinationSpacing; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); expect( fourthIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, ), ), ); - }); + }, + skip: isBrowser, // https://github.com/flutter/flutter/issues/99786 + ); testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=3.0', (WidgetTester tester) async { + // Padding at the top of the rail. + const double topPadding = 8.0; + // Width of a destination. + const double destinationWidth = 126.0; + // Height of a destination indicator with icon. + const double destinationHeight = 32.0; + // Space between the indicator and label. + const double destinationLabelSpacing = 4.0; + // Height of the label. + const double labelHeight = 16.0 * 3.0; + // Height of a destination with both icon and label. + const double destinationHeightWithLabel = destinationHeight + destinationLabelSpacing + labelHeight; + // Space between destinations. + const double destinationSpacing = 12.0; + await _pumpNavigationRail( tester, textScaleFactor: 3.0, @@ -534,74 +591,90 @@ void main() { ), ); - // The rail and destinations sizes grow to fit the larger text labels. final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); - expect(renderBox.size.width, 142.0); + expect(renderBox.size.width, destinationWidth); - // The first destination is 8 from the top because of the default vertical - // padding at the to of the rail. - double nextDestinationY = 8.0; + // The first destination topPadding below the rail top. + double nextDestinationY = topPadding; final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc'); expect( firstIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (142.0 - firstIconRenderBox.size.width) / 2.0, - nextDestinationY + 16.0, + (destinationWidth - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0, ), ), ); - - // The first label sits right below the first icon. - final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc'); expect( firstLabelRenderBox.localToGlobal(Offset.zero), equals( Offset( - (142.0 - firstLabelRenderBox.size.width) / 2.0, - nextDestinationY + 16.0 + firstIconRenderBox.size.height, + (destinationWidth - firstLabelRenderBox.size.width) / 2.0, + nextDestinationY + destinationHeight + destinationLabelSpacing, ), ), ); - nextDestinationY += 16.0 + firstIconRenderBox.size.height + firstLabelRenderBox.size.height + 16.0; + // The second destination is below the first with some spacing. + nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); expect( secondIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (142.0 - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + 24.0, + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, ), ), ); - nextDestinationY += 72.0; + // The third destination is below the second with some spacing. + nextDestinationY += destinationHeight + destinationSpacing; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); expect( thirdIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (142.0 - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + 24.0, + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, ), ), ); - nextDestinationY += 72.0; + // The fourth destination is below the third with some spacing. + nextDestinationY += destinationHeight + destinationSpacing; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); expect( fourthIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (142.0 - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + 24.0, + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, ), ), ); - }); + }, + skip: isBrowser, // https://github.com/flutter/flutter/issues/99786 + ); testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=0.75', (WidgetTester tester) async { + // Padding at the top of the rail. + const double topPadding = 8.0; + // Width of a destination. + const double destinationWidth = 80.0; + // Height of a destination indicator with icon. + const double destinationHeight = 32.0; + // Space between the indicator and label. + const double destinationLabelSpacing = 4.0; + // Height of the label. + const double labelHeight = 16.0 * 0.75; + // Height of a destination with both icon and label. + const double destinationHeightWithLabel = destinationHeight + destinationLabelSpacing + labelHeight; + // Space between destinations. + const double destinationSpacing = 12.0; + await _pumpNavigationRail( tester, textScaleFactor: 0.75, @@ -612,22 +685,19 @@ void main() { ), ); - // A smaller textScaleFactor will not reduce the default width of the rail - // since there is a minWidth. final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); - expect(renderBox.size.width, 72.0); + expect(renderBox.size.width, destinationWidth); - // The first destination is 8 from the top because of the default vertical - // padding at the to of the rail. - double nextDestinationY = 8.0; + // The first destination topPadding below the rail top. + double nextDestinationY = topPadding; final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc'); expect( firstIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - firstIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - firstIconRenderBox.size.height - firstLabelRenderBox.size.height) / 2.0, + (destinationWidth - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0, ), ), ); @@ -635,51 +705,70 @@ void main() { firstLabelRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - firstLabelRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 + firstIconRenderBox.size.height - firstLabelRenderBox.size.height) / 2.0, + (destinationWidth - firstLabelRenderBox.size.width) / 2.0, + nextDestinationY + destinationHeight + destinationLabelSpacing, ), ), ); - // The second destination is 72 below the first destination. - nextDestinationY += 72.0; + // The second destination is below the first with some spacing. + nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); expect( secondIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, ), ), ); - // The third destination is 72 below the second destination. - nextDestinationY += 72.0; + // The third destination is below the second with some spacing. + nextDestinationY += destinationHeight + destinationSpacing; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); expect( thirdIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, ), ), ); - // The fourth destination is 72 below the third destination. - nextDestinationY += 72.0; + // The fourth destination is below the third with some spacing. + nextDestinationY += destinationHeight + destinationSpacing; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); expect( fourthIconRenderBox.localToGlobal(Offset.zero), - equals(Offset( - (72.0 - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, - )), + equals( + Offset( + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, + ), + ), ); - }); + }, + skip: isBrowser, // https://github.com/flutter/flutter/issues/99786 + ); testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { + // Padding at the top of the rail. + const double topPadding = 8.0; + // Width of a destination. + const double destinationWidth = 80.0; + // Height of a destination indicator with icon. + const double destinationHeight = 32.0; + // Space between the indicator and label. + const double destinationLabelSpacing = 4.0; + // Height of the label. + const double labelHeight = 16.0; + // Height of a destination with both icon and label. + const double destinationHeightWithLabel = destinationHeight + destinationLabelSpacing + labelHeight; + // Space between destinations. + const double destinationSpacing = 12.0; + await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -690,19 +779,18 @@ void main() { ); final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); - expect(renderBox.size.width, 72.0); + expect(renderBox.size.width, destinationWidth); - // The first destination is 8 from the top because of the default vertical - // padding at the to of the rail. - double nextDestinationY = 8.0; + // The first destination topPadding below the rail top. + double nextDestinationY = topPadding; final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc'); expect( firstIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - firstIconRenderBox.size.width) / 2.0, - nextDestinationY + 16.0, + (destinationWidth - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0, ), ), ); @@ -710,83 +798,70 @@ void main() { firstLabelRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - firstLabelRenderBox.size.width) / 2.0, - nextDestinationY + 16.0 + firstIconRenderBox.size.height, + (destinationWidth - firstLabelRenderBox.size.width) / 2.0, + nextDestinationY + destinationHeight + destinationLabelSpacing, ), ), ); - // The second destination is 72 below the first destination. - nextDestinationY += 72.0; + // The second destination is below the first with some spacing. + nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); - final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def'); expect( secondIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + 16.0, - ), - ), - ); - expect( - secondLabelRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (72.0 - secondLabelRenderBox.size.width) / 2.0, - nextDestinationY + 16.0 + secondIconRenderBox.size.height, + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, ), ), ); - // The third destination is 72 below the second destination. - nextDestinationY += 72.0; + // The third destination is below the second with some spacing. + nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); - final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi'); expect( thirdIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + 16.0, - ), - ), - ); - expect( - thirdLabelRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (72.0 - thirdLabelRenderBox.size.width) / 2.0, - nextDestinationY + 16.0 + thirdIconRenderBox.size.height, + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, ), ), ); - // The fourth destination is 72 below the third destination. - nextDestinationY += 72.0; + // The fourth destination is below the third with some spacing. + nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); - final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl'); expect( fourthIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + 16.0, + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, ), ), ); - expect( - fourthLabelRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (72.0 - fourthLabelRenderBox.size.width) / 2.0, - nextDestinationY + 16.0 + fourthIconRenderBox.size.height, - ), - ), - ); - }); + }, + skip: isBrowser, // https://github.com/flutter/flutter/issues/99786 + ); testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=3.0', (WidgetTester tester) async { + // Padding at the top of the rail. + const double topPadding = 8.0; + // Width of a destination. + const double destinationWidth = 126.0; + // Height of a destination indicator with icon. + const double destinationHeight = 32.0; + // Space between the indicator and label. + const double destinationLabelSpacing = 4.0; + // Height of the label. + const double labelHeight = 16.0 * 3.0; + // Height of a destination with both icon and label. + const double destinationHeightWithLabel = destinationHeight + destinationLabelSpacing + labelHeight; + // Space between destinations. + const double destinationSpacing = 12.0; + await _pumpNavigationRail( tester, textScaleFactor: 3.0, @@ -797,21 +872,19 @@ void main() { ), ); - // The rail and destinations sizes grow to fit the larger text labels. final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); - expect(renderBox.size.width, 142.0); + expect(renderBox.size.width, destinationWidth); - // The first destination is 8 from the top because of the default vertical - // padding at the to of the rail. - double nextDestinationY = 8.0; + // The first destination topPadding below the rail top. + double nextDestinationY = topPadding; final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc'); expect( firstIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (142.0 - firstIconRenderBox.size.width) / 2.0, - nextDestinationY + 16.0, + (destinationWidth - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0, ), ), ); @@ -819,80 +892,70 @@ void main() { firstLabelRenderBox.localToGlobal(Offset.zero), equals( Offset( - (142.0 - firstLabelRenderBox.size.width) / 2.0, - nextDestinationY + 16.0 + firstIconRenderBox.size.height, + (destinationWidth - firstLabelRenderBox.size.width) / 2.0, + nextDestinationY + destinationHeight + destinationLabelSpacing, ), ), ); - nextDestinationY += 16.0 + firstIconRenderBox.size.height + firstLabelRenderBox.size.height + 16.0; + // The second destination is below the first with some spacing. + nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); - final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def'); expect( secondIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (142.0 - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + 16.0, - ), - ), - ); - expect( - secondLabelRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (142.0 - secondLabelRenderBox.size.width) / 2.0, - nextDestinationY + 16.0 + secondIconRenderBox.size.height, + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, ), ), ); - nextDestinationY += 16.0 + secondIconRenderBox.size.height + secondLabelRenderBox.size.height + 16.0; + // The third destination is below the second with some spacing. + nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); - final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi'); expect( thirdIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (142.0 - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + 16.0, - ), - ), - ); - expect( - thirdLabelRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (142.0 - thirdLabelRenderBox.size.width) / 2.0, - nextDestinationY + 16.0 + thirdIconRenderBox.size.height, + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, ), ), ); - nextDestinationY += 16.0 + thirdIconRenderBox.size.height + thirdLabelRenderBox.size.height + 16.0; + // The fourth destination is below the third with some spacing. + nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); - final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl'); expect( fourthIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (142.0 - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + 16.0, + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, ), ), ); - expect( - fourthLabelRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (142.0 - fourthLabelRenderBox.size.width) / 2.0, - nextDestinationY + 16.0 + fourthIconRenderBox.size.height, - ), - ), - ); - }); + }, + skip: isBrowser, // https://github.com/flutter/flutter/issues/99786 + ); testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=0.75', (WidgetTester tester) async { + // Padding at the top of the rail. + const double topPadding = 8.0; + // Width of a destination. + const double destinationWidth = 80.0; + // Height of a destination indicator with icon. + const double destinationHeight = 32.0; + // Space between the indicator and label. + const double destinationLabelSpacing = 4.0; + // Height of the label. + const double labelHeight = 16.0 * 0.75; + // Height of a destination with both icon and label. + const double destinationHeightWithLabel = destinationHeight + destinationLabelSpacing + labelHeight; + // Space between destinations. + const double destinationSpacing = 12.0; + await _pumpNavigationRail( tester, textScaleFactor: 0.75, @@ -903,21 +966,19 @@ void main() { ), ); - // A smaller textScaleFactor will not reduce the default size of the rail. final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); - expect(renderBox.size.width, 72.0); + expect(renderBox.size.width, destinationWidth); - // The first destination is 8 from the top because of the default vertical - // padding at the to of the rail. - double nextDestinationY = 8.0; + // The first destination topPadding below the rail top. + double nextDestinationY = topPadding; final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc'); expect( firstIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - firstIconRenderBox.size.width) / 2.0, - nextDestinationY + 16.0, + (destinationWidth - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0, ), ), ); @@ -925,83 +986,64 @@ void main() { firstLabelRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - firstLabelRenderBox.size.width) / 2.0, - nextDestinationY + 16.0 + firstIconRenderBox.size.height, + (destinationWidth - firstLabelRenderBox.size.width) / 2.0, + nextDestinationY + destinationHeight + destinationLabelSpacing, ), ), ); - // The second destination is 72 below the first destination. - nextDestinationY += 72.0; + // The second destination is below the first with some spacing. + nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); - final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def'); expect( secondIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + 16.0, - ), - ), - ); - expect( - secondLabelRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (72.0 - secondLabelRenderBox.size.width) / 2.0, - nextDestinationY + 16.0 + secondIconRenderBox.size.height, + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, ), ), ); - // The third destination is 72 below the second destination. - nextDestinationY += 72.0; + // The third destination is below the second with some spacing. + nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); - final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi'); expect( thirdIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + 16.0, - ), - ), - ); - expect( - thirdLabelRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (72.0 - thirdLabelRenderBox.size.width) / 2.0, - nextDestinationY + 16.0 + thirdIconRenderBox.size.height, + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, ), ), ); - // The fourth destination is 72 below the third destination. - nextDestinationY += 72.0; + // The fourth destination is below the third with some spacing. + nextDestinationY += destinationHeightWithLabel + destinationSpacing; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); - final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl'); expect( fourthIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + 16.0, + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, ), ), ); - expect( - fourthLabelRenderBox.localToGlobal(Offset.zero), - equals( - Offset( - (72.0 - fourthLabelRenderBox.size.width) / 2.0, - nextDestinationY + 16.0 + fourthIconRenderBox.size.height, - ), - ), - ); - }); + }, + skip: isBrowser, // https://github.com/flutter/flutter/issues/99786 + ); testWidgets('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { + // Padding at the top of the rail. + const double topPadding = 8.0; + // Width of a destination. + const double compactWidth = 56.0; + // Height of a destination indicator with icon. + const double destinationHeight = 32.0; + // Space between destinations. + const double destinationSpacing = 12.0; + await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -1014,61 +1056,69 @@ void main() { final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); expect(renderBox.size.width, 56.0); - // The first destination is 8 from the top because of the default vertical - // padding at the to of the rail. - double nextDestinationY = 8.0; + // The first destination below the rail top by some padding. + double nextDestinationY = topPadding + destinationSpacing / 2; final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); expect( firstIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (56.0 - firstIconRenderBox.size.width) / 2.0, - nextDestinationY + (56.0 - firstIconRenderBox.size.height) / 2.0, + (compactWidth - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0, ), ), ); - // The second destination is 56 below the first destination. - nextDestinationY += 56.0; + // The second destination is row below the first destination. + nextDestinationY += destinationHeight + destinationSpacing; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); expect( secondIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (56.0 - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (56.0 - secondIconRenderBox.size.height) / 2.0, + (compactWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, ), ), ); - // The third destination is 56 below the second destination. - nextDestinationY += 56.0; + // The third destination is a row below the second destination. + nextDestinationY += destinationHeight + destinationSpacing; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); expect( thirdIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (56.0 - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (56.0 - thirdIconRenderBox.size.height) / 2.0, + (compactWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, ), ), ); - // The fourth destination is 56 below the third destination. - nextDestinationY += 56.0; + // The fourth destination is a row below the third destination. + nextDestinationY += destinationHeight + destinationSpacing; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); expect( fourthIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (56.0 - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (56.0 - fourthIconRenderBox.size.height) / 2.0, + (compactWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, ), ), ); }); testWidgets('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=3.0', (WidgetTester tester) async { + // Padding at the top of the rail. + const double topPadding = 8.0; + // Width of a destination. + const double compactWidth = 56.0; + // Height of a destination indicator with icon. + const double destinationHeight = 32.0; + // Space between destinations. + const double destinationSpacing = 12.0; + await _pumpNavigationRail( tester, textScaleFactor: 3.0, @@ -1082,66 +1132,74 @@ void main() { // Since the rail is icon only, its preferred width should not be affected // by textScaleFactor. final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); - expect(renderBox.size.width, 56.0); + expect(renderBox.size.width, compactWidth); - // The first destination is 8 from the top because of the default vertical - // padding at the to of the rail. - double nextDestinationY = 8.0; + // The first destination below the rail top by some padding. + double nextDestinationY = topPadding + destinationSpacing / 2; final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); expect( firstIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (56.0 - firstIconRenderBox.size.width) / 2.0, - nextDestinationY + (56.0 - firstIconRenderBox.size.height) / 2.0, + (compactWidth - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0, ), ), ); - // The second destination is 56 below the first destination. - nextDestinationY += 56.0; + // The second destination is row below the first destination. + nextDestinationY += destinationHeight + destinationSpacing; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); expect( secondIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (56.0 - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (56.0 - secondIconRenderBox.size.height) / 2.0, + (compactWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, ), ), ); - // The third destination is 56 below the second destination. - nextDestinationY += 56.0; + // The third destination is a row below the second destination. + nextDestinationY += destinationHeight + destinationSpacing; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); expect( thirdIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (56.0 - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (56.0 - thirdIconRenderBox.size.height) / 2.0, + (compactWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, ), ), ); - // The fourth destination is 56 below the third destination. - nextDestinationY += 56.0; + // The fourth destination is a row below the third destination. + nextDestinationY += destinationHeight + destinationSpacing; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); expect( fourthIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (56.0 - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (56.0 - fourthIconRenderBox.size.height) / 2.0, + (compactWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, ), ), ); }); testWidgets('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=0.75', (WidgetTester tester) async { + // Padding at the top of the rail. + const double topPadding = 8.0; + // Width of a destination. + const double compactWidth = 56.0; + // Height of a destination indicator with icon. + const double destinationHeight = 32.0; + // Space between destinations. + const double destinationSpacing = 12.0; + await _pumpNavigationRail( tester, - textScaleFactor: 3.0, + textScaleFactor: 0.75, navigationRail: NavigationRail( selectedIndex: 0, minWidth: 56.0, @@ -1152,63 +1210,71 @@ void main() { // Since the rail is icon only, its preferred width should not be affected // by textScaleFactor. final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); - expect(renderBox.size.width, 56.0); + expect(renderBox.size.width, compactWidth); - // The first destination is 8 from the top because of the default vertical - // padding at the to of the rail. - double nextDestinationY = 8.0; + // The first destination below the rail top by some padding. + double nextDestinationY = topPadding + destinationSpacing / 2; final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); expect( firstIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (56.0 - firstIconRenderBox.size.width) / 2.0, - nextDestinationY + (56.0 - firstIconRenderBox.size.height) / 2.0, + (compactWidth - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0, ), ), ); - // The second destination is 56 below the first destination. - nextDestinationY += 56.0; + // The second destination is row below the first destination. + nextDestinationY += destinationHeight + destinationSpacing; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); expect( secondIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (56.0 - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (56.0 - secondIconRenderBox.size.height) / 2.0, + (compactWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, ), ), ); - // The third destination is 56 below the second destination. - nextDestinationY += 56.0; + // The third destination is a row below the second destination. + nextDestinationY += destinationHeight + destinationSpacing; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); expect( thirdIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (56.0 - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (56.0 - thirdIconRenderBox.size.height) / 2.0, + (compactWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, ), ), ); - // The fourth destination is 56 below the third destination. - nextDestinationY += 56.0; + // The fourth destination is a row below the third destination. + nextDestinationY += destinationHeight + destinationSpacing; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); expect( fourthIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (56.0 - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (56.0 - fourthIconRenderBox.size.height) / 2.0, + (compactWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, ), ), ); }); testWidgets('Group alignment works - [groupAlignment]=-1.0 (default)', (WidgetTester tester) async { + // Padding at the top of the rail. + const double topPadding = 8.0; + // Width of a destination. + const double destinationWidth = 80.0; + // Height of a destination indicator with icon. + const double destinationHeight = 32.0; + // Space between destinations. + const double destinationPadding = 12.0; + await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -1217,61 +1283,72 @@ void main() { ), ); - // The first destination is 8 from the top because of the default vertical - // padding at the to of the rail. - double nextDestinationY = 8.0; + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, destinationWidth); + + // The first destination below the rail top by some padding. + double nextDestinationY = topPadding + destinationPadding / 2; final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); expect( firstIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - firstIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0, + (destinationWidth - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0, ), ), ); - // The second destination is 72 below the first destination. - nextDestinationY += 72.0; + // The second destination is one height below the first destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); expect( secondIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, ), ), ); - // The third destination is 72 below the second destination. - nextDestinationY += 72.0; + // The third destination is one height below the second destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); expect( thirdIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, ), ), ); - // The fourth destination is 72 below the third destination. - nextDestinationY += 72.0; + // The fourth destination is one height below the third destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); expect( fourthIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, ), ), ); }); testWidgets('Group alignment works - [groupAlignment]=0.0', (WidgetTester tester) async { + // Padding at the top of the rail. + const double topPadding = 8.0; + // Width of a destination. + const double destinationWidth = 80.0; + // Height of a destination indicator with icon. + const double destinationHeight = 32.0; + // Space between destinations. + const double destinationPadding = 12.0; + await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -1281,59 +1358,72 @@ void main() { ), ); - double nextDestinationY = 160.0; + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, destinationWidth); + + // The first destination below the rail top by some padding with an offset for the alignment. + double nextDestinationY = topPadding + destinationPadding / 2 + 208; final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); expect( firstIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - firstIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0, + (destinationWidth - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0, ), ), ); - // The second destination is 72 below the first destination. - nextDestinationY += 72.0; + // The second destination is one height below the first destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); expect( secondIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, ), ), ); - // The third destination is 72 below the second destination. - nextDestinationY += 72.0; + // The third destination is one height below the second destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); expect( thirdIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, ), ), ); - // The fourth destination is 72 below the third destination. - nextDestinationY += 72.0; + // The fourth destination is one height below the third destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); expect( fourthIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, ), ), ); }); testWidgets('Group alignment works - [groupAlignment]=1.0', (WidgetTester tester) async { + // Padding at the top of the rail. + const double topPadding = 8.0; + // Width of a destination. + const double destinationWidth = 80.0; + // Height of a destination indicator with icon. + const double destinationHeight = 32.0; + // Space between destinations. + const double destinationPadding = 12.0; + await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -1343,53 +1433,57 @@ void main() { ), ); - double nextDestinationY = 312.0; + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, destinationWidth); + + // The first destination below the rail top by some padding with an offset for the alignment. + double nextDestinationY = topPadding + destinationPadding / 2 + 416; final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); expect( firstIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - firstIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0, + (destinationWidth - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0, ), ), ); - // The second destination is 72 below the first destination. - nextDestinationY += 72.0; + // The second destination is one height below the first destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); expect( secondIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, ), ), ); - // The third destination is 72 below the second destination. - nextDestinationY += 72.0; + // The third destination is one height below the second destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); expect( thirdIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, ), ), ); - // The fourth destination is 72 below the third destination. - nextDestinationY += 72.0; + // The fourth destination is one height below the third destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); expect( fourthIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, ), ), ); @@ -1408,16 +1502,26 @@ void main() { final RenderBox leading = tester.renderObject(find.byType(FloatingActionButton).at(0)); final RenderBox trailing = tester.renderObject(find.byType(FloatingActionButton).at(1)); - expect(leading.localToGlobal(Offset.zero), const Offset(0.0, 8.0)); - expect(trailing.localToGlobal(Offset.zero), const Offset(0.0, 360.0)); + expect(leading.localToGlobal(Offset.zero), Offset((80 - leading.size.width) / 2, 8.0)); + expect(trailing.localToGlobal(Offset.zero), Offset((80 - trailing.size.width) / 2, 248.0)); }); testWidgets('Extended rail animates the width and labels appear - [textDirection]=LTR', (WidgetTester tester) async { + // Padding at the top of the rail. + const double topPadding = 8.0; + // Width of a destination. + const double destinationWidth = 80.0; + // Height of a destination indicator with icon. + const double destinationHeight = 32.0; + // Space between destinations. + const double destinationPadding = 12.0; + bool extended = false; late StateSetter stateSetter; await tester.pumpWidget( MaterialApp( + theme: ThemeData(useMaterial3: true), home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { stateSetter = setState; @@ -1442,7 +1546,7 @@ void main() { final RenderBox rail = tester.firstRenderObject(find.byType(NavigationRail)); - expect(rail.size.width, equals(72.0)); + expect(rail.size.width, destinationWidth); stateSetter(() { extended = true; @@ -1450,22 +1554,21 @@ void main() { await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); - expect(rail.size.width, equals(164.0)); + expect(rail.size.width, equals(168.0)); await tester.pumpAndSettle(); expect(rail.size.width, equals(256.0)); - // The first destination is 8 from the top because of the default vertical - // padding at the to of the rail. - double nextDestinationY = 8.0; + // The first destination below the rail top by some padding. + double nextDestinationY = topPadding + destinationPadding / 2; final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc'); expect( firstIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - firstIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0, + (destinationWidth - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0, ), ), ); @@ -1473,22 +1576,22 @@ void main() { firstLabelRenderBox.localToGlobal(Offset.zero), equals( Offset( - 72.0, - nextDestinationY + (72.0 - firstLabelRenderBox.size.height) / 2.0, + destinationWidth, + nextDestinationY + (destinationHeight - firstLabelRenderBox.size.height) / 2.0, ), ), ); - // The second destination is 72 below the first destination. - nextDestinationY += 72.0; + // The second destination is one height below the first destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def'); expect( secondIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + (destinationWidth - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, ), ), ); @@ -1496,22 +1599,22 @@ void main() { secondLabelRenderBox.localToGlobal(Offset.zero), equals( Offset( - 72.0, - nextDestinationY + (72.0 - secondLabelRenderBox.size.height) / 2.0, + destinationWidth, + nextDestinationY + (destinationHeight - secondLabelRenderBox.size.height) / 2.0, ), ), ); - // The third destination is 72 below the second destination. - nextDestinationY += 72.0; + // The third destination is one height below the second destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi'); expect( thirdIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + (destinationWidth - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, ), ), ); @@ -1519,22 +1622,22 @@ void main() { thirdLabelRenderBox.localToGlobal(Offset.zero), equals( Offset( - 72.0, - nextDestinationY + (72.0 - thirdLabelRenderBox.size.height) / 2.0, + destinationWidth, + nextDestinationY + (destinationHeight - thirdLabelRenderBox.size.height) / 2.0, ), ), ); - // The fourth destination is 72 below the third destination. - nextDestinationY += 72.0; + // The fourth destination is one height below the third destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl'); expect( fourthIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - (72.0 - fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + (destinationWidth - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, ), ), ); @@ -1542,19 +1645,29 @@ void main() { fourthLabelRenderBox.localToGlobal(Offset.zero), equals( Offset( - 72.0, - nextDestinationY + (72.0 - fourthLabelRenderBox.size.height) / 2.0, + destinationWidth, + nextDestinationY + (destinationHeight - fourthLabelRenderBox.size.height) / 2.0, ), ), ); }); testWidgets('Extended rail animates the width and labels appear - [textDirection]=RTL', (WidgetTester tester) async { + // Padding at the top of the rail. + const double topPadding = 8.0; + // Width of a destination. + const double destinationWidth = 80.0; + // Height of a destination indicator with icon. + const double destinationHeight = 32.0; + // Space between destinations. + const double destinationPadding = 12.0; + bool extended = false; late StateSetter stateSetter; await tester.pumpWidget( MaterialApp( + theme: ThemeData(useMaterial3: true), home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { stateSetter = setState; @@ -1583,8 +1696,8 @@ void main() { final RenderBox rail = tester.firstRenderObject(find.byType(NavigationRail)); - expect(rail.size.width, equals(72.0)); - expect(rail.localToGlobal(Offset.zero), equals(const Offset(728.0, 0.0))); + expect(rail.size.width, equals(destinationWidth)); + expect(rail.localToGlobal(Offset.zero), equals(const Offset(720.0, 0.0))); stateSetter(() { extended = true; @@ -1592,24 +1705,23 @@ void main() { await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); - expect(rail.size.width, equals(164.0)); - expect(rail.localToGlobal(Offset.zero), equals(const Offset(636.0, 0.0))); + expect(rail.size.width, equals(168.0)); + expect(rail.localToGlobal(Offset.zero), equals(const Offset(632.0, 0.0))); await tester.pumpAndSettle(); expect(rail.size.width, equals(256.0)); expect(rail.localToGlobal(Offset.zero), equals(const Offset(544.0, 0.0))); - // The first destination is 8 from the top because of the default vertical - // padding at the to of the rail. - double nextDestinationY = 8.0; + // The first destination below the rail top by some padding. + double nextDestinationY = topPadding + destinationPadding / 2; final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc'); expect( firstIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - 800.0 - (72.0 + firstIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0, + 800.0 - (destinationWidth + firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - firstIconRenderBox.size.height) / 2.0, ), ), ); @@ -1617,22 +1729,22 @@ void main() { firstLabelRenderBox.localToGlobal(Offset.zero), equals( Offset( - 800.0 - 72.0 - firstLabelRenderBox.size.width, - nextDestinationY + (72.0 - firstLabelRenderBox.size.height) / 2.0, + 800.0 - destinationWidth - firstLabelRenderBox.size.width, + nextDestinationY + (destinationHeight - firstLabelRenderBox.size.height) / 2.0, ), ), ); - // The second destination is 72 below the first destination. - nextDestinationY += 72.0; + // The second destination is one height below the first destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def'); expect( secondIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - 800.0 - (72.0 + secondIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + 800.0 - (destinationWidth + secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - secondIconRenderBox.size.height) / 2.0, ), ), ); @@ -1640,22 +1752,22 @@ void main() { secondLabelRenderBox.localToGlobal(Offset.zero), equals( Offset( - 800.0 - 72.0 - secondLabelRenderBox.size.width, - nextDestinationY + (72.0 - secondLabelRenderBox.size.height) / 2.0, + 800.0 - destinationWidth - secondLabelRenderBox.size.width, + nextDestinationY + (destinationHeight - secondLabelRenderBox.size.height) / 2.0, ), ), ); - // The third destination is 72 below the second destination. - nextDestinationY += 72.0; + // The third destination is one height below the second destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi'); expect( thirdIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - 800.0 - (72.0 + thirdIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + 800.0 - (destinationWidth + thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - thirdIconRenderBox.size.height) / 2.0, ), ), ); @@ -1663,22 +1775,22 @@ void main() { thirdLabelRenderBox.localToGlobal(Offset.zero), equals( Offset( - 800.0 - 72.0 - thirdLabelRenderBox.size.width, - nextDestinationY + (72.0 - thirdLabelRenderBox.size.height) / 2.0, + 800.0 - destinationWidth - thirdLabelRenderBox.size.width, + nextDestinationY + (destinationHeight - thirdLabelRenderBox.size.height) / 2.0, ), ), ); - // The fourth destination is 72 below the third destination. - nextDestinationY += 72.0; + // The fourth destination is one height below the third destination. + nextDestinationY += destinationHeight + destinationPadding; final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl'); expect( fourthIconRenderBox.localToGlobal(Offset.zero), equals( Offset( - 800.0 - (72.0 + fourthIconRenderBox.size.width) / 2.0, - nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + 800 - (destinationWidth + fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (destinationHeight - fourthIconRenderBox.size.height) / 2.0, ), ), ); @@ -1686,8 +1798,8 @@ void main() { fourthLabelRenderBox.localToGlobal(Offset.zero), equals( Offset( - 800.0 - 72.0 - fourthLabelRenderBox.size.width, - nextDestinationY + (72.0 - fourthLabelRenderBox.size.height) / 2.0, + 800.0 - destinationWidth - fourthLabelRenderBox.size.width, + nextDestinationY + (destinationHeight - fourthLabelRenderBox.size.height) / 2.0, ), ), ); @@ -1699,6 +1811,7 @@ void main() { await tester.pumpWidget( MaterialApp( + theme: ThemeData(useMaterial3: true), home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { stateSetter = setState; @@ -1737,7 +1850,7 @@ void main() { final RenderBox rail = tester.firstRenderObject(find.byType(NavigationRail)); - expect(rail.size.width, equals(72.0)); + expect(rail.size.width, equals(80.0)); stateSetter(() { extended = true; @@ -1745,10 +1858,10 @@ void main() { await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); - expect(rail.size.width, equals(328.0)); + expect(rail.size.width, equals(303.0)); await tester.pumpAndSettle(); - expect(rail.size.width, equals(584.0)); + expect(rail.size.width, equals(526.0)); }); testWidgets('Extended rail final width can be changed', (WidgetTester tester) async { @@ -1757,6 +1870,7 @@ void main() { await tester.pumpWidget( MaterialApp( + theme: ThemeData(useMaterial3: true), home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { stateSetter = setState; @@ -1782,7 +1896,7 @@ void main() { final RenderBox rail = tester.firstRenderObject(find.byType(NavigationRail)); - expect(rail.size.width, equals(72.0)); + expect(rail.size.width, equals(80.0)); stateSetter(() { extended = true; @@ -1799,6 +1913,7 @@ void main() { await tester.pumpWidget( MaterialApp( + theme: ThemeData(useMaterial3: true), home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { stateSetter = setState; @@ -1834,8 +1949,8 @@ void main() { final Finder rail = find.byType(NavigationRail); - // Before starting the animation, the rail has a width of 72. - expect(tester.getSize(rail).width, 72.0); + // Before starting the animation, the rail has a width of 80. + expect(tester.getSize(rail).width, 80.0); stateSetter(() { extended = true; @@ -1845,10 +1960,10 @@ void main() { // Create very close to 0, but non-zero, animation value. await tester.pump(const Duration(milliseconds: 1)); // Expect that it has started to extend. - expect(tester.getSize(rail).width, greaterThan(72.0)); + expect(tester.getSize(rail).width, greaterThan(80.0)); // Expect that it has only extended by a small amount, or that the first // frame does not jump. This helps verify that it is a smooth animation. - expect(tester.getSize(rail).width, closeTo(72.0, 1.0)); + expect(tester.getSize(rail).width, closeTo(80.0, 1.0)); }); testWidgets('Extended rail animation can be consumed', (WidgetTester tester) async { @@ -2164,7 +2279,6 @@ void main() { testWidgets('NavigationRailDestination adds indicator by default when ThemeData.useMaterial3 is true', (WidgetTester tester) async { await _pumpNavigationRail( tester, - theme: ThemeData(useMaterial3: true), navigationRail: NavigationRail( labelType: NavigationRailLabelType.selected, selectedIndex: 0, @@ -2251,7 +2365,7 @@ void main() { expect(find.byType(NavigationIndicator), findsNothing); }); - testWidgets('NavigationRailDestination adds circular indicator when no labels are present', (WidgetTester tester) async { + testWidgets('NavigationRailDestination adds an oval indicator when no labels are present', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -2281,10 +2395,10 @@ void main() { final NavigationIndicator indicator = tester.widget(find.byType(NavigationIndicator).first); expect(indicator.width, 56); - expect(indicator.height, 56); + expect(indicator.height, 32); }); - testWidgets('NavigationRailDestination adds circular indicator when selected labels are present', (WidgetTester tester) async { + testWidgets('NavigationRailDestination adds an oval indicator when selected labels are present', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -2317,7 +2431,7 @@ void main() { expect(indicator.height, 32); }); - testWidgets('NavigationRailDestination adds circular indicator when all labels are present', (WidgetTester tester) async { + testWidgets('NavigationRailDestination adds an oval indicator when all labels are present', (WidgetTester tester) async { await _pumpNavigationRail( tester, navigationRail: NavigationRail( @@ -2388,11 +2502,1768 @@ void main() { ); // Indicator with Stack widget final RenderBox firstIndicator = tester.renderObject(find.byType(Icon).first); - expect(firstIndicator.localToGlobal(Offset.zero).dx, 24.0); + expect(firstIndicator.localToGlobal(Offset.zero).dx, 28.0); // Indicator without Stack widget final RenderBox lastIndicator = tester.renderObject(find.byType(Icon).last); - expect(lastIndicator.localToGlobal(Offset.zero).dx, 24.0); + expect(lastIndicator.localToGlobal(Offset.zero).dx, 28.0); }); + + group('Material 2', () { + // Original Material 2 tests. Remove this group after `useMaterial3` has been deprecated. + testWidgets('Renders at the correct default width - [labelType]=none (default)', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + navigationRail: NavigationRail( + selectedIndex: 0, + destinations: _destinations(), + ), + ); + + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, 72.0); + }); + + testWidgets('Renders at the correct default width - [labelType]=selected', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + navigationRail: NavigationRail( + selectedIndex: 0, + labelType: NavigationRailLabelType.selected, + destinations: _destinations(), + ), + ); + + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, 72.0); + }); + + testWidgets('Renders at the correct default width - [labelType]=all', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + navigationRail: NavigationRail( + selectedIndex: 0, + labelType: NavigationRailLabelType.all, + destinations: _destinations(), + ), + ); + + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, 72.0); + }); + + testWidgets('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + navigationRail: NavigationRail( + selectedIndex: 0, + destinations: _destinations(), + ), + ); + + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, 72.0); + + // The first destination is 8 from the top because of the default vertical + // padding at the to of the rail. + double nextDestinationY = 8.0; + final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + expect( + firstIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The second destination is 72 below the first destination. + nextDestinationY += 72.0; + final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The third destination is 72 below the second destination. + nextDestinationY += 72.0; + final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The fourth destination is 72 below the third destination. + nextDestinationY += 72.0; + final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + ), + ), + ); + }); + + testWidgets('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=3.0', (WidgetTester tester) async { + // Since the rail is icon only, its destinations should not be affected by + // textScaleFactor. + await _pumpNavigationRail( + tester, + useMaterial3: false, + textScaleFactor: 3.0, + navigationRail: NavigationRail( + selectedIndex: 0, + destinations: _destinations(), + ), + ); + + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, 72.0); + + // The first destination is 8 from the top because of the default vertical + // padding at the to of the rail. + double nextDestinationY = 8.0; + final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + expect( + firstIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The second destination is 72 below the first destination. + nextDestinationY += 72.0; + final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The third destination is 72 below the second destination. + nextDestinationY += 72.0; + final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The fourth destination is 72 below the third destination. + nextDestinationY += 72.0; + final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + ), + ), + ); + }); + + testWidgets('Destination spacing is correct - [labelType]=none (default), [textScaleFactor]=0.75', (WidgetTester tester) async { + // Since the rail is icon only, its destinations should not be affected by + // textScaleFactor. + await _pumpNavigationRail( + tester, + useMaterial3: false, + textScaleFactor: 0.75, + navigationRail: NavigationRail( + selectedIndex: 0, + destinations: _destinations(), + ), + ); + + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, 72.0); + + // The first destination is 8 from the top because of the default vertical + // padding at the to of the rail. + double nextDestinationY = 8.0; + final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + expect( + firstIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The second destination is 72 below the first destination. + nextDestinationY += 72.0; + final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The third destination is 72 below the second destination. + nextDestinationY += 72.0; + final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The fourth destination is 72 below the third destination. + nextDestinationY += 72.0; + final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + ), + ), + ); + }); + + testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + navigationRail: NavigationRail( + selectedIndex: 0, + destinations: _destinations(), + labelType: NavigationRailLabelType.selected, + ), + ); + + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, 72.0); + + // The first destination is 8 from the top because of the default vertical + // padding at the to of the rail. + double nextDestinationY = 8.0; + final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc'); + expect( + firstIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - firstIconRenderBox.size.height - firstLabelRenderBox.size.height) / 2.0, + ), + ), + ); + expect( + firstLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - firstLabelRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 + firstIconRenderBox.size.height - firstLabelRenderBox.size.height) / 2.0, + ), + ), + ); + + // The second destination is 72 below the first destination. + nextDestinationY += 72.0; + final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The third destination is 72 below the second destination. + nextDestinationY += 72.0; + final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The fourth destination is 72 below the third destination. + nextDestinationY += 72.0; + final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + ), + ), + ); + }); + + testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=3.0', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + textScaleFactor: 3.0, + navigationRail: NavigationRail( + selectedIndex: 0, + destinations: _destinations(), + labelType: NavigationRailLabelType.selected, + ), + ); + + // The rail and destinations sizes grow to fit the larger text labels. + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, 142.0); + + // The first destination is 8 from the top because of the default vertical + // padding at the to of the rail. + double nextDestinationY = 8.0; + final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + expect( + firstIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (142.0 - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + 16.0, + ), + ), + ); + + // The first label sits right below the first icon. + final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc'); + expect( + firstLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (142.0 - firstLabelRenderBox.size.width) / 2.0, + nextDestinationY + 16.0 + firstIconRenderBox.size.height, + ), + ), + ); + + nextDestinationY += 16.0 + firstIconRenderBox.size.height + firstLabelRenderBox.size.height + 16.0; + final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (142.0 - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + 24.0, + ), + ), + ); + + nextDestinationY += 72.0; + final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (142.0 - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + 24.0, + ), + ), + ); + + nextDestinationY += 72.0; + final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (142.0 - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + 24.0, + ), + ), + ); + }); + + testWidgets('Destination spacing is correct - [labelType]=selected, [textScaleFactor]=0.75', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + textScaleFactor: 0.75, + navigationRail: NavigationRail( + selectedIndex: 0, + destinations: _destinations(), + labelType: NavigationRailLabelType.selected, + ), + ); + + // A smaller textScaleFactor will not reduce the default width of the rail + // since there is a minWidth. + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, 72.0); + + // The first destination is 8 from the top because of the default vertical + // padding at the to of the rail. + double nextDestinationY = 8.0; + final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc'); + expect( + firstIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - firstIconRenderBox.size.height - firstLabelRenderBox.size.height) / 2.0, + ), + ), + ); + expect( + firstLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - firstLabelRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 + firstIconRenderBox.size.height - firstLabelRenderBox.size.height) / 2.0, + ), + ), + ); + + // The second destination is 72 below the first destination. + nextDestinationY += 72.0; + final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The third destination is 72 below the second destination. + nextDestinationY += 72.0; + final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The fourth destination is 72 below the third destination. + nextDestinationY += 72.0; + final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals(Offset( + (72.0 - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + )), + ); + }); + + testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + navigationRail: NavigationRail( + selectedIndex: 0, + destinations: _destinations(), + labelType: NavigationRailLabelType.all, + ), + ); + + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, 72.0); + + // The first destination is 8 from the top because of the default vertical + // padding at the to of the rail. + double nextDestinationY = 8.0; + final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc'); + expect( + firstIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + 16.0, + ), + ), + ); + expect( + firstLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - firstLabelRenderBox.size.width) / 2.0, + nextDestinationY + 16.0 + firstIconRenderBox.size.height, + ), + ), + ); + + // The second destination is 72 below the first destination. + nextDestinationY += 72.0; + final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); + final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def'); + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + 16.0, + ), + ), + ); + expect( + secondLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - secondLabelRenderBox.size.width) / 2.0, + nextDestinationY + 16.0 + secondIconRenderBox.size.height, + ), + ), + ); + + // The third destination is 72 below the second destination. + nextDestinationY += 72.0; + final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); + final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi'); + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + 16.0, + ), + ), + ); + expect( + thirdLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - thirdLabelRenderBox.size.width) / 2.0, + nextDestinationY + 16.0 + thirdIconRenderBox.size.height, + ), + ), + ); + + // The fourth destination is 72 below the third destination. + nextDestinationY += 72.0; + final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); + final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl'); + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + 16.0, + ), + ), + ); + expect( + fourthLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - fourthLabelRenderBox.size.width) / 2.0, + nextDestinationY + 16.0 + fourthIconRenderBox.size.height, + ), + ), + ); + }); + + testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=3.0', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + textScaleFactor: 3.0, + navigationRail: NavigationRail( + selectedIndex: 0, + destinations: _destinations(), + labelType: NavigationRailLabelType.all, + ), + ); + + // The rail and destinations sizes grow to fit the larger text labels. + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, 142.0); + + // The first destination is 8 from the top because of the default vertical + // padding at the to of the rail. + double nextDestinationY = 8.0; + final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc'); + expect( + firstIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (142.0 - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + 16.0, + ), + ), + ); + expect( + firstLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (142.0 - firstLabelRenderBox.size.width) / 2.0, + nextDestinationY + 16.0 + firstIconRenderBox.size.height, + ), + ), + ); + + nextDestinationY += 16.0 + firstIconRenderBox.size.height + firstLabelRenderBox.size.height + 16.0; + final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); + final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def'); + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (142.0 - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + 16.0, + ), + ), + ); + expect( + secondLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (142.0 - secondLabelRenderBox.size.width) / 2.0, + nextDestinationY + 16.0 + secondIconRenderBox.size.height, + ), + ), + ); + + nextDestinationY += 16.0 + secondIconRenderBox.size.height + secondLabelRenderBox.size.height + 16.0; + final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); + final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi'); + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (142.0 - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + 16.0, + ), + ), + ); + expect( + thirdLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (142.0 - thirdLabelRenderBox.size.width) / 2.0, + nextDestinationY + 16.0 + thirdIconRenderBox.size.height, + ), + ), + ); + + nextDestinationY += 16.0 + thirdIconRenderBox.size.height + thirdLabelRenderBox.size.height + 16.0; + final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); + final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl'); + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (142.0 - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + 16.0, + ), + ), + ); + expect( + fourthLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (142.0 - fourthLabelRenderBox.size.width) / 2.0, + nextDestinationY + 16.0 + fourthIconRenderBox.size.height, + ), + ), + ); + }); + + testWidgets('Destination spacing is correct - [labelType]=all, [textScaleFactor]=0.75', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + textScaleFactor: 0.75, + navigationRail: NavigationRail( + selectedIndex: 0, + destinations: _destinations(), + labelType: NavigationRailLabelType.all, + ), + ); + + // A smaller textScaleFactor will not reduce the default size of the rail. + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, 72.0); + + // The first destination is 8 from the top because of the default vertical + // padding at the to of the rail. + double nextDestinationY = 8.0; + final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc'); + expect( + firstIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + 16.0, + ), + ), + ); + expect( + firstLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - firstLabelRenderBox.size.width) / 2.0, + nextDestinationY + 16.0 + firstIconRenderBox.size.height, + ), + ), + ); + + // The second destination is 72 below the first destination. + nextDestinationY += 72.0; + final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); + final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def'); + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + 16.0, + ), + ), + ); + expect( + secondLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - secondLabelRenderBox.size.width) / 2.0, + nextDestinationY + 16.0 + secondIconRenderBox.size.height, + ), + ), + ); + + // The third destination is 72 below the second destination. + nextDestinationY += 72.0; + final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); + final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi'); + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + 16.0, + ), + ), + ); + expect( + thirdLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - thirdLabelRenderBox.size.width) / 2.0, + nextDestinationY + 16.0 + thirdIconRenderBox.size.height, + ), + ), + ); + + // The fourth destination is 72 below the third destination. + nextDestinationY += 72.0; + final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); + final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl'); + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + 16.0, + ), + ), + ); + expect( + fourthLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - fourthLabelRenderBox.size.width) / 2.0, + nextDestinationY + 16.0 + fourthIconRenderBox.size.height, + ), + ), + ); + }); + + testWidgets('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=1.0 (default)', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + navigationRail: NavigationRail( + selectedIndex: 0, + minWidth: 56.0, + destinations: _destinations(), + ), + ); + + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, 56.0); + + // The first destination is 8 from the top because of the default vertical + // padding at the to of the rail. + double nextDestinationY = 8.0; + final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + expect( + firstIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (56.0 - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (56.0 - firstIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The second destination is 56 below the first destination. + nextDestinationY += 56.0; + final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (56.0 - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (56.0 - secondIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The third destination is 56 below the second destination. + nextDestinationY += 56.0; + final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (56.0 - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (56.0 - thirdIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The fourth destination is 56 below the third destination. + nextDestinationY += 56.0; + final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (56.0 - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (56.0 - fourthIconRenderBox.size.height) / 2.0, + ), + ), + ); + }); + + testWidgets('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=3.0', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + textScaleFactor: 3.0, + navigationRail: NavigationRail( + selectedIndex: 0, + minWidth: 56.0, + destinations: _destinations(), + ), + ); + + // Since the rail is icon only, its preferred width should not be affected + // by textScaleFactor. + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, 56.0); + + // The first destination is 8 from the top because of the default vertical + // padding at the to of the rail. + double nextDestinationY = 8.0; + final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + expect( + firstIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (56.0 - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (56.0 - firstIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The second destination is 56 below the first destination. + nextDestinationY += 56.0; + final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (56.0 - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (56.0 - secondIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The third destination is 56 below the second destination. + nextDestinationY += 56.0; + final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (56.0 - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (56.0 - thirdIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The fourth destination is 56 below the third destination. + nextDestinationY += 56.0; + final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (56.0 - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (56.0 - fourthIconRenderBox.size.height) / 2.0, + ), + ), + ); + }); + + testWidgets('Destination spacing is correct for a compact rail - [preferredWidth]=56, [textScaleFactor]=0.75', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + textScaleFactor: 3.0, + navigationRail: NavigationRail( + selectedIndex: 0, + minWidth: 56.0, + destinations: _destinations(), + ), + ); + + // Since the rail is icon only, its preferred width should not be affected + // by textScaleFactor. + final RenderBox renderBox = tester.renderObject(find.byType(NavigationRail)); + expect(renderBox.size.width, 56.0); + + // The first destination is 8 from the top because of the default vertical + // padding at the to of the rail. + double nextDestinationY = 8.0; + final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + expect( + firstIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (56.0 - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (56.0 - firstIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The second destination is 56 below the first destination. + nextDestinationY += 56.0; + final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (56.0 - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (56.0 - secondIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The third destination is 56 below the second destination. + nextDestinationY += 56.0; + final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (56.0 - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (56.0 - thirdIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The fourth destination is 56 below the third destination. + nextDestinationY += 56.0; + final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (56.0 - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (56.0 - fourthIconRenderBox.size.height) / 2.0, + ), + ), + ); + }); + + testWidgets('Group alignment works - [groupAlignment]=-1.0 (default)', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + navigationRail: NavigationRail( + selectedIndex: 0, + destinations: _destinations(), + ), + ); + + // The first destination is 8 from the top because of the default vertical + // padding at the to of the rail. + double nextDestinationY = 8.0; + final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + expect( + firstIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The second destination is 72 below the first destination. + nextDestinationY += 72.0; + final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The third destination is 72 below the second destination. + nextDestinationY += 72.0; + final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The fourth destination is 72 below the third destination. + nextDestinationY += 72.0; + final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + ), + ), + ); + }); + + testWidgets('Group alignment works - [groupAlignment]=0.0', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + navigationRail: NavigationRail( + selectedIndex: 0, + groupAlignment: 0.0, + destinations: _destinations(), + ), + ); + + double nextDestinationY = 160.0; + final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + expect( + firstIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The second destination is 72 below the first destination. + nextDestinationY += 72.0; + final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The third destination is 72 below the second destination. + nextDestinationY += 72.0; + final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The fourth destination is 72 below the third destination. + nextDestinationY += 72.0; + final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + ), + ), + ); + }); + + testWidgets('Group alignment works - [groupAlignment]=1.0', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + navigationRail: NavigationRail( + selectedIndex: 0, + groupAlignment: 1.0, + destinations: _destinations(), + ), + ); + + double nextDestinationY = 312.0; + final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + expect( + firstIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The second destination is 72 below the first destination. + nextDestinationY += 72.0; + final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The third destination is 72 below the second destination. + nextDestinationY += 72.0; + final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + ), + ), + ); + + // The fourth destination is 72 below the third destination. + nextDestinationY += 72.0; + final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + ), + ), + ); + }); + + testWidgets('Leading and trailing appear in the correct places', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + navigationRail: NavigationRail( + selectedIndex: 0, + leading: FloatingActionButton(onPressed: () { }), + trailing: FloatingActionButton(onPressed: () { }), + destinations: _destinations(), + ), + ); + + final RenderBox leading = tester.renderObject(find.byType(FloatingActionButton).at(0)); + final RenderBox trailing = tester.renderObject(find.byType(FloatingActionButton).at(1)); + expect(leading.localToGlobal(Offset.zero), Offset((72 - leading.size.width) / 2.0, 8.0)); + expect(trailing.localToGlobal(Offset.zero), Offset((72 - trailing.size.width) / 2.0, 360.0)); + }); + + testWidgets('Extended rail animates the width and labels appear - [textDirection]=LTR', (WidgetTester tester) async { + bool extended = false; + late StateSetter stateSetter; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: false), + home: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + stateSetter = setState; + return Scaffold( + body: Row( + children: [ + NavigationRail( + selectedIndex: 0, + destinations: _destinations(), + extended: extended, + ), + const Expanded( + child: Text('body'), + ), + ], + ), + ); + }, + ), + ), + ); + + final RenderBox rail = tester.firstRenderObject(find.byType(NavigationRail)); + + expect(rail.size.width, equals(72.0)); + + stateSetter(() { + extended = true; + }); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + expect(rail.size.width, equals(164.0)); + + await tester.pumpAndSettle(); + expect(rail.size.width, equals(256.0)); + + // The first destination is 8 from the top because of the default vertical + // padding at the to of the rail. + double nextDestinationY = 8.0; + final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc'); + expect( + firstIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0, + ), + ), + ); + expect( + firstLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + 72.0, + nextDestinationY + (72.0 - firstLabelRenderBox.size.height) / 2.0, + ), + ), + ); + + // The second destination is 72 below the first destination. + nextDestinationY += 72.0; + final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); + final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def'); + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + ), + ), + ); + expect( + secondLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + 72.0, + nextDestinationY + (72.0 - secondLabelRenderBox.size.height) / 2.0, + ), + ), + ); + + // The third destination is 72 below the second destination. + nextDestinationY += 72.0; + final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); + final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi'); + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + ), + ), + ); + expect( + thirdLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + 72.0, + nextDestinationY + (72.0 - thirdLabelRenderBox.size.height) / 2.0, + ), + ), + ); + + // The fourth destination is 72 below the third destination. + nextDestinationY += 72.0; + final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); + final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl'); + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + (72.0 - fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + ), + ), + ); + expect( + fourthLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + 72.0, + nextDestinationY + (72.0 - fourthLabelRenderBox.size.height) / 2.0, + ), + ), + ); + }); + + testWidgets('Extended rail animates the width and labels appear - [textDirection]=RTL', (WidgetTester tester) async { + bool extended = false; + late StateSetter stateSetter; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: false), + home: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + stateSetter = setState; + return Directionality( + textDirection: TextDirection.rtl, + child: Scaffold( + body: Row( + textDirection: TextDirection.rtl, + children: [ + NavigationRail( + selectedIndex: 0, + destinations: _destinations(), + extended: extended, + ), + const Expanded( + child: Text('body'), + ), + ], + ), + ), + ); + }, + ), + ), + ); + + final RenderBox rail = tester.firstRenderObject(find.byType(NavigationRail)); + + expect(rail.size.width, equals(72.0)); + expect(rail.localToGlobal(Offset.zero), equals(const Offset(728.0, 0.0))); + + stateSetter(() { + extended = true; + }); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + expect(rail.size.width, equals(164.0)); + expect(rail.localToGlobal(Offset.zero), equals(const Offset(636.0, 0.0))); + + await tester.pumpAndSettle(); + expect(rail.size.width, equals(256.0)); + expect(rail.localToGlobal(Offset.zero), equals(const Offset(544.0, 0.0))); + + // The first destination is 8 from the top because of the default vertical + // padding at the to of the rail. + double nextDestinationY = 8.0; + final RenderBox firstIconRenderBox = _iconRenderBox(tester, Icons.favorite); + final RenderBox firstLabelRenderBox = _labelRenderBox(tester, 'Abc'); + expect( + firstIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + 800.0 - (72.0 + firstIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - firstIconRenderBox.size.height) / 2.0, + ), + ), + ); + expect( + firstLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + 800.0 - 72.0 - firstLabelRenderBox.size.width, + nextDestinationY + (72.0 - firstLabelRenderBox.size.height) / 2.0, + ), + ), + ); + + // The second destination is 72 below the first destination. + nextDestinationY += 72.0; + final RenderBox secondIconRenderBox = _iconRenderBox(tester, Icons.bookmark_border); + final RenderBox secondLabelRenderBox = _labelRenderBox(tester, 'Def'); + expect( + secondIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + 800.0 - (72.0 + secondIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - secondIconRenderBox.size.height) / 2.0, + ), + ), + ); + expect( + secondLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + 800.0 - 72.0 - secondLabelRenderBox.size.width, + nextDestinationY + (72.0 - secondLabelRenderBox.size.height) / 2.0, + ), + ), + ); + + // The third destination is 72 below the second destination. + nextDestinationY += 72.0; + final RenderBox thirdIconRenderBox = _iconRenderBox(tester, Icons.star_border); + final RenderBox thirdLabelRenderBox = _labelRenderBox(tester, 'Ghi'); + expect( + thirdIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + 800.0 - (72.0 + thirdIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - thirdIconRenderBox.size.height) / 2.0, + ), + ), + ); + expect( + thirdLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + 800.0 - 72.0 - thirdLabelRenderBox.size.width, + nextDestinationY + (72.0 - thirdLabelRenderBox.size.height) / 2.0, + ), + ), + ); + + // The fourth destination is 72 below the third destination. + nextDestinationY += 72.0; + final RenderBox fourthIconRenderBox = _iconRenderBox(tester, Icons.hotel); + final RenderBox fourthLabelRenderBox = _labelRenderBox(tester, 'Jkl'); + expect( + fourthIconRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + 800.0 - (72.0 + fourthIconRenderBox.size.width) / 2.0, + nextDestinationY + (72.0 - fourthIconRenderBox.size.height) / 2.0, + ), + ), + ); + expect( + fourthLabelRenderBox.localToGlobal(Offset.zero), + equals( + Offset( + 800.0 - 72.0 - fourthLabelRenderBox.size.width, + nextDestinationY + (72.0 - fourthLabelRenderBox.size.height) / 2.0, + ), + ), + ); + }); + + testWidgets('Extended rail gets wider with longer labels are larger text scale', (WidgetTester tester) async { + bool extended = false; + late StateSetter stateSetter; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: false), + home: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + stateSetter = setState; + return Scaffold( + body: Row( + children: [ + MediaQuery( + data: MediaQuery.of(context).copyWith(textScaleFactor: 3.0), + child: NavigationRail( + selectedIndex: 0, + destinations: const [ + NavigationRailDestination( + icon: Icon(Icons.favorite_border), + selectedIcon: Icon(Icons.favorite), + label: Text('Abc'), + ), + NavigationRailDestination( + icon: Icon(Icons.bookmark_border), + selectedIcon: Icon(Icons.bookmark), + label: Text('Longer Label'), + ), + ], + extended: extended, + ), + ), + const Expanded( + child: Text('body'), + ), + ], + ), + ); + }, + ), + ), + ); + + final RenderBox rail = tester.firstRenderObject(find.byType(NavigationRail)); + + expect(rail.size.width, equals(72.0)); + + stateSetter(() { + extended = true; + }); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + expect(rail.size.width, equals(328.0)); + + await tester.pumpAndSettle(); + expect(rail.size.width, equals(584.0)); + }); + + testWidgets('Extended rail final width can be changed', (WidgetTester tester) async { + bool extended = false; + late StateSetter stateSetter; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: false), + home: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + stateSetter = setState; + return Scaffold( + body: Row( + children: [ + NavigationRail( + selectedIndex: 0, + minExtendedWidth: 300, + destinations: _destinations(), + extended: extended, + ), + const Expanded( + child: Text('body'), + ), + ], + ), + ); + }, + ), + ), + ); + + final RenderBox rail = tester.firstRenderObject(find.byType(NavigationRail)); + + expect(rail.size.width, equals(72.0)); + + stateSetter(() { + extended = true; + }); + + await tester.pumpAndSettle(); + expect(rail.size.width, equals(300.0)); + }); + + /// Regression test for https://github.com/flutter/flutter/issues/65657 + testWidgets('Extended rail transition does not jump from the beginning', (WidgetTester tester) async { + bool extended = false; + late StateSetter stateSetter; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: false), + home: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + stateSetter = setState; + return Scaffold( + body: Row( + children: [ + NavigationRail( + selectedIndex: 0, + destinations: const [ + NavigationRailDestination( + icon: Icon(Icons.favorite_border), + selectedIcon: Icon(Icons.favorite), + label: Text('Abc'), + ), + NavigationRailDestination( + icon: Icon(Icons.bookmark_border), + selectedIcon: Icon(Icons.bookmark), + label: Text('Longer Label'), + ), + ], + extended: extended, + ), + const Expanded( + child: Text('body'), + ), + ], + ), + ); + }, + ), + ), + ); + + final Finder rail = find.byType(NavigationRail); + + // Before starting the animation, the rail has a width of 72. + expect(tester.getSize(rail).width, 72.0); + + stateSetter(() { + extended = true; + }); + + await tester.pump(); + // Create very close to 0, but non-zero, animation value. + await tester.pump(const Duration(milliseconds: 1)); + // Expect that it has started to extend. + expect(tester.getSize(rail).width, greaterThan(72.0)); + // Expect that it has only extended by a small amount, or that the first + // frame does not jump. This helps verify that it is a smooth animation. + expect(tester.getSize(rail).width, closeTo(72.0, 1.0)); + }); + + testWidgets('NavigationRailDestination adds circular indicator when no labels are present', (WidgetTester tester) async { + await _pumpNavigationRail( + tester, + useMaterial3: false, + navigationRail: NavigationRail( + useIndicator: true, + labelType: NavigationRailLabelType.none, + selectedIndex: 0, + destinations: const [ + NavigationRailDestination( + icon: Icon(Icons.favorite_border), + selectedIcon: Icon(Icons.favorite), + label: Text('Abc'), + ), + NavigationRailDestination( + icon: Icon(Icons.bookmark_border), + selectedIcon: Icon(Icons.bookmark), + label: Text('Def'), + ), + NavigationRailDestination( + icon: Icon(Icons.star_border), + selectedIcon: Icon(Icons.star), + label: Text('Ghi'), + ), + ], + ), + ); + + final NavigationIndicator indicator = tester.widget(find.byType(NavigationIndicator).first); + + expect(indicator.width, 56); + expect(indicator.height, 56); + }); + + testWidgets('NavigationRailDestination has center aligned indicator - [labelType]=none', (WidgetTester tester) async { + // This is a regression test for + // https://github.com/flutter/flutter/issues/97753 + await _pumpNavigationRail( + tester, + useMaterial3: false, + navigationRail: NavigationRail( + labelType: NavigationRailLabelType.none, + selectedIndex: 0, + destinations: [ + NavigationRailDestination( + icon: Stack( + children: const [ + Icon(Icons.umbrella), + Positioned( + top: 0, + right: 0, + child: Text( + 'Text', + style: TextStyle(fontSize: 10, color: Colors.red), + ), + ), + ], + ), + label: const Text('Abc'), + ), + const NavigationRailDestination( + icon: Icon(Icons.umbrella), + label: Text('Def'), + ), + const NavigationRailDestination( + icon: Icon(Icons.bookmark_border), + label: Text('Ghi'), + ), + ], + ), + ); + // Indicator with Stack widget + final RenderBox firstIndicator = tester.renderObject(find.byType(Icon).first); + expect(firstIndicator.localToGlobal(Offset.zero).dx, 24.0); + // Indicator without Stack widget + final RenderBox lastIndicator = tester.renderObject(find.byType(Icon).last); + expect(lastIndicator.localToGlobal(Offset.zero).dx, 24.0); + }); + + }); // End Material 2 group } TestSemantics _expectedSemantics() { @@ -2476,11 +4347,11 @@ Future _pumpNavigationRail( WidgetTester tester, { double textScaleFactor = 1.0, required NavigationRail navigationRail, - ThemeData? theme, + bool useMaterial3 = true, }) async { await tester.pumpWidget( MaterialApp( - theme: theme, + theme: ThemeData(useMaterial3: useMaterial3), home: Builder( builder: (BuildContext context) { return MediaQuery( diff --git a/packages/flutter/test/material/navigation_rail_theme_test.dart b/packages/flutter/test/material/navigation_rail_theme_test.dart index f9dc4772079..fe76502123b 100644 --- a/packages/flutter/test/material/navigation_rail_theme_test.dart +++ b/packages/flutter/test/material/navigation_rail_theme_test.dart @@ -13,8 +13,10 @@ void main() { }); testWidgets('Default values are used when no NavigationRail or NavigationRailThemeData properties are specified', (WidgetTester tester) async { + // Material 3 defaults await tester.pumpWidget( MaterialApp( + theme: ThemeData.light().copyWith(useMaterial3: true), home: Scaffold( body: NavigationRail( selectedIndex: 0, @@ -26,6 +28,37 @@ void main() { expect(_railMaterial(tester).color, ThemeData().colorScheme.surface); expect(_railMaterial(tester).elevation, 0); + expect(_destinationSize(tester).width, 80.0); + expect(_selectedIconTheme(tester).size, 24.0); + expect(_selectedIconTheme(tester).color, ThemeData().colorScheme.onSecondaryContainer); + expect(_selectedIconTheme(tester).opacity, null); + expect(_unselectedIconTheme(tester).size, 24.0); + expect(_unselectedIconTheme(tester).color, ThemeData().colorScheme.onSurface); + expect(_unselectedIconTheme(tester).opacity, null); + expect(_selectedLabelStyle(tester).fontSize, 14.0); + expect(_unselectedLabelStyle(tester).fontSize, 14.0); + expect(_destinationsAlign(tester).alignment, Alignment.topCenter); + expect(_labelType(tester), NavigationRailLabelType.none); + expect(find.byType(NavigationIndicator), findsWidgets); + }); + + testWidgets('Default values are used when no NavigationRail or NavigationRailThemeData properties are specified (Material 2)', (WidgetTester tester) async { + // This test can be removed when `useMaterial3` is deprecated. + await tester.pumpWidget( + MaterialApp( + theme: ThemeData.light().copyWith(useMaterial3: false), + home: Scaffold( + body: NavigationRail( + selectedIndex: 0, + destinations: _destinations(), + ), + ), + ), + ); + + expect(_railMaterial(tester).color, ThemeData().colorScheme.surface); + expect(_railMaterial(tester).elevation, 0); + expect(_destinationSize(tester).width, 72.0); expect(_selectedIconTheme(tester).size, 24.0); expect(_selectedIconTheme(tester).color, ThemeData().colorScheme.primary); expect(_selectedIconTheme(tester).opacity, 1.0); @@ -308,6 +341,16 @@ TextStyle _unselectedLabelStyle(WidgetTester tester) { ).text.style!; } +Size _destinationSize(WidgetTester tester) { + return tester.getSize( + find.ancestor( + of: find.byIcon(Icons.favorite), + matching: find.byType(Material), + ) + .first + ); +} + Align _destinationsAlign(WidgetTester tester) { // The first Expanded widget is the one within the main Column for the rail // content.