diff --git a/CHANGELOG.md b/CHANGELOG.md index b360deb23..e89e562ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ -## 4.17.0 +## [next] + - refactor: Flutter 3.47.1 support - refactor: migrate Material UI imports to the standalone `material_ui` package - refactor: migrate Cupertino UI imports to the standalone `cupertino_ui` package - refactor: update localization delegates to use `GlobalMaterialLocalizations.delegates` with the new standalone UI packages +- fix: skip hidden `TitleBar` children when collecting semantics ([#1354](https://github.com/bdlukaa/fluent_ui/pull/1354)) ## 4.16.1 diff --git a/lib/src/controls/navigation/navigation_view/title_bar.dart b/lib/src/controls/navigation/navigation_view/title_bar.dart index 45689613d..6a0277186 100644 --- a/lib/src/controls/navigation/navigation_view/title_bar.dart +++ b/lib/src/controls/navigation/navigation_view/title_bar.dart @@ -561,6 +561,25 @@ class _RenderTitleSubtitleOverflow extends RenderBox child = childAfter(child); } } + + /// Skips hidden children when collecting semantics. + /// + /// Hidden children are never laid out (see [performLayout]). If a semantics + /// walk visits a hidden child before it is laid out, Flutter asserts on + /// `_needsLayout`. Minimizing a maximized window with a screen reader + /// running triggers the assert. + @override + void visitChildrenForSemantics(RenderObjectVisitor visitor) { + var child = firstChild; + while (child != null) { + final childParentData = + child.parentData! as _TitleSubtitleOverflowParentData; + if (!childParentData.isHidden) { + visitor(child); + } + child = childAfter(child); + } + } } /// The preferred position for the pane toggle button. diff --git a/test/title_bar_test.dart b/test/title_bar_test.dart index 9d8a80cc9..09e148c80 100644 --- a/test/title_bar_test.dart +++ b/test/title_bar_test.dart @@ -91,4 +91,86 @@ void main() { // Null titleBar should return 0 expect(TitleBar.calculateHeight(ctx2, null), 0); }); + + testWidgets('an initially hidden title has no semantics until it fits', ( + tester, + ) async { + final handle = tester.ensureSemantics(); + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = const Size(200, 600); + addTearDown(tester.view.reset); + const searchLabel = 'Search settings'; + + try { + await tester.pumpWidget( + FluentApp( + home: NavigationView( + titleBar: TitleBar( + isBackButtonVisible: false, + title: Semantics( + label: searchLabel, + button: true, + child: const SizedBox( + width: searchLabel.length * 16, + height: 32, + ), + ), + ), + content: const SizedBox.shrink(), + ), + ), + ); + expect(tester.takeException(), isNull); + expect(find.bySemanticsLabel(searchLabel), findsNothing); + + tester.view.physicalSize = const Size(800, 600); + await tester.pump(); + expect(tester.takeException(), isNull); + expect(find.bySemanticsLabel(searchLabel), findsOneWidget); + } finally { + handle.dispose(); + } + }); + + testWidgets( + 'a dirty title can be hidden and restored with semantics enabled', + (tester) async { + final handle = tester.ensureSemantics(); + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = const Size(800, 600); + addTearDown(tester.view.reset); + + FluentApp buildApp(String title) => FluentApp( + home: NavigationView( + titleBar: TitleBar( + isBackButtonVisible: false, + title: Semantics( + label: title, + button: true, + child: SizedBox(width: title.length * 16, height: 32), + ), + ), + content: const SizedBox.shrink(), + ), + ); + + try { + await tester.pumpWidget(buildApp('Search')); + expect(tester.takeException(), isNull); + expect(find.bySemanticsLabel('Search'), findsOneWidget); + + tester.view.physicalSize = const Size(200, 600); + await tester.pumpWidget(buildApp('Search settings')); + expect(tester.takeException(), isNull); + expect(find.bySemanticsLabel('Search settings'), findsNothing); + + tester.view.physicalSize = const Size(800, 600); + await tester.pump(); + expect(tester.takeException(), isNull); + expect(find.bySemanticsLabel('Search settings'), findsOneWidget); + } finally { + handle.dispose(); + } + }, + ); }