From e2610a450c5c4aa41db01dae5dc0f4be6cd53aff Mon Sep 17 00:00:00 2001 From: rami-a <2364772+rami-a@users.noreply.github.com> Date: Thu, 19 Mar 2020 18:06:02 -0400 Subject: [PATCH] [Material] Allow Appbar to exclude header semantics (#52894) --- .../flutter/lib/src/material/app_bar.dart | 34 +++++- .../flutter/test/material/app_bar_test.dart | 114 ++++++++++++++++++ 2 files changed, 142 insertions(+), 6 deletions(-) diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index 9832bdff1b..dcc805ef54 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -193,6 +193,7 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { this.textTheme, this.primary = true, this.centerTitle, + this.excludeHeaderSemantics = false, this.titleSpacing = NavigationToolbar.kMiddleSpacing, this.toolbarOpacity = 1.0, this.bottomOpacity = 1.0, @@ -387,6 +388,11 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { /// Defaults to being adapted to the current [TargetPlatform]. final bool centerTitle; + /// Whether the title should be wrapped with header [Semantics]. + /// + /// Defaults to false. + final bool excludeHeaderSemantics; + /// The spacing around [title] content on the horizontal axis. This spacing is /// applied even if there is no [leading] content or [actions]. If you want /// [title] to take all the space available, set this value to 0.0. @@ -526,15 +532,21 @@ class _AppBarState extends State { case TargetPlatform.macOS: break; } + + title = _AppBarTitleBox(child: title); + if (!widget.excludeHeaderSemantics) { + title = Semantics( + namesRoute: namesRoute, + child: title, + header: true, + ); + } + title = DefaultTextStyle( style: centerStyle, softWrap: false, overflow: TextOverflow.ellipsis, - child: Semantics( - namesRoute: namesRoute, - child: _AppBarTitleBox(child: title), - header: true, - ), + child: title, ); } @@ -725,6 +737,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { @required this.textTheme, @required this.primary, @required this.centerTitle, + @required this.excludeHeaderSemantics, @required this.titleSpacing, @required this.expandedHeight, @required this.collapsedHeight, @@ -752,6 +765,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { final TextTheme textTheme; final bool primary; final bool centerTitle; + final bool excludeHeaderSemantics; final double titleSpacing; final double expandedHeight; final double collapsedHeight; @@ -803,7 +817,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { automaticallyImplyLeading: automaticallyImplyLeading, title: title, actions: actions, - flexibleSpace: (title == null && flexibleSpace != null) + flexibleSpace: (title == null && flexibleSpace != null && !excludeHeaderSemantics) ? Semantics(child: flexibleSpace, header: true) : flexibleSpace, bottom: bottom, @@ -815,6 +829,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { textTheme: textTheme, primary: primary, centerTitle: centerTitle, + excludeHeaderSemantics: excludeHeaderSemantics, titleSpacing: titleSpacing, shape: shape, toolbarOpacity: toolbarOpacity, @@ -956,6 +971,7 @@ class SliverAppBar extends StatefulWidget { this.textTheme, this.primary = true, this.centerTitle, + this.excludeHeaderSemantics = false, this.titleSpacing = NavigationToolbar.kMiddleSpacing, this.expandedHeight, this.floating = false, @@ -1120,6 +1136,11 @@ class SliverAppBar extends StatefulWidget { /// Defaults to being adapted to the current [TargetPlatform]. final bool centerTitle; + /// Whether the title should be wrapped with header [Semantics]. + /// + /// Defaults to false. + final bool excludeHeaderSemantics; + /// The spacing around [title] content on the horizontal axis. This spacing is /// applied even if there is no [leading] content or [actions]. If you want /// [title] to take all the space available, set this value to 0.0. @@ -1309,6 +1330,7 @@ class _SliverAppBarState extends State with TickerProviderStateMix textTheme: widget.textTheme, primary: widget.primary, centerTitle: widget.centerTitle, + excludeHeaderSemantics: widget.excludeHeaderSemantics, titleSpacing: widget.titleSpacing, expandedHeight: widget.expandedHeight, collapsedHeight: collapsedHeight, diff --git a/packages/flutter/test/material/app_bar_test.dart b/packages/flutter/test/material/app_bar_test.dart index 78f423b595..5a923f2209 100644 --- a/packages/flutter/test/material/app_bar_test.dart +++ b/packages/flutter/test/material/app_bar_test.dart @@ -1432,6 +1432,120 @@ void main() { semantics.dispose(); }); + testWidgets('AppBar excludes header semantics correctly', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + + await tester.pumpWidget( + MaterialApp( + home: Center( + child: AppBar( + leading: const Text('Leading'), + title: const ExcludeSemantics(child: Text('Title')), + excludeHeaderSemantics: true, + actions: const [ + Text('Action 1'), + ], + ), + ), + ), + ); + + expect(semantics, hasSemantics( + TestSemantics.root( + children: [ + TestSemantics( + children: [ + TestSemantics( + flags: [SemanticsFlag.scopesRoute], + children: [ + TestSemantics( + children: [ + TestSemantics( + label: 'Leading', + textDirection: TextDirection.ltr, + ), + TestSemantics( + label: 'Action 1', + textDirection: TextDirection.ltr, + ), + ], + ), + ], + ), + ], + ), + ], + ), + ignoreRect: true, + ignoreTransform: true, + ignoreId: true, + )); + + semantics.dispose(); + }); + + testWidgets('SliverAppBar excludes header semantics correctly', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + + await tester.pumpWidget( + const MaterialApp( + home: CustomScrollView( + slivers: [ + SliverAppBar( + leading: Text('Leading'), + flexibleSpace: ExcludeSemantics(child: Text('Title')), + actions: [Text('Action 1')], + excludeHeaderSemantics: true, + ), + ], + ), + ), + ); + + expect(semantics, hasSemantics( + TestSemantics.root( + children: [ + TestSemantics( + textDirection: TextDirection.ltr, + children: [ + TestSemantics( + flags: [SemanticsFlag.scopesRoute], + children: [ + TestSemantics( + children: [ + TestSemantics( + flags: [SemanticsFlag.hasImplicitScrolling], + children: [ + TestSemantics( + children: [ + TestSemantics( + label: 'Leading', + textDirection: TextDirection.ltr, + ), + TestSemantics( + label: 'Action 1', + textDirection: TextDirection.ltr, + ), + ], + ), + ], + ), + ], + ), + ], + ), + ], + ), + ], + ), + ignoreRect: true, + ignoreTransform: true, + ignoreId: true, + )); + + semantics.dispose(); + }); + testWidgets('AppBar draws a light system bar for a dark background', (WidgetTester tester) async { final ThemeData darkTheme = ThemeData.dark(); await tester.pumpWidget(MaterialApp(