diff --git a/packages/material_ui/lib/src/autocomplete.dart b/packages/material_ui/lib/src/autocomplete.dart index 17c75b3bbe2a..9e8f6e34c0d4 100644 --- a/packages/material_ui/lib/src/autocomplete.dart +++ b/packages/material_ui/lib/src/autocomplete.dart @@ -318,22 +318,19 @@ class _AutocompleteOptionsListState extends State<_Autocomplet itemCount: widget.options.length, itemBuilder: (BuildContext context, int index) { final T option = widget.options.elementAt(index); + final bool highlight = highlightedIndex == index; return Semantics( button: true, + selected: highlight, child: InkWell( key: GlobalObjectKey(option), onTap: () { widget.onSelected(option); }, - child: Builder( - builder: (BuildContext context) { - final highlight = highlightedIndex == index; - return Container( - color: highlight ? Theme.of(context).focusColor : null, - padding: const EdgeInsets.all(16.0), - child: Text(widget.displayStringForOption(option)), - ); - }, + child: Container( + color: highlight ? Theme.of(context).focusColor : null, + padding: const EdgeInsets.all(16.0), + child: Text(widget.displayStringForOption(option)), ), ), ); diff --git a/packages/material_ui/lib/src/drawer_header.dart b/packages/material_ui/lib/src/drawer_header.dart index cabd8c144a13..4a87f2461614 100644 --- a/packages/material_ui/lib/src/drawer_header.dart +++ b/packages/material_ui/lib/src/drawer_header.dart @@ -82,21 +82,24 @@ class DrawerHeader extends StatelessWidget { assert(debugCheckHasMediaQuery(context)); final ThemeData theme = Theme.of(context); final double statusBarHeight = MediaQuery.paddingOf(context).top; - return Container( - height: statusBarHeight + _kDrawerHeaderHeight, - margin: margin, - decoration: BoxDecoration(border: Border(bottom: Divider.createBorderSide(context))), - child: AnimatedContainer( - padding: padding.add(EdgeInsets.only(top: statusBarHeight)), - decoration: decoration, - duration: duration, - curve: curve, - child: child == null - ? null - : DefaultTextStyle( - style: theme.textTheme.bodyLarge!, - child: MediaQuery.removePadding(context: context, removeTop: true, child: child!), - ), + return Semantics( + header: true, + child: Container( + height: statusBarHeight + _kDrawerHeaderHeight, + margin: margin, + decoration: BoxDecoration(border: Border(bottom: Divider.createBorderSide(context))), + child: AnimatedContainer( + padding: padding.add(EdgeInsets.only(top: statusBarHeight)), + decoration: decoration, + duration: duration, + curve: curve, + child: child == null + ? null + : DefaultTextStyle( + style: theme.textTheme.bodyLarge!, + child: MediaQuery.removePadding(context: context, removeTop: true, child: child!), + ), + ), ), ); } diff --git a/packages/material_ui/lib/src/tooltip.dart b/packages/material_ui/lib/src/tooltip.dart index 95aa81b930e0..5cc8699c95dd 100644 --- a/packages/material_ui/lib/src/tooltip.dart +++ b/packages/material_ui/lib/src/tooltip.dart @@ -575,7 +575,8 @@ class TooltipState extends State with SingleTickerProviderStateMixin { onTriggered: widget.onTriggered, dismissDelay: widget.exitDuration ?? _tooltipTheme.exitDuration ?? _defaultExitDuration, positionDelegate: _getDefaultPositionDelegate, - ignorePointer: widget.ignorePointer ?? widget.message != null, + ignorePointer: + widget.ignorePointer ?? _tooltipTheme.ignorePointer ?? widget.message != null, child: effectiveChild, ); } diff --git a/packages/material_ui/lib/src/tooltip_theme.dart b/packages/material_ui/lib/src/tooltip_theme.dart index 4b130d2e75c0..993fe61f5229 100644 --- a/packages/material_ui/lib/src/tooltip_theme.dart +++ b/packages/material_ui/lib/src/tooltip_theme.dart @@ -54,6 +54,7 @@ class TooltipThemeData with Diagnosticable { this.exitDuration, this.triggerMode, this.enableFeedback, + this.ignorePointer, }) : assert( height == null || constraints == null, 'Only one of `height` and `constraints` may be specified.', @@ -137,6 +138,14 @@ class TooltipThemeData with Diagnosticable { /// * [Feedback], for providing platform-specific feedback to certain actions. final bool? enableFeedback; + /// Whether the tooltip overlay should be invisible to hit testing. + /// + /// If `false`, moving the mouse pointer onto the tooltip overlay keeps the + /// tooltip visible (satisfying WCAG 1.4.13 Hoverable). + /// + /// This value is used if [Tooltip.ignorePointer] is null. + final bool? ignorePointer; + /// Creates a copy of this object but with the given fields replaced with the /// new values. TooltipThemeData copyWith({ @@ -159,6 +168,7 @@ class TooltipThemeData with Diagnosticable { Duration? exitDuration, TooltipTriggerMode? triggerMode, bool? enableFeedback, + bool? ignorePointer, }) { return TooltipThemeData( height: height ?? this.height, @@ -173,8 +183,10 @@ class TooltipThemeData with Diagnosticable { textAlign: textAlign ?? this.textAlign, waitDuration: waitDuration ?? this.waitDuration, showDuration: showDuration ?? this.showDuration, + exitDuration: exitDuration ?? this.exitDuration, triggerMode: triggerMode ?? this.triggerMode, enableFeedback: enableFeedback ?? this.enableFeedback, + ignorePointer: ignorePointer ?? this.ignorePointer, ); } @@ -198,6 +210,7 @@ class TooltipThemeData with Diagnosticable { decoration: Decoration.lerp(a?.decoration, b?.decoration, t), textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t), textAlign: t < 0.5 ? a?.textAlign : b?.textAlign, + ignorePointer: t < 0.5 ? a?.ignorePointer : b?.ignorePointer, ); } @@ -218,6 +231,7 @@ class TooltipThemeData with Diagnosticable { exitDuration, triggerMode, enableFeedback, + ignorePointer, ); @override @@ -243,7 +257,8 @@ class TooltipThemeData with Diagnosticable { other.showDuration == showDuration && other.exitDuration == exitDuration && other.triggerMode == triggerMode && - other.enableFeedback == enableFeedback; + other.enableFeedback == enableFeedback && + other.ignorePointer == ignorePointer; } @override @@ -283,6 +298,7 @@ class TooltipThemeData with Diagnosticable { properties.add( DiagnosticsProperty('triggerMode', triggerMode, defaultValue: null), ); + properties.add(DiagnosticsProperty('ignorePointer', ignorePointer, defaultValue: null)); properties.add( FlagProperty('enableFeedback', value: enableFeedback, ifTrue: 'true', showName: true), ); diff --git a/packages/material_ui/pending_changelogs/a11y_autocomplete_drawer_tooltip.yaml b/packages/material_ui/pending_changelogs/a11y_autocomplete_drawer_tooltip.yaml new file mode 100644 index 000000000000..a6490fcc374a --- /dev/null +++ b/packages/material_ui/pending_changelogs/a11y_autocomplete_drawer_tooltip.yaml @@ -0,0 +1,5 @@ +changelog: | + - Exposes `selected` state on `Autocomplete` option semantics. + - Wraps `DrawerHeader` in `Semantics(header: true)`. + - Adds `ignorePointer` to `TooltipThemeData` for WCAG 1.4.13 Hoverable support. +version: minor diff --git a/packages/material_ui/test/autocomplete_test.dart b/packages/material_ui/test/autocomplete_test.dart index 45e403a2e31c..5617cfc2685e 100644 --- a/packages/material_ui/test/autocomplete_test.dart +++ b/packages/material_ui/test/autocomplete_test.dart @@ -893,18 +893,32 @@ void main() { ); await tester.tap(find.byType(TextField)); await tester.pump(); - await tester.enterText(find.byType(TextField), 'aa'); + await tester.enterText(find.byType(TextField), 'a'); await tester.pump(); expect( tester.getSemantics(find.text('aardvark')), matchesSemantics( isButton: true, isFocusable: true, + hasSelectedState: true, + isSelected: true, hasTapAction: true, hasFocusAction: true, label: 'aardvark', ), ); + expect( + tester.getSemantics(find.text('bobcat')), + matchesSemantics( + isButton: true, + isFocusable: true, + hasSelectedState: true, + isSelected: false, + hasTapAction: true, + hasFocusAction: true, + label: 'bobcat', + ), + ); }); testWidgets('Same option in Autocomplete should be selectable again after text is cleared', ( diff --git a/packages/material_ui/test/drawer_test.dart b/packages/material_ui/test/drawer_test.dart index 5bbfd15379fb..eba263e5e630 100644 --- a/packages/material_ui/test/drawer_test.dart +++ b/packages/material_ui/test/drawer_test.dart @@ -1513,4 +1513,19 @@ void main() { expect(tester.getSize(find.byType(Drawer)), Size.zero); expect(tester.getSize(find.byType(DrawerHeader)), Size.zero); }); + + testWidgets('DrawerHeader provides header semantics', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: DrawerHeader(child: Text('Drawer Title'))), + ), + ); + + expect( + tester.getSemantics(find.byType(DrawerHeader)), + matchesSemantics(isHeader: true, label: 'Drawer Title', textDirection: TextDirection.ltr), + ); + handle.dispose(); + }); } diff --git a/packages/material_ui/test/tooltip_theme_test.dart b/packages/material_ui/test/tooltip_theme_test.dart index 6090f5178145..b42efbec1a75 100644 --- a/packages/material_ui/test/tooltip_theme_test.dart +++ b/packages/material_ui/test/tooltip_theme_test.dart @@ -41,6 +41,7 @@ void main() { expect(theme.exitDuration, null); expect(theme.triggerMode, null); expect(theme.enableFeedback, null); + expect(theme.ignorePointer, null); }); testWidgets('Default TooltipThemeData debugFillProperties', (WidgetTester tester) async { @@ -62,6 +63,7 @@ void main() { const exit = Duration(milliseconds: 100); const TooltipTriggerMode triggerMode = TooltipTriggerMode.longPress; const enableFeedback = true; + const ignorePointer = false; const TooltipThemeData( height: 15.0, padding: EdgeInsets.all(20.0), @@ -76,6 +78,7 @@ void main() { exitDuration: exit, triggerMode: triggerMode, enableFeedback: enableFeedback, + ignorePointer: ignorePointer, ).debugFillProperties(builder); final List description = builder.properties @@ -97,6 +100,7 @@ void main() { 'exit duration: $exit', 'triggerMode: $triggerMode', 'enableFeedback: true', + 'ignorePointer: false', ]); }); @@ -1540,6 +1544,23 @@ void main() { ); expect(tester.element(textAncestors.first).size, equals(themeConstraints.biggest)); }); + + testWidgets('Tooltip respects ignorePointer from the ambient theme', (WidgetTester tester) async { + final tooltipKey = GlobalKey(); + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(tooltipTheme: const TooltipThemeData(ignorePointer: false)), + home: Tooltip( + key: tooltipKey, + message: tooltipText, + child: const SizedBox(width: 100, height: 100), + ), + ), + ); + + final RawTooltip rawTooltip = tester.widget(find.byType(RawTooltip)); + expect(rawTooltip.ignorePointer, isFalse); + }); } SemanticsNode findDebugSemantics(RenderObject object) { diff --git a/packages/material_ui/test/user_accounts_drawer_header_test.dart b/packages/material_ui/test/user_accounts_drawer_header_test.dart index d361ba87c4b5..0955305b35e5 100644 --- a/packages/material_ui/test/user_accounts_drawer_header_test.dart +++ b/packages/material_ui/test/user_accounts_drawer_header_test.dart @@ -494,6 +494,7 @@ void main() { expect( tester.semantics.find(find.byType(UserAccountsDrawerHeader)), matchesSemantics( + isHeader: true, isFocusable: true, hasFocusAction: true, label: 'Signed in\nname\nemail', @@ -559,6 +560,7 @@ void main() { expect( tester.semantics.find(find.byType(UserAccountsDrawerHeader)), matchesSemantics( + isHeader: true, label: 'Signed in', textDirection: TextDirection.ltr, children: [