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