Unverified Commit 13a0d475 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Convert menus to use OverlayPortal (#130534)

## Description

This converts the `MenuAnchor` class to use `OverlayPortal` instead of directly using the overlay.

## Related Issues
 - Fixes https://github.com/flutter/flutter/issues/124830

## Tests
 - No tests yet (hence it is a draft)
parent 1599cbeb
...@@ -6,11 +6,12 @@ import 'package:flutter/material.dart'; ...@@ -6,11 +6,12 @@ import 'package:flutter/material.dart';
/// Flutter code sample for [MenuAnchor]. /// Flutter code sample for [MenuAnchor].
// This is the type used by the menu below.
enum SampleItem { itemOne, itemTwo, itemThree }
void main() => runApp(const MenuAnchorApp()); void main() => runApp(const MenuAnchorApp());
// This is the type used by the menu below.
enum SampleItem { itemOne, itemTwo, itemThree }
class MenuAnchorApp extends StatelessWidget { class MenuAnchorApp extends StatelessWidget {
const MenuAnchorApp({super.key}); const MenuAnchorApp({super.key});
...@@ -36,7 +37,10 @@ class _MenuAnchorExampleState extends State<MenuAnchorExample> { ...@@ -36,7 +37,10 @@ class _MenuAnchorExampleState extends State<MenuAnchorExample> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('MenuAnchorButton')), appBar: AppBar(
title: const Text('MenuAnchorButton'),
backgroundColor: Theme.of(context).primaryColorLight,
),
body: Center( body: Center(
child: MenuAnchor( child: MenuAnchor(
builder: builder:
......
...@@ -51,7 +51,18 @@ void main() { ...@@ -51,7 +51,18 @@ void main() {
expect(find.text('Background Color'), findsOneWidget); expect(find.text('Background Color'), findsOneWidget);
// Focusing the background color item with the keyboard caused the submenu
// to open. Tapping it should cause it to close.
await tester.tap(find.text('Background Color')); await tester.tap(find.text('Background Color'));
await tester.pump();
await tester.pumpAndSettle();
expect(find.text(example.MenuEntry.colorRed.label), findsNothing);
expect(find.text(example.MenuEntry.colorGreen.label), findsNothing);
expect(find.text(example.MenuEntry.colorBlue.label), findsNothing);
await tester.tap(find.text('Background Color'));
await tester.pump();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text(example.MenuEntry.colorRed.label), findsOneWidget); expect(find.text(example.MenuEntry.colorRed.label), findsOneWidget);
......
...@@ -291,16 +291,17 @@ class _MenuAnchorState extends State<MenuAnchor> { ...@@ -291,16 +291,17 @@ class _MenuAnchorState extends State<MenuAnchor> {
// for the anchor's region that the CustomSingleChildLayout's delegate // for the anchor's region that the CustomSingleChildLayout's delegate
// uses to determine where to place the menu on the screen and to avoid the // uses to determine where to place the menu on the screen and to avoid the
// view's edges. // view's edges.
final GlobalKey _anchorKey = GlobalKey(debugLabel: kReleaseMode ? null : 'MenuAnchor'); final GlobalKey<_MenuAnchorState> _anchorKey = GlobalKey<_MenuAnchorState>(debugLabel: kReleaseMode ? null : 'MenuAnchor');
_MenuAnchorState? _parent; _MenuAnchorState? _parent;
final FocusScopeNode _menuScopeNode = FocusScopeNode(debugLabel: kReleaseMode ? null : 'MenuAnchor sub menu'); late final FocusScopeNode _menuScopeNode;
MenuController? _internalMenuController; MenuController? _internalMenuController;
final List<_MenuAnchorState> _anchorChildren = <_MenuAnchorState>[]; final List<_MenuAnchorState> _anchorChildren = <_MenuAnchorState>[];
ScrollPosition? _position; ScrollPosition? _scrollPosition;
Size? _viewSize; Size? _viewSize;
OverlayEntry? _overlayEntry; final OverlayPortalController _overlayController = OverlayPortalController(debugLabel: kReleaseMode ? null : 'MenuAnchor controller');
Offset? _menuPosition;
Axis get _orientation => Axis.vertical; Axis get _orientation => Axis.vertical;
bool get _isOpen => _overlayEntry != null; bool get _isOpen => _overlayController.isShowing;
bool get _isRoot => _parent == null; bool get _isRoot => _parent == null;
bool get _isTopLevel => _parent?._isRoot ?? false; bool get _isTopLevel => _parent?._isRoot ?? false;
MenuController get _menuController => widget.controller ?? _internalMenuController!; MenuController get _menuController => widget.controller ?? _internalMenuController!;
...@@ -308,6 +309,7 @@ class _MenuAnchorState extends State<MenuAnchor> { ...@@ -308,6 +309,7 @@ class _MenuAnchorState extends State<MenuAnchor> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_menuScopeNode = FocusScopeNode(debugLabel: kReleaseMode ? null : '${describeIdentity(this)} Sub Menu');
if (widget.controller == null) { if (widget.controller == null) {
_internalMenuController = MenuController(); _internalMenuController = MenuController();
} }
...@@ -331,12 +333,15 @@ class _MenuAnchorState extends State<MenuAnchor> { ...@@ -331,12 +333,15 @@ class _MenuAnchorState extends State<MenuAnchor> {
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
_parent?._removeChild(this); final _MenuAnchorState? newParent = _MenuAnchorState._maybeOf(context);
_parent = _MenuAnchorState._maybeOf(context); if (newParent != _parent) {
_parent?._addChild(this); _parent?._removeChild(this);
_position?.isScrollingNotifier.removeListener(_handleScroll); _parent = newParent;
_position = Scrollable.maybeOf(context)?.position; _parent?._addChild(this);
_position?.isScrollingNotifier.addListener(_handleScroll); }
_scrollPosition?.isScrollingNotifier.removeListener(_handleScroll);
_scrollPosition = Scrollable.maybeOf(context)?.position;
_scrollPosition?.isScrollingNotifier.addListener(_handleScroll);
final Size newSize = MediaQuery.sizeOf(context); final Size newSize = MediaQuery.sizeOf(context);
if (_viewSize != null && newSize != _viewSize) { if (_viewSize != null && newSize != _viewSize) {
// Close the menus if the view changes size. // Close the menus if the view changes size.
...@@ -360,18 +365,25 @@ class _MenuAnchorState extends State<MenuAnchor> { ...@@ -360,18 +365,25 @@ class _MenuAnchorState extends State<MenuAnchor> {
} }
} }
assert(_menuController._anchor == this); assert(_menuController._anchor == this);
if (_overlayEntry != null) {
// Needs to update the overlay entry on the next frame, since it's in the
// overlay.
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
_overlayEntry?.markNeedsBuild();
});
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget child = _buildContents(context); Widget child = OverlayPortal(
controller: _overlayController,
overlayChildBuilder: (BuildContext context) {
return _Submenu(
anchor: this,
menuStyle: widget.style,
alignmentOffset: widget.alignmentOffset ?? Offset.zero,
menuPosition: _menuPosition,
clipBehavior: widget.clipBehavior,
menuChildren: widget.menuChildren,
crossAxisUnconstrained: widget.crossAxisUnconstrained,
);
},
child: _buildContents(context),
);
if (!widget.anchorTapClosesMenu) { if (!widget.anchorTapClosesMenu) {
child = TapRegion( child = TapRegion(
...@@ -394,18 +406,20 @@ class _MenuAnchorState extends State<MenuAnchor> { ...@@ -394,18 +406,20 @@ class _MenuAnchorState extends State<MenuAnchor> {
} }
Widget _buildContents(BuildContext context) { Widget _buildContents(BuildContext context) {
return Builder( return Actions(
key: _anchorKey, actions: <Type, Action<Intent>>{
builder: (BuildContext context) { DirectionalFocusIntent: _MenuDirectionalFocusAction(),
if (widget.builder == null) { PreviousFocusIntent: _MenuPreviousFocusAction(),
return widget.child ?? const SizedBox(); NextFocusIntent: _MenuNextFocusAction(),
} DismissIntent: DismissMenuAction(controller: _menuController),
return widget.builder!(
context,
_menuController,
widget.child,
);
}, },
child: Builder(
key: _anchorKey,
builder: (BuildContext context) {
return widget.builder?.call(context, _menuController, widget.child)
?? widget.child ?? const SizedBox();
},
),
); );
} }
...@@ -424,33 +438,42 @@ class _MenuAnchorState extends State<MenuAnchor> { ...@@ -424,33 +438,42 @@ class _MenuAnchorState extends State<MenuAnchor> {
assert(_isRoot || _debugMenuInfo('Added root child: $child')); assert(_isRoot || _debugMenuInfo('Added root child: $child'));
assert(!_anchorChildren.contains(child)); assert(!_anchorChildren.contains(child));
_anchorChildren.add(child); _anchorChildren.add(child);
assert(_debugMenuInfo('Added:\n${child.widget.toStringDeep()}'));
assert(_debugMenuInfo('Tree:\n${widget.toStringDeep()}')); assert(_debugMenuInfo('Tree:\n${widget.toStringDeep()}'));
} }
void _removeChild(_MenuAnchorState child) { void _removeChild(_MenuAnchorState child) {
assert(_isRoot || _debugMenuInfo('Removed root child: $child')); assert(_isRoot || _debugMenuInfo('Removed root child: $child'));
assert(_anchorChildren.contains(child)); assert(_anchorChildren.contains(child));
assert(_debugMenuInfo('Removing:\n${child.widget.toStringDeep()}'));
_anchorChildren.remove(child); _anchorChildren.remove(child);
assert(_debugMenuInfo('Tree:\n${widget.toStringDeep()}')); assert(_debugMenuInfo('Tree:\n${widget.toStringDeep()}'));
} }
_MenuAnchorState? get _nextSibling { List<_MenuAnchorState> _getFocusableChildren() {
final int index = _parent!._anchorChildren.indexOf(this); if (_parent == null) {
assert(index != -1, 'Unable to find this widget $this in parent $_parent'); return <_MenuAnchorState>[];
if (index < _parent!._anchorChildren.length - 1) {
return _parent!._anchorChildren[index + 1];
} }
return null; return _parent!._anchorChildren.where((_MenuAnchorState menu) {
return menu.widget.childFocusNode?.canRequestFocus ?? false;
},).toList();
} }
_MenuAnchorState? get _previousSibling { _MenuAnchorState? get _nextFocusableSibling {
final int index = _parent!._anchorChildren.indexOf(this); final List<_MenuAnchorState> focusable = _getFocusableChildren();
assert(index != -1, 'Unable to find this widget $this in parent $_parent'); if (focusable.isEmpty) {
if (index > 0) { return null;
return _parent!._anchorChildren[index - 1]; }
} return focusable[(focusable.indexOf(this) + 1) % focusable.length];
}
_MenuAnchorState? get _previousFocusableSibling {
final List<_MenuAnchorState> focusable = _getFocusableChildren();
if (focusable.isEmpty) {
return null; return null;
} }
return focusable[(focusable.indexOf(this) - 1 + focusable.length) % focusable.length];
}
_MenuAnchorState get _root { _MenuAnchorState get _root {
_MenuAnchorState anchor = this; _MenuAnchorState anchor = this;
...@@ -462,19 +485,27 @@ class _MenuAnchorState extends State<MenuAnchor> { ...@@ -462,19 +485,27 @@ class _MenuAnchorState extends State<MenuAnchor> {
_MenuAnchorState get _topLevel { _MenuAnchorState get _topLevel {
_MenuAnchorState handle = this; _MenuAnchorState handle = this;
while (handle._parent!._isTopLevel) { while (handle._parent != null && !handle._parent!._isTopLevel) {
handle = handle._parent!; handle = handle._parent!;
} }
return handle; return handle;
} }
void _childChangedOpenState() { void _childChangedOpenState() {
if (mounted) { _parent?._childChangedOpenState();
_parent?._childChangedOpenState(); assert(mounted);
if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
setState(() { setState(() {
// Mark dirty, but only if mounted. // Mark dirty now, but only if not in a build.
});
} else {
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
setState(() {
// Mark dirty after this frame, but only if in a build.
});
}); });
} }
} }
void _focusButton() { void _focusButton() {
...@@ -524,53 +555,12 @@ class _MenuAnchorState extends State<MenuAnchor> { ...@@ -524,53 +555,12 @@ class _MenuAnchorState extends State<MenuAnchor> {
assert(_debugMenuInfo( assert(_debugMenuInfo(
'Opening $this at ${position ?? Offset.zero} with alignment offset ${widget.alignmentOffset ?? Offset.zero}')); 'Opening $this at ${position ?? Offset.zero} with alignment offset ${widget.alignmentOffset ?? Offset.zero}'));
_parent?._closeChildren(); // Close all siblings. _parent?._closeChildren(); // Close all siblings.
assert(_overlayEntry == null); assert(!_overlayController.isShowing);
final BuildContext outerContext = context;
_parent?._childChangedOpenState(); _parent?._childChangedOpenState();
setState(() { _menuPosition = position;
_overlayEntry = OverlayEntry( _overlayController.show();
builder: (BuildContext context) {
final OverlayState overlay = Overlay.of(outerContext);
return Positioned.directional(
textDirection: Directionality.of(outerContext),
top: 0,
start: 0,
child: Directionality(
textDirection: Directionality.of(outerContext),
child: InheritedTheme.captureAll(
// Copy all the themes from the supplied outer context to the
// overlay.
outerContext,
_MenuAnchorScope(
// Re-advertize the anchor here in the overlay, since
// otherwise a search for the anchor by descendants won't find
// it.
anchorKey: _anchorKey,
anchor: this,
isOpen: _isOpen,
child: _Submenu(
anchor: this,
menuStyle: widget.style,
alignmentOffset: widget.alignmentOffset ?? Offset.zero,
menuPosition: position,
clipBehavior: widget.clipBehavior,
menuChildren: widget.menuChildren,
crossAxisUnconstrained: widget.crossAxisUnconstrained,
),
),
to: overlay.context,
),
),
);
},
);
});
if (_isRoot) {
FocusManager.instance.addEarlyKeyEventHandler(_checkForEscape);
}
Overlay.of(context).insert(_overlayEntry!);
widget.onOpen?.call(); widget.onOpen?.call();
} }
...@@ -587,15 +577,24 @@ class _MenuAnchorState extends State<MenuAnchor> { ...@@ -587,15 +577,24 @@ class _MenuAnchorState extends State<MenuAnchor> {
FocusManager.instance.removeEarlyKeyEventHandler(_checkForEscape); FocusManager.instance.removeEarlyKeyEventHandler(_checkForEscape);
} }
_closeChildren(inDispose: inDispose); _closeChildren(inDispose: inDispose);
_overlayEntry?.remove(); // Don't hide if we're in the middle of a build.
_overlayEntry?.dispose(); if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
_overlayEntry = null; _overlayController.hide();
} else if (!inDispose) {
SchedulerBinding.instance.addPostFrameCallback((_) {
_overlayController.hide();
});
}
if (!inDispose) { if (!inDispose) {
// Notify that _childIsOpen changed state, but only if not // Notify that _childIsOpen changed state, but only if not
// currently disposing. // currently disposing.
_parent?._childChangedOpenState(); _parent?._childChangedOpenState();
widget.onClose?.call(); widget.onClose?.call();
setState(() {}); if (mounted && SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
setState(() {
// Mark dirty, but only if mounted and not in a build.
});
}
} }
} }
...@@ -612,6 +611,11 @@ class _MenuAnchorState extends State<MenuAnchor> { ...@@ -612,6 +611,11 @@ class _MenuAnchorState extends State<MenuAnchor> {
static _MenuAnchorState? _maybeOf(BuildContext context) { static _MenuAnchorState? _maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<_MenuAnchorScope>()?.anchor; return context.dependOnInheritedWidgetOfExactType<_MenuAnchorScope>()?.anchor;
} }
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.debug}) {
return describeIdentity(this);
}
} }
/// A controller to manage a menu created by a [MenuBar] or [MenuAnchor]. /// A controller to manage a menu created by a [MenuBar] or [MenuAnchor].
...@@ -1901,6 +1905,7 @@ class _SubmenuButtonState extends State<SubmenuButton> { ...@@ -1901,6 +1905,7 @@ class _SubmenuButtonState extends State<SubmenuButton> {
}); });
_waitingToFocusMenu = true; _waitingToFocusMenu = true;
} }
setState(() { /* Rebuild with updated controller.isOpen value */ });
widget.onOpen?.call(); widget.onOpen?.call();
}, },
style: widget.menuStyle, style: widget.menuStyle,
...@@ -1911,9 +1916,7 @@ class _SubmenuButtonState extends State<SubmenuButton> { ...@@ -1911,9 +1916,7 @@ class _SubmenuButtonState extends State<SubmenuButton> {
// once. // once.
ButtonStyle mergedStyle = widget.themeStyleOf(context)?.merge(widget.defaultStyleOf(context)) ButtonStyle mergedStyle = widget.themeStyleOf(context)?.merge(widget.defaultStyleOf(context))
?? widget.defaultStyleOf(context); ?? widget.defaultStyleOf(context);
if (widget.style != null) { mergedStyle = widget.style?.merge(mergedStyle) ?? mergedStyle;
mergedStyle = widget.style!.merge(mergedStyle);
}
void toggleShowMenu(BuildContext context) { void toggleShowMenu(BuildContext context) {
if (controller._anchor == null) { if (controller._anchor == null) {
...@@ -1944,7 +1947,7 @@ class _SubmenuButtonState extends State<SubmenuButton> { ...@@ -1944,7 +1947,7 @@ class _SubmenuButtonState extends State<SubmenuButton> {
} }
child = MergeSemantics( child = MergeSemantics(
child: Semantics( child: Semantics(
expanded: controller.isOpen, expanded: _enabled && controller.isOpen,
child: TextButton( child: TextButton(
style: mergedStyle, style: mergedStyle,
focusNode: _buttonFocusNode, focusNode: _buttonFocusNode,
...@@ -2330,17 +2333,20 @@ class _MenuBarAnchorState extends _MenuAnchorState { ...@@ -2330,17 +2333,20 @@ class _MenuBarAnchorState extends _MenuAnchorState {
@override @override
Widget _buildContents(BuildContext context) { Widget _buildContents(BuildContext context) {
final bool isOpen = _isOpen;
return FocusScope( return FocusScope(
node: _menuScopeNode, node: _menuScopeNode,
skipTraversal: !_isOpen, skipTraversal: !isOpen,
canRequestFocus: _isOpen, canRequestFocus: isOpen,
child: ExcludeFocus( child: ExcludeFocus(
excluding: !_isOpen, excluding: !isOpen,
child: Shortcuts( child: Shortcuts(
shortcuts: _kMenuTraversalShortcuts, shortcuts: _kMenuTraversalShortcuts,
child: Actions( child: Actions(
actions: <Type, Action<Intent>>{ actions: <Type, Action<Intent>>{
DirectionalFocusIntent: _MenuDirectionalFocusAction(), DirectionalFocusIntent: _MenuDirectionalFocusAction(),
PreviousFocusIntent: _MenuPreviousFocusAction(),
NextFocusIntent: _MenuNextFocusAction(),
DismissIntent: DismissMenuAction(controller: _menuController), DismissIntent: DismissMenuAction(controller: _menuController),
}, },
child: Builder(builder: (BuildContext context) { child: Builder(builder: (BuildContext context) {
...@@ -2365,6 +2371,53 @@ class _MenuBarAnchorState extends _MenuAnchorState { ...@@ -2365,6 +2371,53 @@ class _MenuBarAnchorState extends _MenuAnchorState {
} }
} }
class _MenuPreviousFocusAction extends PreviousFocusAction {
@override
bool invoke(PreviousFocusIntent intent) {
assert(_debugMenuInfo('_MenuNextFocusAction invoked with $intent'));
final BuildContext? context = FocusManager.instance.primaryFocus?.context;
if (context == null) {
return super.invoke(intent);
}
final _MenuAnchorState? anchor = _MenuAnchorState._maybeOf(context);
if (anchor == null || !anchor._root._isOpen) {
return super.invoke(intent);
}
return _moveToPreviousFocusable(anchor);
}
static bool _moveToPreviousFocusable(_MenuAnchorState currentMenu) {
final _MenuAnchorState? sibling = currentMenu._previousFocusableSibling;
sibling?._focusButton();
return true;
}
}
class _MenuNextFocusAction extends NextFocusAction {
@override
bool invoke(NextFocusIntent intent) {
assert(_debugMenuInfo('_MenuNextFocusAction invoked with $intent'));
final BuildContext? context = FocusManager.instance.primaryFocus?.context;
if (context == null) {
return super.invoke(intent);
}
final _MenuAnchorState? anchor = _MenuAnchorState._maybeOf(context);
if (anchor == null || !anchor._root._isOpen) {
return super.invoke(intent);
}
return _moveToNextFocusable(anchor);
}
static bool _moveToNextFocusable(_MenuAnchorState currentMenu) {
final _MenuAnchorState? sibling = currentMenu._nextFocusableSibling;
sibling?._focusButton();
return true;
}
}
class _MenuDirectionalFocusAction extends DirectionalFocusAction { class _MenuDirectionalFocusAction extends DirectionalFocusAction {
/// Creates a [DirectionalFocusAction]. /// Creates a [DirectionalFocusAction].
_MenuDirectionalFocusAction(); _MenuDirectionalFocusAction();
...@@ -2384,7 +2437,7 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction { ...@@ -2384,7 +2437,7 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction {
} }
final bool buttonIsFocused = anchor.widget.childFocusNode?.hasPrimaryFocus ?? false; final bool buttonIsFocused = anchor.widget.childFocusNode?.hasPrimaryFocus ?? false;
Axis orientation; Axis orientation;
if (buttonIsFocused) { if (buttonIsFocused && anchor._parent != null) {
orientation = anchor._parent!._orientation; orientation = anchor._parent!._orientation;
} else { } else {
orientation = anchor._orientation; orientation = anchor._orientation;
...@@ -2442,19 +2495,20 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction { ...@@ -2442,19 +2495,20 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction {
return; return;
} }
} else { } else {
if (_moveToNextTopLevel(anchor)) { if (_moveToNextFocusableTopLevel(anchor)) {
return; return;
} }
} }
case TextDirection.ltr: case TextDirection.ltr:
switch (anchor._parent!._orientation) { switch (anchor._parent?._orientation) {
case Axis.horizontal: case Axis.horizontal:
if (_moveToPreviousTopLevel(anchor)) { case null:
if (_moveToPreviousFocusableTopLevel(anchor)) {
return; return;
} }
case Axis.vertical: case Axis.vertical:
if (buttonIsFocused) { if (buttonIsFocused) {
if (_moveToPreviousTopLevel(anchor)) { if (_moveToPreviousFocusableTopLevel(anchor)) {
return; return;
} }
} else { } else {
...@@ -2481,9 +2535,10 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction { ...@@ -2481,9 +2535,10 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction {
case Axis.vertical: case Axis.vertical:
switch (Directionality.of(context)) { switch (Directionality.of(context)) {
case TextDirection.rtl: case TextDirection.rtl:
switch (anchor._parent!._orientation) { switch (anchor._parent?._orientation) {
case Axis.horizontal: case Axis.horizontal:
if (_moveToPreviousTopLevel(anchor)) { case null:
if (_moveToPreviousFocusableTopLevel(anchor)) {
return; return;
} }
case Axis.vertical: case Axis.vertical:
...@@ -2497,7 +2552,7 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction { ...@@ -2497,7 +2552,7 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction {
return; return;
} }
} else { } else {
if (_moveToNextTopLevel(anchor)) { if (_moveToNextFocusableTopLevel(anchor)) {
return; return;
} }
} }
...@@ -2513,24 +2568,18 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction { ...@@ -2513,24 +2568,18 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction {
// otherwise the anti-hysteresis code will interfere with moving to the // otherwise the anti-hysteresis code will interfere with moving to the
// correct node. // correct node.
if (currentMenu.widget.childFocusNode != null) { if (currentMenu.widget.childFocusNode != null) {
final FocusTraversalPolicy? policy = FocusTraversalGroup.maybeOf(primaryFocus!.context!);
if (currentMenu.widget.childFocusNode!.nearestScope != null) { if (currentMenu.widget.childFocusNode!.nearestScope != null) {
final FocusTraversalPolicy? policy = FocusTraversalGroup.maybeOf(primaryFocus!.context!);
policy?.invalidateScopeData(currentMenu.widget.childFocusNode!.nearestScope!); policy?.invalidateScopeData(currentMenu.widget.childFocusNode!.nearestScope!);
} }
return false;
} }
return false; return false;
} }
bool _moveToNextTopLevel(_MenuAnchorState currentMenu) { bool _moveToNextFocusableTopLevel(_MenuAnchorState currentMenu) {
final _MenuAnchorState? sibling = currentMenu._topLevel._nextSibling; final _MenuAnchorState? sibling = currentMenu._topLevel._nextFocusableSibling;
if (sibling == null) { sibling?._focusButton();
// Wrap around to the first top level. return true;
currentMenu._topLevel._parent!._anchorChildren.first._focusButton();
} else {
sibling._focusButton();
}
return true;
} }
bool _moveToParent(_MenuAnchorState currentMenu) { bool _moveToParent(_MenuAnchorState currentMenu) {
...@@ -2547,23 +2596,17 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction { ...@@ -2547,23 +2596,17 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction {
// otherwise the anti-hysteresis code will interfere with moving to the // otherwise the anti-hysteresis code will interfere with moving to the
// correct node. // correct node.
if (currentMenu.widget.childFocusNode != null) { if (currentMenu.widget.childFocusNode != null) {
final FocusTraversalPolicy? policy = FocusTraversalGroup.maybeOf(primaryFocus!.context!);
if (currentMenu.widget.childFocusNode!.nearestScope != null) { if (currentMenu.widget.childFocusNode!.nearestScope != null) {
final FocusTraversalPolicy? policy = FocusTraversalGroup.maybeOf(primaryFocus!.context!);
policy?.invalidateScopeData(currentMenu.widget.childFocusNode!.nearestScope!); policy?.invalidateScopeData(currentMenu.widget.childFocusNode!.nearestScope!);
} }
return false;
} }
return false; return false;
} }
bool _moveToPreviousTopLevel(_MenuAnchorState currentMenu) { bool _moveToPreviousFocusableTopLevel(_MenuAnchorState currentMenu) {
final _MenuAnchorState? sibling = currentMenu._topLevel._previousSibling; final _MenuAnchorState? sibling = currentMenu._topLevel._previousFocusableSibling;
if (sibling == null) { sibling?._focusButton();
// Already on the first one, wrap around to the last one.
currentMenu._topLevel._parent!._anchorChildren.last._focusButton();
} else {
sibling._focusButton();
}
return true; return true;
} }
...@@ -2903,7 +2946,7 @@ class _MenuAcceleratorLabelState extends State<MenuAcceleratorLabel> { ...@@ -2903,7 +2946,7 @@ class _MenuAcceleratorLabelState extends State<MenuAcceleratorLabel> {
super.initState(); super.initState();
if (_platformSupportsAccelerators) { if (_platformSupportsAccelerators) {
_showAccelerators = _altIsPressed(); _showAccelerators = _altIsPressed();
HardwareKeyboard.instance.addHandler(_handleKeyEvent); HardwareKeyboard.instance.addHandler(_listenToKeyEvent);
} }
_updateDisplayLabel(); _updateDisplayLabel();
} }
...@@ -2917,7 +2960,7 @@ class _MenuAcceleratorLabelState extends State<MenuAcceleratorLabel> { ...@@ -2917,7 +2960,7 @@ class _MenuAcceleratorLabelState extends State<MenuAcceleratorLabel> {
_shortcutRegistryEntry = null; _shortcutRegistryEntry = null;
_shortcutRegistry = null; _shortcutRegistry = null;
_anchor = null; _anchor = null;
HardwareKeyboard.instance.removeHandler(_handleKeyEvent); HardwareKeyboard.instance.removeHandler(_listenToKeyEvent);
} }
super.dispose(); super.dispose();
} }
...@@ -2952,16 +2995,13 @@ class _MenuAcceleratorLabelState extends State<MenuAcceleratorLabel> { ...@@ -2952,16 +2995,13 @@ class _MenuAcceleratorLabelState extends State<MenuAcceleratorLabel> {
).isNotEmpty; ).isNotEmpty;
} }
bool _handleKeyEvent(KeyEvent event) { bool _listenToKeyEvent(KeyEvent event) {
assert(_platformSupportsAccelerators); assert(_platformSupportsAccelerators);
final bool altIsPressed = _altIsPressed(); setState(() {
if (altIsPressed != _showAccelerators) { _showAccelerators = _altIsPressed();
setState(() { _updateAcceleratorShortcut();
_showAccelerators = altIsPressed; });
_updateAcceleratorShortcut(); // Just listening, so it doesn't ever handle a key.
});
}
// Just listening, does't ever handle a key.
return false; return false;
} }
...@@ -2979,7 +3019,7 @@ class _MenuAcceleratorLabelState extends State<MenuAcceleratorLabel> { ...@@ -2979,7 +3019,7 @@ class _MenuAcceleratorLabelState extends State<MenuAcceleratorLabel> {
// 4) Is part of an anchor that either doesn't have a submenu, or doesn't // 4) Is part of an anchor that either doesn't have a submenu, or doesn't
// have any submenus currently open (only the "deepest" open menu should // have any submenus currently open (only the "deepest" open menu should
// have accelerator shortcuts registered). // have accelerator shortcuts registered).
if (_showAccelerators && _acceleratorIndex != -1 && _binding?.onInvoke != null && !(_binding!.hasSubmenu && (_anchor?._isOpen ?? false))) { if (_showAccelerators && _acceleratorIndex != -1 && _binding?.onInvoke != null && (!_binding!.hasSubmenu || !(_anchor?._isOpen ?? false))) {
final String acceleratorCharacter = _displayLabel[_acceleratorIndex].toLowerCase(); final String acceleratorCharacter = _displayLabel[_acceleratorIndex].toLowerCase();
_shortcutRegistryEntry = _shortcutRegistry?.addAll( _shortcutRegistryEntry = _shortcutRegistry?.addAll(
<ShortcutActivator, Intent>{ <ShortcutActivator, Intent>{
...@@ -3452,7 +3492,7 @@ class _MenuPanelState extends State<_MenuPanel> { ...@@ -3452,7 +3492,7 @@ class _MenuPanelState extends State<_MenuPanel> {
} }
} }
// A widget that defines the menu drawn inside of the overlay entry. // A widget that defines the menu drawn in the overlay.
class _Submenu extends StatelessWidget { class _Submenu extends StatelessWidget {
const _Submenu({ const _Submenu({
required this.anchor, required this.anchor,
...@@ -3552,6 +3592,7 @@ class _Submenu extends StatelessWidget { ...@@ -3552,6 +3592,7 @@ class _Submenu extends StatelessWidget {
hitTestBehavior: HitTestBehavior.deferToChild, hitTestBehavior: HitTestBehavior.deferToChild,
child: FocusScope( child: FocusScope(
node: anchor._menuScopeNode, node: anchor._menuScopeNode,
skipTraversal: true,
child: Actions( child: Actions(
actions: <Type, Action<Intent>>{ actions: <Type, Action<Intent>>{
DirectionalFocusIntent: _MenuDirectionalFocusAction(), DirectionalFocusIntent: _MenuDirectionalFocusAction(),
...@@ -3559,16 +3600,12 @@ class _Submenu extends StatelessWidget { ...@@ -3559,16 +3600,12 @@ class _Submenu extends StatelessWidget {
}, },
child: Shortcuts( child: Shortcuts(
shortcuts: _kMenuTraversalShortcuts, shortcuts: _kMenuTraversalShortcuts,
child: Directionality( child: _MenuPanel(
// Copy the directionality from the button into the overlay. menuStyle: menuStyle,
textDirection: textDirection, clipBehavior: clipBehavior,
child: _MenuPanel( orientation: anchor._orientation,
menuStyle: menuStyle, crossAxisUnconstrained: crossAxisUnconstrained,
clipBehavior: clipBehavior, children: menuChildren,
orientation: anchor._orientation,
crossAxisUnconstrained: crossAxisUnconstrained,
children: menuChildren,
),
), ),
), ),
), ),
......
...@@ -257,7 +257,7 @@ abstract class ViewportOffset extends ChangeNotifier { ...@@ -257,7 +257,7 @@ abstract class ViewportOffset extends ChangeNotifier {
/// Whether a viewport is allowed to change [pixels] implicitly to respond to /// Whether a viewport is allowed to change [pixels] implicitly to respond to
/// a call to [RenderObject.showOnScreen]. /// a call to [RenderObject.showOnScreen].
/// ///
/// [RenderObject.showOnScreen] is for example used to bring a text field /// [RenderObject.showOnScreen] is, for example, used to bring a text field
/// fully on screen after it has received focus. This property controls /// fully on screen after it has received focus. This property controls
/// whether the viewport associated with this offset is allowed to change the /// whether the viewport associated with this offset is allowed to change the
/// offset's [pixels] value to fulfill such a request. /// offset's [pixels] value to fulfill such a request.
......
...@@ -189,7 +189,8 @@ abstract class FocusTraversalPolicy with Diagnosticable { ...@@ -189,7 +189,8 @@ abstract class FocusTraversalPolicy with Diagnosticable {
}) { }) {
node.requestFocus(); node.requestFocus();
Scrollable.ensureVisible( Scrollable.ensureVisible(
node.context!, alignment: alignment ?? 1.0, node.context!,
alignment: alignment ?? 1,
alignmentPolicy: alignmentPolicy ?? ScrollPositionAlignmentPolicy.explicit, alignmentPolicy: alignmentPolicy ?? ScrollPositionAlignmentPolicy.explicit,
duration: duration ?? Duration.zero, duration: duration ?? Duration.zero,
curve: curve ?? Curves.ease, curve: curve ?? Curves.ease,
...@@ -467,7 +468,6 @@ abstract class FocusTraversalPolicy with Diagnosticable { ...@@ -467,7 +468,6 @@ abstract class FocusTraversalPolicy with Diagnosticable {
groups[key]!.members.addAll(sortedMembers); groups[key]!.members.addAll(sortedMembers);
} }
// Traverse the group tree, adding the children of members in the order they // Traverse the group tree, adding the children of members in the order they
// appear in the member lists. // appear in the member lists.
final List<FocusNode> sortedDescendants = <FocusNode>[]; final List<FocusNode> sortedDescendants = <FocusNode>[];
...@@ -504,9 +504,9 @@ abstract class FocusTraversalPolicy with Diagnosticable { ...@@ -504,9 +504,9 @@ abstract class FocusTraversalPolicy with Diagnosticable {
// The scope.traversalDescendants will not contain currentNode if it // The scope.traversalDescendants will not contain currentNode if it
// skips traversal or not focusable. // skips traversal or not focusable.
assert( assert(
difference.length == 1 && difference.contains(currentNode), difference.isEmpty || (difference.length == 1 && difference.contains(currentNode)),
'Sorted descendants contains different nodes than FocusScopeNode.traversalDescendants would. ' 'Difference between sorted descendants and FocusScopeNode.traversalDescendants contains '
'These are the different nodes: ${difference.where((FocusNode node) => node != currentNode)}', 'something other than the current skipped node. This is the difference: $difference',
); );
return true; return true;
} }
......
...@@ -1414,7 +1414,7 @@ class OverlayPortalController { ...@@ -1414,7 +1414,7 @@ class OverlayPortalController {
: _zOrderIndex != null; : _zOrderIndex != null;
} }
/// Conventience method for toggling the current [isShowing] status. /// Convenience method for toggling the current [isShowing] status.
/// ///
/// This method should typically not be called while the widget tree is being /// This method should typically not be called while the widget tree is being
/// rebuilt. /// rebuilt.
......
...@@ -795,9 +795,13 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -795,9 +795,13 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
Curve curve = Curves.ease, Curve curve = Curves.ease,
ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit, ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
RenderObject? targetRenderObject, RenderObject? targetRenderObject,
}) { }) async {
assert(object.attached); assert(object.attached);
final RenderAbstractViewport viewport = RenderAbstractViewport.of(object); final RenderAbstractViewport? viewport = RenderAbstractViewport.maybeOf(object);
// If no viewport is found, return.
if (viewport == null) {
return;
}
Rect? targetRect; Rect? targetRect;
if (targetRenderObject != null && targetRenderObject != object) { if (targetRenderObject != null && targetRenderObject != object) {
...@@ -842,12 +846,12 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -842,12 +846,12 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
} }
if (target == pixels) { if (target == pixels) {
return Future<void>.value(); return;
} }
if (duration == Duration.zero) { if (duration == Duration.zero) {
jumpTo(target); jumpTo(target);
return Future<void>.value(); return;
} }
return animateTo(target, duration: duration, curve: curve); return animateTo(target, duration: duration, curve: curve);
......
...@@ -461,11 +461,12 @@ class Scrollable extends StatefulWidget { ...@@ -461,11 +461,12 @@ class Scrollable extends StatefulWidget {
}) { }) {
final List<Future<void>> futures = <Future<void>>[]; final List<Future<void>> futures = <Future<void>>[];
// The `targetRenderObject` is used to record the first target renderObject. // The targetRenderObject is used to record the first target renderObject.
// If there are multiple scrollable widgets nested, we should let // If there are multiple scrollable widgets nested, the targetRenderObject
// the `targetRenderObject` as visible as possible to improve the user experience. // is made to be as visible as possible to improve the user experience. If
// Otherwise, let the outer renderObject as visible as possible maybe cause // the targetRenderObject is already visible, then let the outer
// the `targetRenderObject` invisible. // renderObject be as visible as possible.
//
// Also see https://github.com/flutter/flutter/issues/65100 // Also see https://github.com/flutter/flutter/issues/65100
RenderObject? targetRenderObject; RenderObject? targetRenderObject;
ScrollableState? scrollable = Scrollable.maybeOf(context); ScrollableState? scrollable = Scrollable.maybeOf(context);
...@@ -481,7 +482,7 @@ class Scrollable extends StatefulWidget { ...@@ -481,7 +482,7 @@ class Scrollable extends StatefulWidget {
); );
futures.addAll(newFutures); futures.addAll(newFutures);
targetRenderObject = targetRenderObject ?? context.findRenderObject(); targetRenderObject ??= context.findRenderObject();
context = scrollable.context; context = scrollable.context;
scrollable = Scrollable.maybeOf(context); scrollable = Scrollable.maybeOf(context);
} }
......
...@@ -61,7 +61,7 @@ void main() { ...@@ -61,7 +61,7 @@ void main() {
final Finder menuMaterial = find.ancestor( final Finder menuMaterial = find.ancestor(
of: find.widgetWithText(TextButton, TestMenu.mainMenu0.label), of: find.widgetWithText(TextButton, TestMenu.mainMenu0.label),
matching: find.byType(Material), matching: find.byType(Material),
).last; ).at(1);
Material material = tester.widget<Material>(menuMaterial); Material material = tester.widget<Material>(menuMaterial);
expect(material.color, themeData.colorScheme.surface); expect(material.color, themeData.colorScheme.surface);
expect(material.shadowColor, themeData.colorScheme.shadow); expect(material.shadowColor, themeData.colorScheme.shadow);
...@@ -143,7 +143,7 @@ void main() { ...@@ -143,7 +143,7 @@ void main() {
final Finder menuMaterial = find.ancestor( final Finder menuMaterial = find.ancestor(
of: find.byType(SingleChildScrollView), of: find.byType(SingleChildScrollView),
matching: find.byType(Material), matching: find.byType(Material),
); ).first;
final Size menuSize = tester.getSize(menuMaterial); final Size menuSize = tester.getSize(menuMaterial);
expect(menuSize, const Size(180.0, 304.0)); expect(menuSize, const Size(180.0, 304.0));
...@@ -161,7 +161,7 @@ void main() { ...@@ -161,7 +161,7 @@ void main() {
final Finder updatedMenu = find.ancestor( final Finder updatedMenu = find.ancestor(
of: find.byType(SingleChildScrollView), of: find.byType(SingleChildScrollView),
matching: find.byType(Material), matching: find.byType(Material),
); ).first;
final double updatedMenuWidth = tester.getSize(updatedMenu).width; final double updatedMenuWidth = tester.getSize(updatedMenu).width;
expect(updatedMenuWidth, 200.0); expect(updatedMenuWidth, 200.0);
}); });
...@@ -192,7 +192,7 @@ void main() { ...@@ -192,7 +192,7 @@ void main() {
final Finder menuMaterial = find.ancestor( final Finder menuMaterial = find.ancestor(
of: find.byType(SingleChildScrollView), of: find.byType(SingleChildScrollView),
matching: find.byType(Material), matching: find.byType(Material),
); ).first;
final double menuWidth = tester.getSize(menuMaterial).width; final double menuWidth = tester.getSize(menuMaterial).width;
expect(menuWidth, closeTo(180.5, 0.1)); expect(menuWidth, closeTo(180.5, 0.1));
...@@ -210,7 +210,7 @@ void main() { ...@@ -210,7 +210,7 @@ void main() {
final Finder updatedMenu = find.ancestor( final Finder updatedMenu = find.ancestor(
of: find.byType(SingleChildScrollView), of: find.byType(SingleChildScrollView),
matching: find.byType(Material), matching: find.byType(Material),
); ).first;
final double updatedMenuWidth = tester.getSize(updatedMenu).width; final double updatedMenuWidth = tester.getSize(updatedMenu).width;
expect(updatedMenuWidth, 200.0); expect(updatedMenuWidth, 200.0);
}); });
...@@ -644,8 +644,8 @@ void main() { ...@@ -644,8 +644,8 @@ void main() {
final Finder menuMaterial = find.ancestor( final Finder menuMaterial = find.ancestor(
of: find.widgetWithText(MenuItemButton, TestMenu.mainMenu0.label), of: find.widgetWithText(MenuItemButton, TestMenu.mainMenu0.label),
matching: find.byType(Material), matching: find.byType(Material),
).last; );
expect(menuMaterial, findsOneWidget); expect(menuMaterial, findsNWidgets(3));
// didChangeMetrics // didChangeMetrics
tester.view.physicalSize = const Size(700.0, 700.0); tester.view.physicalSize = const Size(700.0, 700.0);
......
...@@ -80,7 +80,7 @@ void main() { ...@@ -80,7 +80,7 @@ void main() {
final Finder menuMaterial = find.ancestor( final Finder menuMaterial = find.ancestor(
of: find.widgetWithText(TextButton, 'Item 0'), of: find.widgetWithText(TextButton, 'Item 0'),
matching: find.byType(Material), matching: find.byType(Material),
).last; ).at(1);
Material material = tester.widget<Material>(menuMaterial); Material material = tester.widget<Material>(menuMaterial);
expect(material.color, themeData.colorScheme.surface); expect(material.color, themeData.colorScheme.surface);
expect(material.shadowColor, themeData.colorScheme.shadow); expect(material.shadowColor, themeData.colorScheme.shadow);
...@@ -159,7 +159,7 @@ void main() { ...@@ -159,7 +159,7 @@ void main() {
final Finder menuMaterial = find.ancestor( final Finder menuMaterial = find.ancestor(
of: find.widgetWithText(TextButton, 'Item 0'), of: find.widgetWithText(TextButton, 'Item 0'),
matching: find.byType(Material), matching: find.byType(Material),
).last; ).at(1);
Material material = tester.widget<Material>(menuMaterial); Material material = tester.widget<Material>(menuMaterial);
expect(material.color, Colors.grey); expect(material.color, Colors.grey);
expect(material.shadowColor, Colors.brown); expect(material.shadowColor, Colors.brown);
...@@ -262,7 +262,7 @@ void main() { ...@@ -262,7 +262,7 @@ void main() {
final Finder menuMaterial = find.ancestor( final Finder menuMaterial = find.ancestor(
of: find.widgetWithText(TextButton, 'Item 0'), of: find.widgetWithText(TextButton, 'Item 0'),
matching: find.byType(Material), matching: find.byType(Material),
).last; ).at(1);
Material material = tester.widget<Material>(menuMaterial); Material material = tester.widget<Material>(menuMaterial);
expect(material.color, Colors.yellow); expect(material.color, Colors.yellow);
expect(material.shadowColor, Colors.green); expect(material.shadowColor, Colors.green);
...@@ -383,7 +383,7 @@ void main() { ...@@ -383,7 +383,7 @@ void main() {
final Finder menuMaterial = find.ancestor( final Finder menuMaterial = find.ancestor(
of: find.widgetWithText(TextButton, 'Item 0'), of: find.widgetWithText(TextButton, 'Item 0'),
matching: find.byType(Material), matching: find.byType(Material),
).last; ).at(1);
Material material = tester.widget<Material>(menuMaterial); Material material = tester.widget<Material>(menuMaterial);
expect(material.color, Colors.limeAccent); expect(material.color, Colors.limeAccent);
expect(material.shadowColor, Colors.deepOrangeAccent); expect(material.shadowColor, Colors.deepOrangeAccent);
......
...@@ -93,9 +93,11 @@ void main() { ...@@ -93,9 +93,11 @@ void main() {
textDirection: textDirection, textDirection: textDirection,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
GestureDetector(onTap: () { GestureDetector(
onPressed?.call(TestMenu.outsideButton); onTap: () {
}, child: Text(TestMenu.outsideButton.label)), onPressed?.call(TestMenu.outsideButton);
},
child: Text(TestMenu.outsideButton.label)),
MenuAnchor( MenuAnchor(
childFocusNode: focusNode, childFocusNode: focusNode,
controller: controller, controller: controller,
...@@ -279,10 +281,12 @@ void main() { ...@@ -279,10 +281,12 @@ void main() {
); );
// menu bar(horizontal menu) // menu bar(horizontal menu)
Finder menuMaterial = find.ancestor( Finder menuMaterial = find
of: find.byType(TextButton), .ancestor(
matching: find.byType(Material), of: find.byType(TextButton),
).first; matching: find.byType(Material),
)
.first;
Material material = tester.widget<Material>(menuMaterial); Material material = tester.widget<Material>(menuMaterial);
expect(opened, isEmpty); expect(opened, isEmpty);
...@@ -292,10 +296,12 @@ void main() { ...@@ -292,10 +296,12 @@ void main() {
expect(material.elevation, 3.0); expect(material.elevation, 3.0);
expect(material.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)))); expect(material.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))));
Finder buttonMaterial = find.descendant( Finder buttonMaterial = find
of: find.byType(TextButton), .descendant(
matching: find.byType(Material), of: find.byType(TextButton),
).first; matching: find.byType(Material),
)
.first;
material = tester.widget<Material>(buttonMaterial); material = tester.widget<Material>(buttonMaterial);
expect(material.color, Colors.transparent); expect(material.color, Colors.transparent);
expect(material.elevation, 0.0); expect(material.elevation, 0.0);
...@@ -308,10 +314,12 @@ void main() { ...@@ -308,10 +314,12 @@ void main() {
await tester.tap(find.text(TestMenu.mainMenu1.label)); await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump(); await tester.pump();
menuMaterial = find.ancestor( menuMaterial = find
of: find.widgetWithText(TextButton, TestMenu.subMenu10.label), .ancestor(
matching: find.byType(Material), of: find.widgetWithText(TextButton, TestMenu.subMenu10.label),
).first; matching: find.byType(Material),
)
.first;
material = tester.widget<Material>(menuMaterial); material = tester.widget<Material>(menuMaterial);
expect(opened.last, equals(TestMenu.mainMenu1)); expect(opened.last, equals(TestMenu.mainMenu1));
...@@ -321,10 +329,12 @@ void main() { ...@@ -321,10 +329,12 @@ void main() {
expect(material.elevation, 3.0); expect(material.elevation, 3.0);
expect(material.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)))); expect(material.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))));
buttonMaterial = find.descendant( buttonMaterial = find
of: find.widgetWithText(TextButton, TestMenu.subMenu10.label), .descendant(
matching: find.byType(Material), of: find.widgetWithText(TextButton, TestMenu.subMenu10.label),
).first; matching: find.byType(Material),
)
.first;
material = tester.widget<Material>(buttonMaterial); material = tester.widget<Material>(buttonMaterial);
expect(material.color, Colors.transparent); expect(material.color, Colors.transparent);
expect(material.elevation, 0.0); expect(material.elevation, 0.0);
...@@ -361,10 +371,12 @@ void main() { ...@@ -361,10 +371,12 @@ void main() {
); );
// menu bar(horizontal menu) // menu bar(horizontal menu)
Finder menuMaterial = find.ancestor( Finder menuMaterial = find
of: find.widgetWithText(TextButton, TestMenu.mainMenu5.label), .ancestor(
matching: find.byType(Material), of: find.widgetWithText(TextButton, TestMenu.mainMenu5.label),
).first; matching: find.byType(Material),
)
.first;
Material material = tester.widget<Material>(menuMaterial); Material material = tester.widget<Material>(menuMaterial);
expect(opened, isEmpty); expect(opened, isEmpty);
...@@ -374,10 +386,12 @@ void main() { ...@@ -374,10 +386,12 @@ void main() {
expect(material.elevation, 3.0); expect(material.elevation, 3.0);
expect(material.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)))); expect(material.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))));
Finder buttonMaterial = find.descendant( Finder buttonMaterial = find
of: find.widgetWithText(TextButton, TestMenu.mainMenu5.label), .descendant(
matching: find.byType(Material), of: find.widgetWithText(TextButton, TestMenu.mainMenu5.label),
).first; matching: find.byType(Material),
)
.first;
material = tester.widget<Material>(buttonMaterial); material = tester.widget<Material>(buttonMaterial);
expect(material.color, Colors.transparent); expect(material.color, Colors.transparent);
expect(material.elevation, 0.0); expect(material.elevation, 0.0);
...@@ -388,10 +402,12 @@ void main() { ...@@ -388,10 +402,12 @@ void main() {
await tester.tap(find.text(TestMenu.mainMenu2.label)); await tester.tap(find.text(TestMenu.mainMenu2.label));
await tester.pump(); await tester.pump();
menuMaterial = find.ancestor( menuMaterial = find
of: find.widgetWithText(TextButton, TestMenu.subMenu20.label), .ancestor(
matching: find.byType(Material), of: find.widgetWithText(TextButton, TestMenu.subMenu20.label),
).first; matching: find.byType(Material),
)
.first;
material = tester.widget<Material>(menuMaterial); material = tester.widget<Material>(menuMaterial);
expect(material.color, themeData.colorScheme.surface); expect(material.color, themeData.colorScheme.surface);
...@@ -400,10 +416,12 @@ void main() { ...@@ -400,10 +416,12 @@ void main() {
expect(material.elevation, 3.0); expect(material.elevation, 3.0);
expect(material.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)))); expect(material.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))));
buttonMaterial = find.descendant( buttonMaterial = find
of: find.widgetWithText(TextButton, TestMenu.subMenu20.label), .descendant(
matching: find.byType(Material), of: find.widgetWithText(TextButton, TestMenu.subMenu20.label),
).first; matching: find.byType(Material),
)
.first;
material = tester.widget<Material>(buttonMaterial); material = tester.widget<Material>(buttonMaterial);
expect(material.color, Colors.transparent); expect(material.color, Colors.transparent);
expect(material.elevation, 0.0); expect(material.elevation, 0.0);
...@@ -1137,7 +1155,6 @@ void main() { ...@@ -1137,7 +1155,6 @@ void main() {
expect(closed, equals(<TestMenu>[TestMenu.mainMenu1])); expect(closed, equals(<TestMenu>[TestMenu.mainMenu1]));
}); });
testWidgetsWithLeakTracking('Menus close and consume tap when open and tapped outside', (WidgetTester tester) async { testWidgetsWithLeakTracking('Menus close and consume tap when open and tapped outside', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
buildTestApp(consumesOutsideTap: true, onPressed: onPressed, onOpen: onOpen, onClose: onClose), buildTestApp(consumesOutsideTap: true, onPressed: onPressed, onOpen: onOpen, onClose: onClose),
...@@ -1639,8 +1656,12 @@ void main() { ...@@ -1639,8 +1656,12 @@ void main() {
child: const Text('Show menu'), child: const Text('Show menu'),
); );
}, },
onOpen: () { rootOpened = true; }, onOpen: () {
onClose: () { rootOpened = false; }, rootOpened = true;
},
onClose: () {
rootOpened = false;
},
menuChildren: createTestMenus( menuChildren: createTestMenus(
onPressed: onPressed, onPressed: onPressed,
onOpen: onOpen, onOpen: onOpen,
...@@ -2483,9 +2504,9 @@ void main() { ...@@ -2483,9 +2504,9 @@ void main() {
equals(const <Rect>[ equals(const <Rect>[
Rect.fromLTRB(4.0, 0.0, 112.0, 48.0), Rect.fromLTRB(4.0, 0.0, 112.0, 48.0),
Rect.fromLTRB(112.0, 0.0, 220.0, 48.0), Rect.fromLTRB(112.0, 0.0, 220.0, 48.0),
Rect.fromLTRB(220.0, 0.0, 328.0, 48.0),
Rect.fromLTRB(328.0, 0.0, 506.0, 48.0),
Rect.fromLTRB(112.0, 104.0, 326.0, 152.0), Rect.fromLTRB(112.0, 104.0, 326.0, 152.0),
Rect.fromLTRB(220.0, 0.0, 328.0, 48.0),
Rect.fromLTRB(328.0, 0.0, 506.0, 48.0)
]), ]),
); );
}); });
...@@ -2530,9 +2551,9 @@ void main() { ...@@ -2530,9 +2551,9 @@ void main() {
equals(const <Rect>[ equals(const <Rect>[
Rect.fromLTRB(688.0, 0.0, 796.0, 48.0), Rect.fromLTRB(688.0, 0.0, 796.0, 48.0),
Rect.fromLTRB(580.0, 0.0, 688.0, 48.0), Rect.fromLTRB(580.0, 0.0, 688.0, 48.0),
Rect.fromLTRB(472.0, 0.0, 580.0, 48.0),
Rect.fromLTRB(294.0, 0.0, 472.0, 48.0),
Rect.fromLTRB(474.0, 104.0, 688.0, 152.0), Rect.fromLTRB(474.0, 104.0, 688.0, 152.0),
Rect.fromLTRB(472.0, 0.0, 580.0, 48.0),
Rect.fromLTRB(294.0, 0.0, 472.0, 48.0)
]), ]),
); );
}); });
...@@ -2575,9 +2596,9 @@ void main() { ...@@ -2575,9 +2596,9 @@ void main() {
equals(const <Rect>[ equals(const <Rect>[
Rect.fromLTRB(4.0, 0.0, 112.0, 48.0), Rect.fromLTRB(4.0, 0.0, 112.0, 48.0),
Rect.fromLTRB(112.0, 0.0, 220.0, 48.0), Rect.fromLTRB(112.0, 0.0, 220.0, 48.0),
Rect.fromLTRB(220.0, 0.0, 328.0, 48.0),
Rect.fromLTRB(328.0, 0.0, 506.0, 48.0),
Rect.fromLTRB(86.0, 104.0, 300.0, 152.0), Rect.fromLTRB(86.0, 104.0, 300.0, 152.0),
Rect.fromLTRB(220.0, 0.0, 328.0, 48.0),
Rect.fromLTRB(328.0, 0.0, 506.0, 48.0)
]), ]),
); );
}); });
...@@ -2620,9 +2641,9 @@ void main() { ...@@ -2620,9 +2641,9 @@ void main() {
equals(const <Rect>[ equals(const <Rect>[
Rect.fromLTRB(188.0, 0.0, 296.0, 48.0), Rect.fromLTRB(188.0, 0.0, 296.0, 48.0),
Rect.fromLTRB(80.0, 0.0, 188.0, 48.0), Rect.fromLTRB(80.0, 0.0, 188.0, 48.0),
Rect.fromLTRB(0.0, 104.0, 214.0, 152.0),
Rect.fromLTRB(-28.0, 0.0, 80.0, 48.0), Rect.fromLTRB(-28.0, 0.0, 80.0, 48.0),
Rect.fromLTRB(-206.0, 0.0, -28.0, 48.0), Rect.fromLTRB(-206.0, 0.0, -28.0, 48.0)
Rect.fromLTRB(0.0, 104.0, 214.0, 152.0)
]), ]),
); );
}); });
...@@ -3284,8 +3305,7 @@ void main() { ...@@ -3284,8 +3305,7 @@ void main() {
children: <TestSemantics>[ children: <TestSemantics>[
TestSemantics( TestSemantics(
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState, flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState, SemanticsFlag.hasExpandedState],
SemanticsFlag.hasExpandedState],
label: 'ABC', label: 'ABC',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
), ),
...@@ -3305,11 +3325,10 @@ void main() { ...@@ -3305,11 +3325,10 @@ void main() {
MaterialApp( MaterialApp(
home: Center( home: Center(
child: SubmenuButton( child: SubmenuButton(
onHover: (bool value) {},
style: SubmenuButton.styleFrom(fixedSize: const Size(88.0, 36.0)), style: SubmenuButton.styleFrom(fixedSize: const Size(88.0, 36.0)),
menuChildren: <Widget>[ menuChildren: <Widget>[
MenuItemButton( MenuItemButton(
style: SubmenuButton.styleFrom(fixedSize: const Size(120.0, 36.0)), style: MenuItemButton.styleFrom(fixedSize: const Size(120.0, 36.0)),
child: const Text('Item 0'), child: const Text('Item 0'),
onPressed: () {}, onPressed: () {},
), ),
...@@ -3331,47 +3350,57 @@ void main() { ...@@ -3331,47 +3350,57 @@ void main() {
TestSemantics( TestSemantics(
id: 1, id: 1,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0), rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
children: <TestSemantics> [ children: <TestSemantics>[
TestSemantics( TestSemantics(
id: 2, id: 2,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0), rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
children: <TestSemantics> [ children: <TestSemantics>[
TestSemantics( TestSemantics(
id: 3, id: 3,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0), rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
flags: <SemanticsFlag> [SemanticsFlag.scopesRoute], flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics> [ children: <TestSemantics>[
TestSemantics( TestSemantics(
id: 4, id: 4,
flags: <SemanticsFlag>[SemanticsFlag.hasExpandedState, SemanticsFlag.isExpanded, SemanticsFlag.isFocused, SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, flags: <SemanticsFlag>[
SemanticsFlag.isFocusable], SemanticsFlag.isFocused,
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
SemanticsFlag.hasExpandedState,
SemanticsFlag.isExpanded,
],
actions: <SemanticsAction>[SemanticsAction.tap], actions: <SemanticsAction>[SemanticsAction.tap],
label: 'ABC', label: 'ABC',
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
) ),
] ],
) ),
] ],
), ),
TestSemantics( TestSemantics(
id: 6, id: 6,
rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 64.0), rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 64.0),
children: <TestSemantics> [ children: <TestSemantics>[
TestSemantics( TestSemantics(
id: 7, id: 7,
rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0),
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
children: <TestSemantics>[
TestSemantics(
id: 8,
label: 'Item 0',
rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0), rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0),
flags: <SemanticsFlag> [SemanticsFlag.hasImplicitScrolling], flags: <SemanticsFlag>[
children: <TestSemantics> [ SemanticsFlag.hasEnabledState,
TestSemantics( SemanticsFlag.isEnabled,
id: 8, SemanticsFlag.isFocusable,
label: 'Item 0',
rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0),
flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable],
actions: <SemanticsAction>[SemanticsAction.tap],
),
], ],
), actions: <SemanticsAction>[SemanticsAction.tap],
], ),
],
),
],
), ),
], ],
), ),
...@@ -3392,27 +3421,32 @@ void main() { ...@@ -3392,27 +3421,32 @@ void main() {
TestSemantics( TestSemantics(
id: 1, id: 1,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0), rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
children: <TestSemantics> [ children: <TestSemantics>[
TestSemantics( TestSemantics(
id: 2, id: 2,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0), rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
children: <TestSemantics> [ children: <TestSemantics>[
TestSemantics( TestSemantics(
id: 3, id: 3,
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0), rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
flags: <SemanticsFlag> [SemanticsFlag.scopesRoute], flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics> [ children: <TestSemantics>[
TestSemantics( TestSemantics(
id: 4, id: 4,
flags: <SemanticsFlag>[SemanticsFlag.hasExpandedState, SemanticsFlag.isFocused, SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, flags: <SemanticsFlag>[
SemanticsFlag.isFocusable], SemanticsFlag.hasExpandedState,
actions: <SemanticsAction>[SemanticsAction.tap], SemanticsFlag.isFocused,
label: 'ABC', SemanticsFlag.hasEnabledState,
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0), SemanticsFlag.isEnabled,
) SemanticsFlag.isFocusable,
] ],
) actions: <SemanticsAction>[SemanticsAction.tap],
] label: 'ABC',
rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
),
],
),
],
), ),
], ],
), ),
...@@ -3437,7 +3471,7 @@ void main() { ...@@ -3437,7 +3471,7 @@ void main() {
final ThemeData themeData = ThemeData( final ThemeData themeData = ThemeData(
textTheme: const TextTheme( textTheme: const TextTheme(
labelLarge: menuTextStyle, labelLarge: menuTextStyle,
) ),
); );
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
...@@ -3456,10 +3490,12 @@ void main() { ...@@ -3456,10 +3490,12 @@ void main() {
); );
// Test menu button text style uses the TextTheme.labelLarge. // Test menu button text style uses the TextTheme.labelLarge.
Finder buttonMaterial = find.descendant( Finder buttonMaterial = find
of: find.byType(TextButton), .descendant(
matching: find.byType(Material), of: find.byType(TextButton),
).first; matching: find.byType(Material),
)
.first;
Material material = tester.widget<Material>(buttonMaterial); Material material = tester.widget<Material>(buttonMaterial);
expect(material.textStyle?.fontSize, menuTextStyle.fontSize); expect(material.textStyle?.fontSize, menuTextStyle.fontSize);
expect(material.textStyle?.fontStyle, menuTextStyle.fontStyle); expect(material.textStyle?.fontStyle, menuTextStyle.fontStyle);
...@@ -3471,10 +3507,12 @@ void main() { ...@@ -3471,10 +3507,12 @@ void main() {
await tester.pump(); await tester.pump();
// Test menu item text style uses the TextTheme.labelLarge. // Test menu item text style uses the TextTheme.labelLarge.
buttonMaterial = find.descendant( buttonMaterial = find
of: find.widgetWithText(TextButton, TestMenu.subMenu10.label), .descendant(
matching: find.byType(Material), of: find.widgetWithText(TextButton, TestMenu.subMenu10.label),
).first; matching: find.byType(Material),
)
.first;
material = tester.widget<Material>(buttonMaterial); material = tester.widget<Material>(buttonMaterial);
expect(material.textStyle?.fontSize, menuTextStyle.fontSize); expect(material.textStyle?.fontSize, menuTextStyle.fontSize);
expect(material.textStyle?.fontStyle, menuTextStyle.fontStyle); expect(material.textStyle?.fontStyle, menuTextStyle.fontStyle);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment