feat: request approval (#16784)

* feat: add connections without sending passwords

Add Without password actions and pass the selection through desktop,
mobile and Web sessions. Omit the password when constructing LoginRequest,
preserve saved credentials, and reuse the existing approval and OS login
dialogs. Keep protocol and controlled-side authentication changes separate.

* fix: preserve login method across session windows

* fix: skip password persistence for consent connections

* refactor: unify request approval menus and naming

* chore: move request approval translation key to the end

* i18n: translate request approval menu label

* fix: align request approval menu layout and icons

* fix: avoid duplicate focus stops in request approval menus

* fix: distinguish request approval submenus on mobile

* fix: open overflowing submenus to the left

* Revert "fix: distinguish request approval submenus on mobile"

This reverts commit 7f0e50d4e4.
This commit is contained in:
21pages authored and GitHub committed 2026-11-02 23:48:27 +08:00
1 parent 037b8787fe
commit a85ad89ffc
89 files changed
+1110 -152

No files matched your search

@@ -656,11 +656,13 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
this.selectedItemIndex,
this.textDirection,
this.padding,
this.avoidBounds,
);
this.avoidBounds, {
this.submenuAnchor,
});
// Rectangle of underlying button, relative to the overlay's dimensions.
final RelativeRect position;
final Rect? submenuAnchor;
// The sizes of each item are computed when the menu is laid out, and before
// the route is laid out.
@@ -735,6 +737,14 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
DisplayFeatureSubScreen.subScreensInBounds(
Offset.zero & size, avoidBounds);
final Rect subScreen = _closestScreen(subScreens, originCenter);
final anchor = submenuAnchor;
if (anchor != null) {
final left = anchor.left - childSize.width;
if (wantedPosition.dx + childSize.width >
subScreen.right - _kMenuScreenPadding - padding.right) {
return _fitInsideScreen(subScreen, childSize, Offset(left, y));
}
}
return _fitInsideScreen(subScreen, childSize, wantedPosition);
}
@@ -781,6 +791,7 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
assert(itemSizes.length == oldDelegate.itemSizes.length);
return position != oldDelegate.position ||
submenuAnchor != oldDelegate.submenuAnchor ||
selectedItemIndex != oldDelegate.selectedItemIndex ||
textDirection != oldDelegate.textDirection ||
!listEquals(itemSizes, oldDelegate.itemSizes) ||
@@ -792,6 +803,7 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
class _PopupMenuRoute<T> extends PopupRoute<T> {
_PopupMenuRoute({
required this.position,
this.submenuAnchor,
required this.items,
this.menuWrapper,
this.initialValue,
@@ -805,6 +817,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
}) : itemSizes = List<Size?>.filled(items.length, null);
final RelativeRect position;
final Rect? submenuAnchor;
final List<PopupMenuEntry<T>> items;
final MenuWrapper? menuWrapper;
final List<Size?> itemSizes;
@@ -874,6 +887,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
Directionality.of(context),
mediaQuery.padding,
_avoidBounds(mediaQuery),
submenuAnchor: submenuAnchor,
),
child: capturedThemes.wrap(menu),
);
@@ -1042,6 +1056,7 @@ class PopupMenu<T> extends StatelessWidget {
Future<T?> showMenu<T>({
required BuildContext context,
required RelativeRect position,
Rect? submenuAnchor,
required List<PopupMenuEntry<T>> items,
MenuWrapper? menuWrapper,
T? initialValue,
@@ -1070,6 +1085,7 @@ Future<T?> showMenu<T>({
Navigator.of(context, rootNavigator: useRootNavigator);
return navigator.push(_PopupMenuRoute<T>(
position: position,
submenuAnchor: submenuAnchor,
items: items,
menuWrapper: menuWrapper,
initialValue: initialValue,
@@ -1352,6 +1368,9 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
menuWrapper: widget.menuWrapper,
initialValue: widget.initialValue,
position: position,
submenuAnchor: widget.position == PopupMenuPosition.overSide
? button.localToGlobal(Offset.zero, ancestor: overlay) & button.size
: null,
shape: widget.shape ?? popupMenuTheme.shape,
color: widget.color ?? popupMenuTheme.color,
constraints: widget.constraints,
+53 -17
View File
@@ -15,6 +15,8 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
this.enabled,
this.textStyle,
this.onTap,
this.dismissParentOnCanceled = false,
this.useButtonLayout = false,
this.position = mod_menu.PopupMenuPosition.overSide,
this.offset = Offset.zero,
required this.itemBuilder,
@@ -27,6 +29,8 @@ class PopupMenuChildrenItem<T> extends mod_menu.PopupMenuEntry<T> {
final EdgeInsets? padding;
final RxBool? enabled;
final void Function()? onTap;
final bool dismissParentOnCanceled;
final bool useButtonLayout;
final List<mod_menu.PopupMenuEntry<T>> Function(BuildContext) itemBuilder;
final Widget child;
@@ -69,20 +73,42 @@ class MyPopupMenuItemState<T, W extends PopupMenuChildrenItem<T>>
position: widget.position,
offset: widget.offset,
onSelected: handleTap,
onCanceled: widget.dismissParentOnCanceled
? () {
if (ModalRoute.of(context)?.isCurrent == true) {
Navigator.pop(context);
}
}
: null,
itemBuilder: widget.itemBuilder,
padding: EdgeInsets.zero,
child: AnimatedDefaultTextStyle(
style: style,
duration: kThemeChangeDuration,
child: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(
minHeight: widget.height, maxHeight: widget.height),
padding:
widget.padding ?? const EdgeInsets.symmetric(horizontal: 16),
child: widget.child,
),
),
child: Builder(builder: (context) {
final child = AnimatedDefaultTextStyle(
style: style,
duration: kThemeChangeDuration,
child: Container(
alignment: AlignmentDirectional.centerStart,
constraints: BoxConstraints(
minHeight: widget.height, maxHeight: widget.height),
padding: widget.padding ??
const EdgeInsets.symmetric(horizontal: 16),
child: widget.child,
),
);
if (!widget.useButtonLayout) return child;
final onPressed = enabled.value
? () => context
.findAncestorStateOfType<mod_menu.PopupMenuButtonState<T>>()
?.showButtonMenu()
: null;
// The popup trigger owns keyboard focus; retain the menu button sizing.
return ExcludeFocus(
child: TextButton(
onPressed: onPressed,
child: TextButton(onPressed: onPressed, child: child),
),
);
}),
));
}
}
@@ -645,11 +671,15 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
final String text;
final List<MenuEntryBase<T>> entries;
final EdgeInsets? padding;
final bool dismissParentOnCanceled;
final double arrowScale;
MenuEntrySubMenu({
required this.text,
required this.entries,
this.padding,
this.dismissParentOnCanceled = false,
this.arrowScale = MenuConfig.iconScale,
RxBool? enabled,
}) : super(enabled: enabled);
@@ -662,13 +692,14 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
enabled: super.enabled,
height: conf.height,
padding: padding,
dismissParentOnCanceled: dismissParentOnCanceled,
useButtonLayout: true,
position: mod_menu.PopupMenuPosition.overSide,
itemBuilder: (BuildContext context) => entries
.map((entry) => entry.build(context, conf))
.expand((i) => i)
.toList(),
child: Row(children: [
const SizedBox(width: MenuConfig.midPadding),
Obx(() => Text(
text,
style: super.enabled!.value
@@ -678,10 +709,15 @@ class MenuEntrySubMenu<T> extends MenuEntryBase<T> {
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Obx(() => Icon(
Icons.keyboard_arrow_right,
color: super.enabled!.value ? conf.commonColor : Colors.grey,
)),
child: Transform.scale(
scale: arrowScale,
child: Obx(() => Icon(
Icons.keyboard_arrow_right,
color: super.enabled!.value
? Theme.of(context).unselectedWidgetColor
: disabledStyle().color,
)),
),
))
]),
)