diff --git a/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.0.dart b/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.0.dart index f5e85c835ead6..f104f7ddbb9c6 100644 --- a/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.0.dart +++ b/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.0.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -128,7 +126,6 @@ class CustomMenu extends StatelessWidget { child: Semantics( scopesRoute: true, explicitChildNodes: true, - role: SemanticsRole.menu, child: TapRegion( groupId: info.tapRegionGroupId, onTapOutside: (PointerDownEvent event) { diff --git a/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.1.dart b/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.1.dart index bef75fca46b46..cbf44c8567ddd 100644 --- a/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.1.dart +++ b/examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.1.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -93,70 +91,66 @@ class _RawMenuAnchorGroupExampleState extends State { clipBehavior: Clip.hardEdge, child: RawMenuAnchorGroup( controller: controller, - child: Semantics( - role: SemanticsRole.menu, - child: Row( - children: [ - for (int i = 0; i < menuItems.length; i++) - CustomSubmenu( - focusNode: focusNodes[i], - anchor: Builder( - builder: (BuildContext context) { - final MenuController submenuController = - MenuController.maybeOf(context)!; - final MenuItem item = menuItems[i]; - final ButtonStyle openBackground = MenuItemButton.styleFrom( - backgroundColor: const Color(0x0D1A1A1A), - ); - return MergeSemantics( - child: Semantics( - expanded: controller.isOpen, - child: MenuItemButton( - style: submenuController.isOpen ? openBackground : null, - onHover: (bool value) { - // If any submenu in the menu bar is already open, other - // submenus should open on hover. Otherwise, blur the menu item - // button if the menu button is no longer hovered. - if (controller.isOpen) { - if (value) { - submenuController.open(); - } - } else if (!value) { - Focus.of(context).unfocus(); - } - }, - onPressed: () { - if (submenuController.isOpen) { - submenuController.close(); - } else { + child: Row( + children: [ + for (int i = 0; i < menuItems.length; i++) + CustomSubmenu( + focusNode: focusNodes[i], + anchor: Builder( + builder: (BuildContext context) { + final MenuController submenuController = MenuController.maybeOf(context)!; + final MenuItem item = menuItems[i]; + final ButtonStyle openBackground = MenuItemButton.styleFrom( + backgroundColor: const Color(0x0D1A1A1A), + ); + return MergeSemantics( + child: Semantics( + expanded: controller.isOpen, + child: MenuItemButton( + style: submenuController.isOpen ? openBackground : null, + onHover: (bool value) { + // If any submenu in the menu bar is already open, other + // submenus should open on hover. Otherwise, blur the menu item + // button if the menu button is no longer hovered. + if (controller.isOpen) { + if (value) { submenuController.open(); } - }, - leadingIcon: item.leading, - child: Text(item.label), - ), + } else if (!value) { + Focus.of(context).unfocus(); + } + }, + onPressed: () { + if (submenuController.isOpen) { + submenuController.close(); + } else { + submenuController.open(); + } + }, + leadingIcon: item.leading, + child: Text(item.label), ), - ); - }, - ), - children: [ - for (final MenuItem child in menuItems[i].children ?? []) - MenuItemButton( - onPressed: () { - setState(() { - _selected = child; - }); - - // Close the menu bar after a selection. - controller.close(); - }, - leadingIcon: child.leading, - child: Text(child.label), ), - ], + ); + }, ), - ], - ), + children: [ + for (final MenuItem child in menuItems[i].children ?? []) + MenuItemButton( + onPressed: () { + setState(() { + _selected = child; + }); + + // Close the menu bar after a selection. + controller.close(); + }, + leadingIcon: child.leading, + child: Text(child.label), + ), + ], + ), + ], ), ), ), diff --git a/packages/flutter/lib/src/material/menu_anchor.dart b/packages/flutter/lib/src/material/menu_anchor.dart index 507125ea1d3bd..8746f71c0d44a 100644 --- a/packages/flutter/lib/src/material/menu_anchor.dart +++ b/packages/flutter/lib/src/material/menu_anchor.dart @@ -940,9 +940,7 @@ class _MenuItemButtonState extends State { child = MouseRegion(onHover: _handlePointerHover, onExit: _handlePointerExit, child: child); } - return MergeSemantics( - child: Semantics(role: SemanticsRole.menuItem, enabled: widget.enabled, child: child), - ); + return MergeSemantics(child: child); } void _handleFocusChange() { @@ -1156,54 +1154,44 @@ class CheckboxMenuButton extends StatelessWidget { @override Widget build(BuildContext context) { - return MergeSemantics( - child: Semantics( - role: SemanticsRole.menuItemCheckbox, - checked: value ?? false, - mixed: tristate ? value == null : null, - child: MenuItemButton( - key: key, - onPressed: - onChanged == null - ? null - : () { - switch (value) { - case false: - onChanged!(true); - case true: - onChanged!(tristate ? null : false); - case null: - onChanged!(false); - } - }, - onHover: onHover, - onFocusChange: onFocusChange, - focusNode: focusNode, - style: style, - shortcut: shortcut, - statesController: statesController, - leadingIcon: ExcludeFocus( - child: IgnorePointer( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: Checkbox.width, - maxWidth: Checkbox.width, - ), - child: Checkbox( - tristate: tristate, - value: value, - onChanged: onChanged, - isError: isError, - ), - ), + return MenuItemButton( + key: key, + onPressed: + onChanged == null + ? null + : () { + switch (value) { + case false: + onChanged!(true); + case true: + onChanged!(tristate ? null : false); + case null: + onChanged!(false); + } + }, + onHover: onHover, + onFocusChange: onFocusChange, + focusNode: focusNode, + style: style, + shortcut: shortcut, + statesController: statesController, + leadingIcon: ExcludeFocus( + child: IgnorePointer( + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: Checkbox.width, maxWidth: Checkbox.width), + child: Checkbox( + tristate: tristate, + value: value, + onChanged: onChanged, + isError: isError, ), ), - clipBehavior: clipBehavior, - trailingIcon: trailingIcon, - closeOnActivate: closeOnActivate, - child: child, ), ), + clipBehavior: clipBehavior, + trailingIcon: trailingIcon, + closeOnActivate: closeOnActivate, + child: child, ); } } @@ -1365,49 +1353,40 @@ class RadioMenuButton extends StatelessWidget { @override Widget build(BuildContext context) { - return MergeSemantics( - child: Semantics( - role: SemanticsRole.menuItemRadio, - checked: value == groupValue, - child: MenuItemButton( - key: key, - onPressed: - onChanged == null - ? null - : () { - if (toggleable && groupValue == value) { - return onChanged!(null); - } - onChanged!(value); - }, - onHover: onHover, - onFocusChange: onFocusChange, - focusNode: focusNode, - style: style, - shortcut: shortcut, - statesController: statesController, - leadingIcon: ExcludeFocus( - child: IgnorePointer( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: Checkbox.width, - maxWidth: Checkbox.width, - ), - child: Radio( - value: value, - groupValue: groupValue, - onChanged: onChanged, - toggleable: toggleable, - ), - ), + return MenuItemButton( + key: key, + onPressed: + onChanged == null + ? null + : () { + if (toggleable && groupValue == value) { + return onChanged!(null); + } + onChanged!(value); + }, + onHover: onHover, + onFocusChange: onFocusChange, + focusNode: focusNode, + style: style, + shortcut: shortcut, + statesController: statesController, + leadingIcon: ExcludeFocus( + child: IgnorePointer( + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: Checkbox.width, maxWidth: Checkbox.width), + child: Radio( + value: value, + groupValue: groupValue, + onChanged: onChanged, + toggleable: toggleable, ), ), - clipBehavior: clipBehavior, - trailingIcon: trailingIcon, - closeOnActivate: closeOnActivate, - child: child, ), ), + clipBehavior: clipBehavior, + trailingIcon: trailingIcon, + closeOnActivate: closeOnActivate, + child: child, ); } } @@ -1846,24 +1825,23 @@ class _SubmenuButtonState extends State { } } - child = Semantics( - container: true, - role: SemanticsRole.menuItem, - expanded: _enabled && controller.isOpen, - enabled: _enabled, - child: TextButton( - style: mergedStyle, - focusNode: _buttonFocusNode, - onFocusChange: _enabled ? widget.onFocusChange : null, - onPressed: _enabled ? toggleShowMenu : null, - isSemanticButton: null, - child: _MenuItemLabel( - leadingIcon: widget.leadingIcon, - trailingIcon: widget.trailingIcon, - hasSubmenu: true, - showDecoration: (_parent?._orientation ?? Axis.horizontal) == Axis.vertical, - submenuIcon: submenuIcon, - child: child, + child = MergeSemantics( + child: Semantics( + expanded: _enabled && controller.isOpen, + child: TextButton( + style: mergedStyle, + focusNode: _buttonFocusNode, + onFocusChange: _enabled ? widget.onFocusChange : null, + onPressed: _enabled ? toggleShowMenu : null, + isSemanticButton: null, + child: _MenuItemLabel( + leadingIcon: widget.leadingIcon, + trailingIcon: widget.trailingIcon, + hasSubmenu: true, + showDecoration: (_parent?._orientation ?? Axis.horizontal) == Axis.vertical, + submenuIcon: submenuIcon, + child: child, + ), ), ), ); @@ -1894,9 +1872,9 @@ class _SubmenuButtonState extends State { // After closing the children of this submenu, this submenu button will // regain focus. Because submenu buttons open on focus, this submenu will // immediately reopen. To prevent this from happening, we prevent focus on - // SubmenuButtons that do not already have focus using the _isOpenOnFocusEnabled + // SubmenuButtons that do not already have focus using the _openOnFocus // flag. This flag is reset after one frame. - if (!_buttonFocusNode.hasPrimaryFocus) { + if (!_buttonFocusNode.hasFocus) { _isOpenOnFocusEnabled = false; SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) { FocusManager.instance.applyFocusChangesIfNeeded(); @@ -3283,10 +3261,7 @@ class _MenuPanelState extends State<_MenuPanel> { ); } - return Semantics( - role: widget.orientation == Axis.vertical ? SemanticsRole.menu : SemanticsRole.menuBar, - child: ConstrainedBox(constraints: effectiveConstraints, child: menuPanel), - ); + return ConstrainedBox(constraints: effectiveConstraints, child: menuPanel); } Widget _intrinsicCrossSize({required Widget child}) { diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index 55b687b96747a..69e47675486ee 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -126,7 +126,7 @@ sealed class _DebugSemanticsRoleChecks { SemanticsRole.row => _semanticsRow, SemanticsRole.columnHeader => _semanticsColumnHeader, SemanticsRole.radioGroup => _semanticsRadioGroup, - SemanticsRole.menu => _noCheckRequired, + SemanticsRole.menu => _semanticsMenu, SemanticsRole.menuBar => _semanticsMenuBar, SemanticsRole.menuItem => _semanticsMenuItem, SemanticsRole.menuItemCheckbox => _semanticsMenuItemCheckbox, @@ -259,6 +259,14 @@ sealed class _DebugSemanticsRoleChecks { return error; } + static FlutterError? _semanticsMenu(SemanticsNode node) { + if (node.childrenCount < 1) { + return FlutterError('a menu cannot be empty'); + } + + return null; + } + static FlutterError? _semanticsMenuBar(SemanticsNode node) { if (node.childrenCount < 1) { return FlutterError('a menu bar cannot be empty'); diff --git a/packages/flutter/lib/src/widgets/raw_menu_anchor.dart b/packages/flutter/lib/src/widgets/raw_menu_anchor.dart index ed268cb19c484..8a6397f661fa9 100644 --- a/packages/flutter/lib/src/widgets/raw_menu_anchor.dart +++ b/packages/flutter/lib/src/widgets/raw_menu_anchor.dart @@ -590,24 +590,6 @@ class _RawMenuAnchorState extends State with _RawMenuAnchorBaseMi @override Widget buildAnchor(BuildContext context) { - // Only when both `child` and `builder` are not null, can the anchor and its - // children have a parent-child relationship. This is useful for a11y - // traversal in a `MenuBar` composed of a list of `SubmenuButton`s. - final Widget? overlayPortal = - widget.child == null || widget.builder == null - ? null - : useRootOverlay - ? OverlayPortal.targetsRootOverlay( - controller: _overlayController, - overlayChildBuilder: _buildOverlay, - child: widget.child, - ) - : OverlayPortal( - controller: _overlayController, - overlayChildBuilder: _buildOverlay, - child: widget.child, - ); - final Widget child = Shortcuts( includeSemantics: false, shortcuts: _kMenuTraversalShortcuts, @@ -618,7 +600,7 @@ class _RawMenuAnchorState extends State with _RawMenuAnchorBaseMi child: Builder( key: _anchorKey, builder: (BuildContext context) { - return widget.builder?.call(context, menuController, overlayPortal) ?? + return widget.builder?.call(context, menuController, widget.child) ?? widget.child ?? const SizedBox(); }, @@ -626,22 +608,19 @@ class _RawMenuAnchorState extends State with _RawMenuAnchorBaseMi ), ); - if (widget.child == null || widget.builder == null) { - if (useRootOverlay) { - return OverlayPortal.targetsRootOverlay( - controller: _overlayController, - overlayChildBuilder: _buildOverlay, - child: child, - ); - } else { - return OverlayPortal( - controller: _overlayController, - overlayChildBuilder: _buildOverlay, - child: child, - ); - } + if (useRootOverlay) { + return OverlayPortal.targetsRootOverlay( + controller: _overlayController, + overlayChildBuilder: _buildOverlay, + child: child, + ); + } else { + return OverlayPortal( + controller: _overlayController, + overlayChildBuilder: _buildOverlay, + child: child, + ); } - return child; } @override diff --git a/packages/flutter/test/material/menu_anchor_test.dart b/packages/flutter/test/material/menu_anchor_test.dart index d1ad444e650c0..6b01b1e2475af 100644 --- a/packages/flutter/test/material/menu_anchor_test.dart +++ b/packages/flutter/test/material/menu_anchor_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui'; - import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -76,11 +74,17 @@ void main() { return results; } + Finder findMenuBarItemLabels() { + return find.byWidgetPredicate( + (Widget widget) => widget.runtimeType.toString() == '_MenuItemLabel', + ); + } + // Finds the mnemonic associated with the menu item that has the given label. Finder findMnemonic(String label) { return find .descendant( - of: find.ancestor(of: find.text(label), matching: find.byType(MenuItemButton)), + of: find.ancestor(of: find.text(label), matching: findMenuBarItemLabels()), matching: find.byType(Text), ) .last; @@ -216,6 +220,10 @@ void main() { await tester.tap(find.text(TestMenu.mainMenu1.label)); await tester.pump(); + expect( + tester.getRect(find.byType(MenuBar)), + equals(const Rect.fromLTRB(145.0, 0.0, 655.0, 48.0)), + ); expect( tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(145.0, 0.0, 655.0, 48.0)), @@ -1353,7 +1361,6 @@ void main() { ), ); }); - testWidgets('menus can be traversed multiple times', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/150334 await tester.pumpWidget( @@ -1361,13 +1368,10 @@ void main() { home: Material( child: Column( children: [ - Semantics( - role: SemanticsRole.menu, - child: MenuItemButton( - autofocus: true, - onPressed: () {}, - child: const Text('External Focus'), - ), + MenuItemButton( + autofocus: true, + onPressed: () {}, + child: const Text('External Focus'), ), MenuBar( controller: controller, @@ -3121,14 +3125,10 @@ void main() { home: Scaffold( body: SizedBox( width: 200, - // This is added because a menu item must be a child of a menu or menu bar. - child: Semantics( - role: SemanticsRole.menu, - child: MenuItemButton( - overflowAxis: Axis.vertical, - onPressed: () {}, - child: const Text('MenuItem Button does not overflow when child is long'), - ), + child: MenuItemButton( + overflowAxis: Axis.vertical, + onPressed: () {}, + child: const Text('MenuItem Button does not overflow when child is long'), ), ), ), @@ -3145,16 +3145,10 @@ void main() { home: Scaffold( body: SizedBox( width: constrainedLayout ? 200 : null, - // This is added because a menu item must be a child of a menu or menu bar. - child: Semantics( - role: SemanticsRole.menu, - child: MenuItemButton( - overflowAxis: overflowAxis, - onPressed: () {}, - child: const Text( - 'This is a very long text that will wrap to the multiple lines.', - ), - ), + child: MenuItemButton( + overflowAxis: overflowAxis, + onPressed: () {}, + child: const Text('This is a very long text that will wrap to the multiple lines.'), ), ), ), @@ -3192,14 +3186,10 @@ void main() { await tester.pumpWidget( MaterialApp( home: Scaffold( - // This is added because a menu item must be a child of a menu or menu bar. - body: Semantics( - role: SemanticsRole.menu, - child: MenuItemButton( - style: MenuItemButton.styleFrom(overlayColor: overlayColor), - onPressed: () {}, - child: const Text('MenuItem'), - ), + body: MenuItemButton( + style: MenuItemButton.styleFrom(overlayColor: overlayColor), + onPressed: () {}, + child: const Text('MenuItem'), ), ), ), @@ -3228,14 +3218,7 @@ void main() { // Regression test for https://github.com/flutter/flutter/issues/147479. testWidgets('MenuItemButton can build when its child is null', (WidgetTester tester) async { await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: SizedBox( - width: 200, - child: Semantics(role: SemanticsRole.menu, child: const MenuItemButton()), - ), - ), - ), + const MaterialApp(home: Scaffold(body: SizedBox(width: 200, child: MenuItemButton()))), ); expect(tester.takeException(), isNull); @@ -4169,13 +4152,10 @@ void main() { Directionality( textDirection: TextDirection.ltr, child: Center( - child: Semantics( - role: SemanticsRole.menu, - child: MenuItemButton( - style: MenuItemButton.styleFrom(fixedSize: const Size(88.0, 36.0)), - onPressed: () {}, - child: const Text('ABC'), - ), + child: MenuItemButton( + style: MenuItemButton.styleFrom(fixedSize: const Size(88.0, 36.0)), + onPressed: () {}, + child: const Text('ABC'), ), ), ), @@ -4188,25 +4168,20 @@ void main() { TestSemantics.root( children: [ TestSemantics.rootChild( - role: SemanticsRole.menu, - children: [ - TestSemantics( - role: SemanticsRole.menuItem, - flags: [ - SemanticsFlag.hasEnabledState, - SemanticsFlag.isEnabled, - SemanticsFlag.isFocusable, - ], - actions: [SemanticsAction.tap, SemanticsAction.focus], - label: 'ABC', - ), + actions: [SemanticsAction.tap, SemanticsAction.focus], + label: 'ABC', + rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), + transform: Matrix4.translationValues(356.0, 276.0, 0.0), + flags: [ + SemanticsFlag.hasEnabledState, + SemanticsFlag.isEnabled, + SemanticsFlag.isFocusable, ], + textDirection: TextDirection.ltr, ), ], ), ignoreId: true, - ignoreRect: true, - ignoreTransform: true, ), ); @@ -4218,15 +4193,12 @@ void main() { await tester.pumpWidget( MaterialApp( home: Center( - child: Semantics( - role: SemanticsRole.menu, - child: MenuItemButton( - semanticsLabel: 'TestWidget', - shortcut: const SingleActivator(LogicalKeyboardKey.comma), - style: MenuItemButton.styleFrom(fixedSize: const Size(88.0, 36.0)), - onPressed: () {}, - child: const Text('ABC'), - ), + child: MenuItemButton( + semanticsLabel: 'TestWidget', + shortcut: const SingleActivator(LogicalKeyboardKey.comma), + style: MenuItemButton.styleFrom(fixedSize: const Size(88.0, 36.0)), + onPressed: () {}, + child: const Text('ABC'), ), ), ), @@ -4242,15 +4214,11 @@ void main() { Directionality( textDirection: TextDirection.ltr, child: Center( - // This is added because a menu item must be a child of a menu or menu bar. - child: Semantics( - role: SemanticsRole.menu, - child: SubmenuButton( - onHover: (bool value) {}, - style: SubmenuButton.styleFrom(fixedSize: const Size(88.0, 36.0)), - menuChildren: const [], - child: const Text('ABC'), - ), + child: SubmenuButton( + onHover: (bool value) {}, + style: SubmenuButton.styleFrom(fixedSize: const Size(88.0, 36.0)), + menuChildren: const [], + child: const Text('ABC'), ), ), ), @@ -4263,29 +4231,18 @@ void main() { TestSemantics.root( children: [ TestSemantics( - role: SemanticsRole.menu, - children: [ - TestSemantics( - flags: [ - SemanticsFlag.hasEnabledState, - SemanticsFlag.hasExpandedState, - ], - role: SemanticsRole.menuItem, - children: [ - TestSemantics( - flags: [SemanticsFlag.hasEnabledState], - label: 'ABC', - textDirection: TextDirection.ltr, - ), - ], - ), + rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), + flags: [ + SemanticsFlag.hasEnabledState, + SemanticsFlag.hasExpandedState, ], + label: 'ABC', + textDirection: TextDirection.ltr, ), ], ), ignoreTransform: true, ignoreId: true, - ignoreRect: true, ), ); @@ -4297,20 +4254,16 @@ void main() { await tester.pumpWidget( MaterialApp( home: Center( - // This is added because a menu item must be a child of a menu or menu bar. - child: Semantics( - role: SemanticsRole.menu, - child: SubmenuButton( - style: SubmenuButton.styleFrom(fixedSize: const Size(88.0, 36.0)), - menuChildren: [ - MenuItemButton( - style: MenuItemButton.styleFrom(fixedSize: const Size(120.0, 36.0)), - child: const Text('Item 0'), - onPressed: () {}, - ), - ], - child: const Text('ABC'), - ), + child: SubmenuButton( + style: SubmenuButton.styleFrom(fixedSize: const Size(88.0, 36.0)), + menuChildren: [ + MenuItemButton( + style: MenuItemButton.styleFrom(fixedSize: const Size(120.0, 36.0)), + child: const Text('Item 0'), + onPressed: () {}, + ), + ], + child: const Text('ABC'), ), ), ), @@ -4339,25 +4292,35 @@ void main() { children: [ TestSemantics( id: 4, + flags: [ + SemanticsFlag.isFocused, + SemanticsFlag.hasEnabledState, + SemanticsFlag.isEnabled, + SemanticsFlag.isFocusable, + SemanticsFlag.hasExpandedState, + SemanticsFlag.isExpanded, + ], + actions: [ + SemanticsAction.tap, + SemanticsAction.focus, + ], + label: 'ABC', rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), - role: SemanticsRole.menu, + ), + TestSemantics( + id: 6, + rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 64.0), children: [ TestSemantics( - id: 5, - rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), - flags: [ - SemanticsFlag.hasEnabledState, - SemanticsFlag.isEnabled, - SemanticsFlag.hasExpandedState, - SemanticsFlag.isExpanded, - ], - role: SemanticsRole.menuItem, + id: 7, + rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0), + flags: [SemanticsFlag.hasImplicitScrolling], children: [ TestSemantics( - id: 6, - rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), + id: 8, + label: 'Item 0', + rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0), flags: [ - SemanticsFlag.isFocused, SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, @@ -4366,41 +4329,6 @@ void main() { SemanticsAction.tap, SemanticsAction.focus, ], - label: 'ABC', - textDirection: TextDirection.ltr, - children: [ - TestSemantics( - id: 7, - rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 64.0), - role: SemanticsRole.menu, - children: [ - TestSemantics( - id: 8, - rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0), - flags: [ - SemanticsFlag.hasImplicitScrolling, - ], - children: [ - TestSemantics( - id: 9, - rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0), - flags: [ - SemanticsFlag.hasEnabledState, - SemanticsFlag.isEnabled, - SemanticsFlag.isFocusable, - ], - actions: [ - SemanticsAction.tap, - SemanticsAction.focus, - ], - label: 'Item 0', - role: SemanticsRole.menuItem, - ), - ], - ), - ], - ), - ], ), ], ), @@ -4441,38 +4369,19 @@ void main() { children: [ TestSemantics( id: 4, - role: SemanticsRole.menu, - rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), - children: [ - TestSemantics( - id: 5, - rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), - flags: [ - SemanticsFlag.hasExpandedState, - SemanticsFlag.hasEnabledState, - SemanticsFlag.isEnabled, - ], - role: SemanticsRole.menuItem, - children: [ - TestSemantics( - id: 6, - rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), - flags: [ - SemanticsFlag.isFocused, - SemanticsFlag.hasEnabledState, - SemanticsFlag.isEnabled, - SemanticsFlag.isFocusable, - ], - actions: [ - SemanticsAction.tap, - SemanticsAction.focus, - ], - label: 'ABC', - textDirection: TextDirection.ltr, - ), - ], - ), + flags: [ + SemanticsFlag.hasExpandedState, + SemanticsFlag.isFocused, + SemanticsFlag.hasEnabledState, + SemanticsFlag.isEnabled, + SemanticsFlag.isFocusable, ], + actions: [ + SemanticsAction.tap, + SemanticsAction.focus, + ], + label: 'ABC', + rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), ), ], ), @@ -4548,18 +4457,15 @@ void main() { home: Material( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return Semantics( - role: SemanticsRole.menu, - child: SubmenuButton( - focusNode: focusNode, - onFocusChange: (bool value) { - setState(() { - onFocusChangeCalled += 1; - }); - }, - menuChildren: const [MenuItemButton(child: Text('item 0'))], - child: const Text('Submenu 0'), - ), + return SubmenuButton( + focusNode: focusNode, + onFocusChange: (bool value) { + setState(() { + onFocusChangeCalled += 1; + }); + }, + menuChildren: const [MenuItemButton(child: Text('item 0'))], + child: const Text('Submenu 0'), ); }, ), @@ -4607,15 +4513,12 @@ void main() { await tester.pumpWidget( MaterialApp( home: Scaffold( - body: Semantics( - role: SemanticsRole.menu, - child: SubmenuButton( - style: SubmenuButton.styleFrom(overlayColor: overlayColor), - menuChildren: [ - MenuItemButton(onPressed: () {}, child: const Text('MenuItemButton')), - ], - child: const Text('Submenu'), - ), + body: SubmenuButton( + style: SubmenuButton.styleFrom(overlayColor: overlayColor), + menuChildren: [ + MenuItemButton(onPressed: () {}, child: const Text('MenuItemButton')), + ], + child: const Text('Submenu'), ), ), ), @@ -4696,19 +4599,15 @@ void main() { return MaterialApp( home: Material( child: Center( - // This is added because a menu item must be a child of a menu or menu bar. - child: Semantics( - role: SemanticsRole.menu, - child: MenuItemButton( - style: MenuItemButton.styleFrom( - iconColor: iconColor, - iconSize: iconSize, - disabledIconColor: disabledIconColor, - ), - onPressed: enabled ? () {} : null, - trailingIcon: const Icon(Icons.add), - child: const Text('Button'), + child: MenuItemButton( + style: MenuItemButton.styleFrom( + iconColor: iconColor, + iconSize: iconSize, + disabledIconColor: disabledIconColor, ), + onPressed: enabled ? () {} : null, + trailingIcon: const Icon(Icons.add), + child: const Text('Button'), ), ), ), @@ -4736,18 +4635,15 @@ void main() { return MaterialApp( home: Material( child: Center( - child: Semantics( - role: SemanticsRole.menu, - child: SubmenuButton( - style: SubmenuButton.styleFrom( - iconColor: iconColor, - iconSize: iconSize, - disabledIconColor: disabledIconColor, - ), - trailingIcon: const Icon(Icons.add), - menuChildren: [if (enabled) const Text('Item')], - child: const Text('SubmenuButton'), + child: SubmenuButton( + style: SubmenuButton.styleFrom( + iconColor: iconColor, + iconSize: iconSize, + disabledIconColor: disabledIconColor, ), + trailingIcon: const Icon(Icons.add), + menuChildren: [if (enabled) const Text('Item')], + child: const Text('SubmenuButton'), ), ), ), diff --git a/packages/flutter/test/widgets/semantics_role_checks_test.dart b/packages/flutter/test/widgets/semantics_role_checks_test.dart index 2ff716132584c..aea1c8567d5b5 100644 --- a/packages/flutter/test/widgets/semantics_role_checks_test.dart +++ b/packages/flutter/test/widgets/semantics_role_checks_test.dart @@ -300,6 +300,43 @@ void main() { }); }); + group('menu', () { + testWidgets('failure case, empty child', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Semantics( + role: SemanticsRole.menu, + child: const ExcludeSemantics(child: Text('something')), + ), + ), + ); + final Object? exception = tester.takeException(); + expect(exception, isFlutterError); + final FlutterError error = exception! as FlutterError; + expect(error.message, 'a menu cannot be empty'); + }); + + testWidgets('Success case', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Semantics( + role: SemanticsRole.menu, + explicitChildNodes: true, + child: Semantics( + role: SemanticsRole.menuItem, + selected: false, + onTap: () {}, + child: const Text('some child'), + ), + ), + ), + ); + expect(tester.takeException(), isNull); + }); + }); + group('menuBar', () { testWidgets('failure case, empty child', (WidgetTester tester) async { await tester.pumpWidget(