Skip to content

Commit 17fa784

Browse files
Copilotbdlukaa
andauthored
fix(NavigationView): prevent spurious overlay flash when auto mode resizes from minimal → compact (#1316)
* Initial plan * fix: NavigationView auto mode no longer shows brief overlay on minimal→compact resize Root cause: _OpenNavigationPane's AnimatedContainer shared _panelKey with _CompactNavigationPane. When transitioning from minimal to compact mode, Flutter's GlobalKey mechanism reused the AnimatedContainer state (width=320) causing a spurious 320→50 width animation visible as a brief overlay. Fix: - Add usePanelKey parameter to _OpenNavigationPane (default true) - Pass usePanelKey: false in _buildMinimalView to prevent state reuse - Reset _minimalPaneOpen in _resolveDisplayMode when leaving minimal mode Also: add tests and update CHANGELOG Co-authored-by: bdlukaa <45696119+bdlukaa@users.noreply.github.com> Agent-Logs-Url: https://github.com/bdlukaa/fluent_ui/sessions/a98cf501-1708-4d0d-aa16-0b6a2cc5ec59 * chore: update changelog --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bdlukaa <45696119+bdlukaa@users.noreply.github.com> Co-authored-by: Bruno D'Luka <brunodlukaa@gmail.com>
1 parent 5ac7a6d commit 17fa784

4 files changed

Lines changed: 189 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## 4.15.0
22

3+
- fix: `NavigationView` auto display mode no longer shows a brief overlay when resizing from minimal to compact mode ([#1316](https://github.com/bdlukaa/fluent_ui/pull/1316))
34
- fix: `MenuFlyout` no longer throws `TypeError` on sub-items ([#1337](https://github.com/bdlukaa/fluent_ui/issues/1337))
45
- fix: `MenuFlyout` sub-item tree now correctly expands to the left and shows a `chevron_left` icon when right-to-left directionality is enabled ([#1342](https://github.com/bdlukaa/fluent_ui/issues/1342))
56
- feat: Controls now respond to `VisualDensity` from `FluentThemeData` for compact sizing. Use `FluentThemeData(visualDensity: VisualDensity.compact)` to enable compact mode ([#1175](https://github.com/bdlukaa/fluent_ui/issues/1175))

lib/src/controls/navigation/navigation_view/pane.dart

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1117,6 +1117,7 @@ class _OpenNavigationPane extends StatefulWidget {
11171117
this.onItemSelected,
11181118
this.initiallyOpen = false,
11191119
this.onAnimationEnd,
1120+
this.usePanelKey = true,
11201121
}) : super(key: pane.key);
11211122

11221123
final NavigationPane pane;
@@ -1125,6 +1126,16 @@ class _OpenNavigationPane extends StatefulWidget {
11251126
final bool initiallyOpen;
11261127
final VoidCallback? onAnimationEnd;
11271128

1129+
/// Whether to use the shared [NavigationViewState._panelKey] for the
1130+
/// internal [AnimatedContainer].
1131+
///
1132+
/// When `true` (the default), the [AnimatedContainer]'s state is shared
1133+
/// across compact and expanded modes, enabling the smooth width animation
1134+
/// between them. Set to `false` in minimal mode to prevent the
1135+
/// [AnimatedContainer] state from being reused when transitioning to compact
1136+
/// mode, which would otherwise cause a spurious width animation.
1137+
final bool usePanelKey;
1138+
11281139
@override
11291140
State<_OpenNavigationPane> createState() => _OpenNavigationPaneState();
11301141
}
@@ -1161,7 +1172,7 @@ class _OpenNavigationPaneState extends State<_OpenNavigationPane> {
11611172
return AnimatedContainer(
11621173
duration: theme.animationDuration ?? Duration.zero,
11631174
curve: theme.animationCurve ?? Curves.linear,
1164-
key: view._panelKey,
1175+
key: widget.usePanelKey ? view._panelKey : null,
11651176
width: paneWidth,
11661177
onEnd: widget.onAnimationEnd,
11671178
child: LayoutBuilder(

lib/src/controls/navigation/navigation_view/view.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,11 @@ class NavigationViewState extends State<NavigationView> {
549549

550550
if (autoDisplayMode != _displayMode) {
551551
widget.onDisplayModeChanged?.call(autoDisplayMode);
552+
// Reset minimal pane open state when leaving minimal mode so that
553+
// re-entering minimal mode starts with the pane closed.
554+
if (_displayMode == PaneDisplayMode.minimal) {
555+
_minimalPaneOpen = false;
556+
}
552557
}
553558

554559
_displayMode = autoDisplayMode;
@@ -967,6 +972,7 @@ class NavigationViewState extends State<NavigationView> {
967972
child: _OpenNavigationPane(
968973
theme: theme,
969974
pane: pane,
975+
usePanelKey: false,
970976
onItemSelected: () {
971977
if (_displayMode == PaneDisplayMode.minimal) {
972978
isMinimalPaneOpen = false;

test/navigation_view_test.dart

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,4 +1022,174 @@ void main() {
10221022
expect(flyoutBox.size.width, greaterThan(kCompactNavigationPaneWidth));
10231023
});
10241024
});
1025+
1026+
// Tests for auto display mode transition
1027+
group('Auto display mode transition', () {
1028+
testWidgets(
1029+
'No overlay when resizing from minimal to compact',
1030+
(tester) async {
1031+
// Start in minimal mode (width <= 640)
1032+
double viewWidth = 300;
1033+
1034+
await tester.pumpWidget(
1035+
FluentApp(
1036+
home: StatefulBuilder(
1037+
builder: (context, setState) {
1038+
return SizedBox(
1039+
width: viewWidth,
1040+
height: 800,
1041+
child: NavigationView(
1042+
pane: NavigationPane(
1043+
selected: 0,
1044+
displayMode: PaneDisplayMode.auto,
1045+
items: [
1046+
PaneItem(
1047+
icon: const Icon(FluentIcons.home),
1048+
title: const Text('Home'),
1049+
body: const SizedBox(),
1050+
),
1051+
],
1052+
),
1053+
),
1054+
);
1055+
},
1056+
),
1057+
),
1058+
);
1059+
1060+
await tester.pumpAndSettle();
1061+
expect(find.byType(NavigationView), findsOneWidget);
1062+
1063+
// Resize to compact mode (641–1007px)
1064+
viewWidth = 800;
1065+
await tester.pumpWidget(
1066+
FluentApp(
1067+
home: StatefulBuilder(
1068+
builder: (context, setState) {
1069+
return SizedBox(
1070+
width: viewWidth,
1071+
height: 800,
1072+
child: NavigationView(
1073+
pane: NavigationPane(
1074+
selected: 0,
1075+
displayMode: PaneDisplayMode.auto,
1076+
items: [
1077+
PaneItem(
1078+
icon: const Icon(FluentIcons.home),
1079+
title: const Text('Home'),
1080+
body: const SizedBox(),
1081+
),
1082+
],
1083+
),
1084+
),
1085+
);
1086+
},
1087+
),
1088+
),
1089+
);
1090+
1091+
// After a single frame (not pumpAndSettle), verify the compact pane
1092+
// is at compact width – not at open-pane width as it would be if the
1093+
// AnimatedContainer state were incorrectly reused from minimal mode.
1094+
await tester.pump();
1095+
1096+
expect(find.byType(NavigationView), findsOneWidget);
1097+
1098+
// The render box of the NavigationView should not show the full-width
1099+
// open pane (320 px). The pane should appear at compact width (50 px)
1100+
// immediately, with no ongoing width animation.
1101+
final navViewBox = tester.renderObject<RenderBox>(
1102+
find.byType(NavigationView),
1103+
);
1104+
expect(navViewBox.size.width, 800);
1105+
1106+
// Settle any remaining animations and verify no errors occur.
1107+
await tester.pumpAndSettle();
1108+
expect(find.byType(NavigationView), findsOneWidget);
1109+
},
1110+
);
1111+
1112+
testWidgets(
1113+
'Minimal pane open state is reset when transitioning to compact',
1114+
(tester) async {
1115+
final navKey = GlobalKey<NavigationViewState>();
1116+
double viewWidth = 300;
1117+
1118+
await tester.pumpWidget(
1119+
FluentApp(
1120+
home: StatefulBuilder(
1121+
builder: (context, setState) {
1122+
return SizedBox(
1123+
width: viewWidth,
1124+
height: 800,
1125+
child: NavigationView(
1126+
key: navKey,
1127+
pane: NavigationPane(
1128+
selected: 0,
1129+
displayMode: PaneDisplayMode.auto,
1130+
items: [
1131+
PaneItem(
1132+
icon: const Icon(FluentIcons.home),
1133+
title: const Text('Home'),
1134+
body: const SizedBox(),
1135+
),
1136+
],
1137+
),
1138+
),
1139+
);
1140+
},
1141+
),
1142+
),
1143+
);
1144+
1145+
await tester.pumpAndSettle();
1146+
1147+
// Verify we are in minimal mode.
1148+
expect(navKey.currentState?.displayMode, PaneDisplayMode.minimal);
1149+
1150+
// Open the minimal pane.
1151+
navKey.currentState?.isMinimalPaneOpen = true;
1152+
await tester.pumpAndSettle();
1153+
expect(navKey.currentState?.isMinimalPaneOpen, true);
1154+
1155+
// Resize to compact mode.
1156+
viewWidth = 800;
1157+
await tester.pumpWidget(
1158+
FluentApp(
1159+
home: StatefulBuilder(
1160+
builder: (context, setState) {
1161+
return SizedBox(
1162+
width: viewWidth,
1163+
height: 800,
1164+
child: NavigationView(
1165+
key: navKey,
1166+
pane: NavigationPane(
1167+
selected: 0,
1168+
displayMode: PaneDisplayMode.auto,
1169+
items: [
1170+
PaneItem(
1171+
icon: const Icon(FluentIcons.home),
1172+
title: const Text('Home'),
1173+
body: const SizedBox(),
1174+
),
1175+
],
1176+
),
1177+
),
1178+
);
1179+
},
1180+
),
1181+
),
1182+
);
1183+
1184+
await tester.pumpAndSettle();
1185+
1186+
// Display mode should now be compact.
1187+
expect(navKey.currentState?.displayMode, PaneDisplayMode.compact);
1188+
1189+
// The minimal-pane-open flag must have been reset during the
1190+
// mode transition so it doesn't leak into the compact mode.
1191+
expect(navKey.currentState?.isMinimalPaneOpen, false);
1192+
},
1193+
);
1194+
});
10251195
}

0 commit comments

Comments
 (0)