fix: distinguish request approval submenus on mobile

This commit is contained in:
21pages committed 2026-11-02 08:44:37 +08:00
1 parent bb68f6f7d6
commit 7f0e50d4e4
4 files changed
+84 -5

No files matched your search

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../../common.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import '../../desktop/widgets/popup_menu.dart';
@@ -33,10 +34,13 @@ class RequestApprovalMenu extends MenuEntryBase<String> {
text: title,
dismissParentOnCanceled: true,
arrowScale: arrowScale,
barrierColor: isMobile ? Colors.black26 : null,
padding: (padding ?? EdgeInsets.zero)
.copyWith(right: (padding?.right ?? 0) + arrowInset),
entries: entries
.map((entry) => MenuEntryButton<String>(
entries: [
if (isMobile)
_RequestApprovalMenuTitle(title, padding),
...entries.map((entry) => MenuEntryButton<String>(
childBuilder: entry.childBuilder,
proc: entry.proc,
padding: entry.padding,
@@ -45,8 +49,39 @@ class RequestApprovalMenu extends MenuEntryBase<String> {
dismissCallback: () {
if (Navigator.canPop(context)) Navigator.pop(context);
},
))
.toList(),
)),
],
).build(context, conf);
}
}
class _RequestApprovalMenuTitle extends MenuEntryBase<String> {
final String title;
final EdgeInsets? padding;
_RequestApprovalMenuTitle(this.title, this.padding);
@override
List<mod_menu.PopupMenuEntry<String>> build(
BuildContext context, MenuConfig conf) => [
mod_menu.PopupMenuItem<String>(
enabled: false,
height: conf.height,
padding: EdgeInsets.zero,
child: TextButton(
onPressed: null,
child: Container(
alignment: AlignmentDirectional.centerStart,
height: conf.height,
padding: padding,
child: Semantics(
header: true,
child: Text(title,
style: enabledStyle(context)
.copyWith(fontWeight: FontWeight.w600)),
),
),
),
),
];
}
@@ -797,6 +797,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
this.initialValue,
this.elevation,
required this.barrierLabel,
this.barrierColor,
this.semanticLabel,
this.shape,
this.color,
@@ -832,7 +833,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
bool get barrierDismissible => true;
@override
Color? get barrierColor => null;
final Color? barrierColor;
@override
final String barrierLabel;
@@ -1049,6 +1050,7 @@ Future<T?> showMenu<T>({
String? semanticLabel,
ShapeBorder? shape,
Color? color,
Color? barrierColor,
bool useRootNavigator = false,
BoxConstraints? constraints,
}) {
@@ -1076,6 +1078,7 @@ Future<T?> showMenu<T>({
elevation: elevation,
semanticLabel: semanticLabel,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: barrierColor,
shape: shape,
color: color,
capturedThemes:
@@ -1152,6 +1155,7 @@ class PopupMenuButton<T> extends StatefulWidget {
this.enabled = true,
this.shape,
this.color,
this.barrierColor,
this.enableFeedback,
this.constraints,
this.position = PopupMenuPosition.over,
@@ -1248,6 +1252,7 @@ class PopupMenuButton<T> extends StatefulWidget {
/// If [PopupMenuThemeData.color] is also null, then
/// Theme.of(context).cardColor is used.
final Color? color;
final Color? barrierColor;
/// Whether detected gestures should provide acoustic and/or haptic feedback.
///
@@ -1354,6 +1359,7 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
position: position,
shape: widget.shape ?? popupMenuTheme.shape,
color: widget.color ?? popupMenuTheme.color,
barrierColor: widget.barrierColor,
constraints: widget.constraints,
).then<void>((T? newValue) {
if (!mounted) return null;
@@ -17,6 +17,7 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
this.onTap,
this.dismissParentOnCanceled = false,
this.useButtonLayout = false,
this.barrierColor,
this.position = mod_menu.PopupMenuPosition.overSide,
this.offset = Offset.zero,
required this.itemBuilder,
@@ -31,6 +32,7 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
final void Function()? onTap;
final bool dismissParentOnCanceled;
final bool useButtonLayout;
final Color? barrierColor;
final List<mod_menu.PopupMenuEntry<T>> Function(BuildContext) itemBuilder;
final Widget child;
@@ -82,6 +84,7 @@ class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
: null,
itemBuilder: widget.itemBuilder,
padding: EdgeInsets.zero,
barrierColor: widget.barrierColor,
child: Builder(builder: (context) {
final child = AnimatedDefaultTextStyle(
style: style,
@@ -673,6 +676,7 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
final EdgeInsets? padding;
final bool dismissParentOnCanceled;
final double arrowScale;
final Color? barrierColor;
MenuEntrySubMenu({
required this.text,
@@ -680,6 +684,7 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
this.padding,
this.dismissParentOnCanceled = false,
this.arrowScale = MenuConfig.iconScale,
this.barrierColor,
RxBool? enabled,
}) : super(enabled: enabled);
@@ -694,6 +699,7 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
padding: padding,
dismissParentOnCanceled: dismissParentOnCanceled,
useButtonLayout: true,
barrierColor: barrierColor,
position: mod_menu.PopupMenuPosition.overSide,
itemBuilder: (BuildContext context) => entries
.map((entry) => entry.build(context, conf))
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_hbb/common.dart' as common;
import 'package:flutter_hbb/common/widgets/request_approval_menu.dart';
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
as mod_menu;
@@ -89,6 +90,34 @@ Future<void> openMenu(WidgetTester tester, List<String> actions,
}
void main() {
testWidgets('only mobile approval submenus have a heading and dim background',
(tester) async {
final wasMobile = common.isMobile;
addTearDown(() => common.isMobile = wasMobile);
for (final mobile in [true, false]) {
common.isMobile = mobile;
final actions = <String>[];
await openMenu(tester, actions);
await tester.tap(find.text('Request approval'));
await tester.pumpAndSettle();
expect(find.text('Request approval'), findsNWidgets(mobile ? 2 : 1));
expect(
find.byWidgetPredicate((widget) =>
widget is ModalBarrier && widget.color == Colors.black26),
mobile ? findsOneWidget : findsNothing);
if (mobile) {
await tester.tap(find.text('Request approval').last);
await tester.pumpAndSettle();
expect(actions, isEmpty);
expect(find.text('Request approval'), findsNWidgets(2));
}
await tester.tapAt(const Offset(2, 2));
await tester.pumpAndSettle();
expect(find.text('Request approval'), findsNothing);
expect(find.text('Rename'), findsNothing);
}
});
testWidgets('approval trigger has one Tab stop and opens with Enter',
(tester) async {
await openMenu(tester, []);
@@ -221,6 +250,9 @@ void main() {
testWidgets('the whole approval row opens the submenu on a narrow screen',
(tester) async {
final wasMobile = common.isMobile;
common.isMobile = true;
addTearDown(() => common.isMobile = wasMobile);
tester.view.physicalSize = const Size(360, 640);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);