flutter / flutter/flutter

Menu Anchor alignment offset does not work when right alignment is set

Open
#159,413 7 comments 3 reactions 0 assignees View on GitHub
found in release: 3.24 found in release: 3.27 has reproducible steps p: material_ui P2 package team-design triaged-design
Dominant language
Dart
Stars
179k
Forks
31.1k
PR merge metrics
PR metrics pending

Description

### Steps to reproduce

1. flutter create bug
2. paste the code sample
3. flutter run -d chrome
4. press the menu button
5. the open menu is aligned to the right of the screen, but it hugs the edge. The additional offset is ignored.
6. Change the dx offset from `-60` to `60` and use `Alignment.bottomLeft` for the menu style alignment
7. Observe that the offset is applied, along with the alignment

### Expected results

The alignment offset should be applied on top of the general menu style alignment, regardless of the axis (where applicable).
In other words, if the menu offset moves the menu away from the menu alignment direction, towards the center of the FlutterView, this should be allowed.

I.e. the following should be allowed (if the FlutterView has enough space)
- left alignment, but offset towards the right
- right alignment, but offset towards the left
- top alignment, but offset towards the bottom
- bottom alignment, but offset towards the top

### Actual results

The alignment X-offset is ignored for `Alignment.bottomRight` and `Alignment.topRight`.
The alignment X-offset is working as expected for `Alignment.bottomLeft`.
The alignment Y-offset is working as expected.

I did not test any other combinations, so the various Alignments should be tested with all possible combinations of +/- x and y offsets. Maybe there are other cases that don't work?

### Code sample

Code sample

```dart
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
backgroundColor: Colors.white,
body: Align(
alignment: Alignment.topCenter,
child: Container(
height: 72.0,
color: Colors.red,
child: const Align(
alignment: Alignment.centerRight,
child: Padding(
padding: EdgeInsets.only(left: 4, right: 16),
child: CustomMenuAnchor(),
),
),
),
),
),
);
}
}

class CustomMenuAnchor extends StatefulWidget {
const CustomMenuAnchor({super.key});

@override
State createState() => _CustomMenuAnchorState();
}

class _CustomMenuAnchorState extends State {
final FocusNode _buttonFocusNode = FocusNode();
final MenuController _menuController = MenuController();
final ValueNotifier _isMenuOpenNotifier = ValueNotifier(false);

@override
void dispose() {
_buttonFocusNode.dispose();
_isMenuOpenNotifier.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return _CustomMenuAnchorImpl(
buttonFocusNode: _buttonFocusNode,
menuController: _menuController,
isMenuOpenNotifier: _isMenuOpenNotifier,
);
}
}

class _CustomMenuAnchorImpl extends StatelessWidget {
const _CustomMenuAnchorImpl({
required this.buttonFocusNode,
required this.isMenuOpenNotifier,
required this.menuController,
});

final FocusNode buttonFocusNode;

final ValueNotifier isMenuOpenNotifier;

final MenuController menuController;

void _onOpenMenu() {
isMenuOpenNotifier.value = true;
}

void _onCloseMenu() {
isMenuOpenNotifier.value = false;
}

void _toggleMenuOpen() {
if (menuController.isOpen) {
menuController.close();
} else {
menuController.open();
}
}

MenuStyle _getMenuStyleForScreenDimensions(Size screenSize) {
final Size maximumSize;
final EdgeInsets padding;

if (screenSize.width < 360) {
maximumSize = Size(screenSize.width - 16, 400);
} else {
maximumSize = const Size(600, 400);
}

if (screenSize.width < 490) {
padding = const EdgeInsets.symmetric(vertical: 16);
} else {
padding = const EdgeInsets.symmetric(horizontal: 32, vertical: 16);
}

return MenuStyle(
// TODO: not providing the alignment here makes the offset work? It also works for left aligned values.
alignment: Alignment.bottomRight,
maximumSize: WidgetStatePropertyAll(maximumSize),
padding: WidgetStatePropertyAll(padding),
shape: const WidgetStatePropertyAll(
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(6))),
),
);
}

Widget _buildMenuButton(BuildContext context, MenuController controller) {
const Color iconColor = Colors.white;
const Widget downIcon = Icon(Icons.arrow_drop_down, key: ValueKey(true), color: iconColor);
const Widget upIcon = Icon(Icons.arrow_drop_up, key: ValueKey(false), color: iconColor);

final Widget menuArrowIcon = ValueListenableBuilder(
valueListenable: isMenuOpenNotifier,
builder: (BuildContext context, bool isOpen, Widget? child) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (Widget child, Animation anim) => RotationTransition(
turns: Tween(begin: child.key == const ValueKey(false) ? 0 : 1, end: 0.5).animate(anim),
child: FadeTransition(opacity: anim, child: child),
),
child: isOpen ? downIcon : upIcon,
);
},
);

return Builder(
builder: (BuildContext context) {
final ButtonStyle? style = TextButtonTheme.of(context).style;

return TextButtonTheme(
data: TextButtonThemeData(
style: style?.copyWith(splashFactory: NoSplash.splashFactory),
),
child: Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
label: const Text(
'Some button',
style: TextStyle(color: iconColor),
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.start,
maxLines: 1,
),
icon: menuArrowIcon,
iconAlignment: IconAlignment.end,
focusNode: buttonFocusNode,
onPressed: _toggleMenuOpen,
),
),
);
},
);
}

List _buildMenuItems(BuildContext context) {
const TextStyle menuItemStyle = TextStyle(fontSize: 18, color: Colors.black);

return [
for (int i = 0; i < 10; i++) ...[
MenuItemButton(
onPressed: () {},
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 260),
child: Text(
'Item $i',
style: menuItemStyle,
overflow: TextOverflow.ellipsis,
maxLines: 3,
softWrap: true,
),
),
),
const Divider(thickness: 1, height: 1),
],
MenuItemButton(
onPressed: () {},
child: const Text(
'Some other button',
style: menuItemStyle,
overflow: TextOverflow.ellipsis,
),
),
];
}

@override
Widget build(BuildContext context) {
final MenuStyle menuStyle = _getMenuStyleForScreenDimensions(MediaQuery.sizeOf(context));

return MenuAnchor(
// TODO: the negative x alignment offset does not work, because top/bottom right alignment is specified?
// Move the menu 60 pixels to the left, away from the screen right edge.
// Strangely enough, the y axis offset does work.
alignmentOffset: const Offset(-60, 0),
controller: menuController,
childFocusNode: buttonFocusNode,
style: menuStyle,
onOpen: _onOpenMenu,
onClose: _onCloseMenu,
menuChildren: _buildMenuItems(context),
builder: (BuildContext context, MenuController controller, _) => _buildMenuButton(context, controller),
);
}
}
```

### Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

### Logs

Logs

```console
[Paste your logs here]
```

### Flutter Doctor output

Doctor output

```console
[✓] Flutter (Channel stable, 3.24.5, on macOS 14.6.1 23G93 darwin-x64, locale en-BE)
• Flutter version 3.24.5 on channel stable at /Users/navaronbracke/Documents/flutter
• Upstream repository git@github.com:navaronbracke/flutter.git
• FLUTTER_GIT_URL = git@github.com:navaronbracke/flutter.git
• Framework revision dec2ee5c1f (12 days ago), 2024-11-13 11:13:06 -0800
• Engine revision a18df97ca5
• Dart version 3.5.4
• DevTools version 2.37.3

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
• Android SDK at /Users/navaronbracke/Library/Android/sdk
• Platform android-34, build-tools 34.0.0
• ANDROID_HOME = /Users/navaronbracke/Library/Android/sdk
• Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 17.0.11+0-17.0.11b1207.24-11852314)
• All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 16.1)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 16B40
• CocoaPods version 1.16.2

[✓] Chrome - develop for the web
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2024.1)
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 17.0.11+0-17.0.11b1207.24-11852314)

[✓] VS Code (version 1.95.3)
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.100.0

[✓] Connected device (2 available)
• macOS (desktop) • macos • darwin-x64 • macOS 14.6.1 23G93 darwin-x64
• Chrome (web) • chrome • web-javascript • Google Chrome 131.0.6778.86

[✓] Network resources
• All expected network resources are available.

• No issues found!
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.