From 95588e43b3b05fcf4c0204cc04a66ef55e0d2fea Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 2 Mar 2026 17:43:03 +0000
Subject: [PATCH 1/4] Initial plan
From b4bea4799c270c797498ee6e17cc23a5b832a4f0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 2 Mar 2026 17:58:23 +0000
Subject: [PATCH 2/4] feat: add PreviewView support to ContextMenu for
LongPressed mode
- Add PreviewView bindable property to ContextMenu
- iOS: create ContextMenuPreviewViewController to wrap MAUI view as UIViewController preview
- iOS: use previewProvider in UIContextMenuConfiguration when PreviewView is set
- Android: show PreviewView in a PopupWindow above anchor on long press, dismiss with menu
- Add sample demonstrating PreviewView with a long document title use case
- Update CHANGELOG.md (minor bump to 55.3.0)
Co-authored-by: haavamoa <2527084+haavamoa@users.noreply.github.com>
---
CHANGELOG.md | 3 ++
.../ContextMenus/ContextMenuSamples.xaml | 28 ++++++++++
.../LocalizedStrings.Designer.cs | 6 +++
.../LocalizedStrings/LocalizedStrings.en.resx | 3 ++
.../LocalizedStrings/LocalizedStrings.resx | 3 ++
.../Android/ContextMenuPlatformEffect.cs | 52 ++++++++++++++++++-
.../ContextMenus/ContextMenu.Properties.cs | 19 +++++++
.../ContextMenuPlatformEffect.LongPressed.cs | 7 ++-
.../iOS/ContextMenuPreviewViewController.cs | 38 ++++++++++++++
9 files changed, 156 insertions(+), 3 deletions(-)
create mode 100644 src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPreviewViewController.cs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 566fc8355..3b6d125ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+## [55.3.0]
+- [ContextMenu] Added `PreviewView` property to `ContextMenu` for use with `LongPressed` mode. On iOS, the view is shown as the native `UIContextMenuInteraction` preview. On Android, the view is shown in a popup above the context menu.
+
## [55.2.2]
- [iOS26][Tip] Added more padding.
diff --git a/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml b/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml
index a817683ae..72a2b72cd 100644
--- a/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml
+++ b/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml
@@ -209,6 +209,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.Designer.cs b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.Designer.cs
index c2b76608b..7eba890a4 100644
--- a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.Designer.cs
+++ b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.Designer.cs
@@ -615,6 +615,12 @@ internal static string Context_Menu_LongPress_OneItemOneGroup {
}
}
+ internal static string Context_Menu_LongPressWithPreview {
+ get {
+ return ResourceManager.GetString("Context_Menu_LongPressWithPreview", resourceCulture);
+ }
+ }
+
internal static string DefaultStateViews {
get {
return ResourceManager.GetString("DefaultStateViews", resourceCulture);
diff --git a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.en.resx b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.en.resx
index bbcbaa413..189d9a506 100644
--- a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.en.resx
+++ b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.en.resx
@@ -303,6 +303,9 @@
Single item and single group
+
+ Long press with preview
+
Default state views
diff --git a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.resx b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.resx
index 0f9b07409..503ed5c3f 100644
--- a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.resx
+++ b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.resx
@@ -308,6 +308,9 @@
Én handling og én gruppe
+
+ Trykk og hold med forhåndsvisning
+
Standard tilstand views
diff --git a/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs b/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs
index 79b1bfe02..9aaf837ef 100644
--- a/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs
+++ b/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs
@@ -1,6 +1,10 @@
+using Android.Graphics.Drawables;
using Android.Views;
+using Android.Widget;
using AndroidX.AppCompat.Widget;
+using DIPS.Mobile.UI.API.Library;
using DIPS.Mobile.UI.Components.ContextMenus.Android;
+using Microsoft.Maui.Platform;
using Object = Java.Lang.Object;
using PopupMenu = Android.Widget.PopupMenu;
using View = Android.Views.View;
@@ -45,13 +49,14 @@ protected override partial void OnAttached()
}
}
- public class ContextMenuHandler : Object, PopupMenu.IOnMenuItemClickListener
+ public class ContextMenuHandler : Object, PopupMenu.IOnMenuItemClickListener, PopupMenu.IOnDismissListener
{
private readonly ContextMenu m_contextMenu;
private readonly View m_control;
private Dictionary m_menuItems;
private PopupMenu m_popupMenu;
+ private PopupWindow? m_previewPopupWindow;
public ContextMenuHandler(ContextMenu contextMenu, View view)
{
@@ -81,14 +86,57 @@ public void OpenContextMenu(object? sender, EventArgs e)
}));
SetListeners();
-
+
+ if (m_contextMenu.PreviewView != null)
+ {
+ ShowPreview();
+ }
+
m_popupMenu.Show();
}
+ private void ShowPreview()
+ {
+ var activity = Platform.CurrentActivity;
+ var mauiContext = DUI.GetCurrentMauiContext;
+ if (activity is null || mauiContext is null || m_contextMenu.PreviewView is null)
+ return;
+
+ var previewNativeView = m_contextMenu.PreviewView.ToPlatform(mauiContext);
+
+ previewNativeView.Measure(
+ View.MeasureSpec.MakeMeasureSpec(m_control.Width, MeasureSpecMode.AtMost),
+ View.MeasureSpec.MakeMeasureSpec(0, MeasureSpecMode.Unspecified));
+
+ m_previewPopupWindow = new PopupWindow(
+ previewNativeView,
+ m_control.Width,
+ ViewGroup.LayoutParams.WrapContent,
+ false);
+
+ m_previewPopupWindow.SetBackgroundDrawable(new ColorDrawable(Android.Graphics.Color.Transparent));
+ m_previewPopupWindow.Elevation = (float)Sizes.GetSize(SizeName.size_1);
+
+ var previewHeight = previewNativeView.MeasuredHeight;
+ m_previewPopupWindow.ShowAsDropDown(m_control, 0, -(m_control.Height + previewHeight));
+ }
+
+ private void DismissPreview()
+ {
+ m_previewPopupWindow?.Dismiss();
+ m_previewPopupWindow = null;
+ }
+
+ public void OnDismiss(PopupMenu? menu)
+ {
+ DismissPreview();
+ }
+
private void SetListeners()
{
m_popupMenu.SetOnMenuItemClickListener(this);
+ m_popupMenu.SetOnDismissListener(this);
}
public bool OnMenuItemClick(IMenuItem? theTappedNativeItem)
diff --git a/src/library/DIPS.Mobile.UI/Components/ContextMenus/ContextMenu.Properties.cs b/src/library/DIPS.Mobile.UI/Components/ContextMenus/ContextMenu.Properties.cs
index 83be24bd5..2970b0e03 100644
--- a/src/library/DIPS.Mobile.UI/Components/ContextMenus/ContextMenu.Properties.cs
+++ b/src/library/DIPS.Mobile.UI/Components/ContextMenus/ContextMenu.Properties.cs
@@ -93,6 +93,25 @@ public ContextMenuHorizontalOptions ContextMenuHorizontalOptions
set => SetValue(ContextMenuHorizontalOptionsProperty, value);
}
+ ///
+ ///
+ ///
+ public static readonly BindableProperty PreviewViewProperty = BindableProperty.Create(
+ nameof(PreviewView),
+ typeof(View),
+ typeof(ContextMenu));
+
+ ///
+ /// An optional view to display as a preview when the context menu is opened in mode.
+ /// On iOS, the view is shown as the native UIContextMenuInteraction preview.
+ /// On Android, the view is shown in a popup above the context menu.
+ ///
+ public View? PreviewView
+ {
+ get => (View?)GetValue(PreviewViewProperty);
+ set => SetValue(PreviewViewProperty, value);
+ }
+
///
/// Get the mode of the context menu.
///
diff --git a/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPlatformEffect.LongPressed.cs b/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPlatformEffect.LongPressed.cs
index 862c5f2d0..eb39939ad 100644
--- a/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPlatformEffect.LongPressed.cs
+++ b/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPlatformEffect.LongPressed.cs
@@ -47,7 +47,12 @@ public override void WillEnd(UIContextMenuInteraction interaction, UIContextMenu
contextMenu);
var menu = UIMenu.Create(contextMenu.Title, dict.Select(k => k.Value).ToArray());
- return UIContextMenuConfiguration.Create(null, null, actions => menu);
+ var previewView = contextMenu.PreviewView;
+ Func? previewProvider = previewView != null
+ ? () => new ContextMenuPreviewViewController(previewView)
+ : null;
+
+ return UIContextMenuConfiguration.Create(null, previewProvider, actions => menu);
}
public Element? Element { get; set; }
diff --git a/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPreviewViewController.cs b/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPreviewViewController.cs
new file mode 100644
index 000000000..07b630b6f
--- /dev/null
+++ b/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPreviewViewController.cs
@@ -0,0 +1,38 @@
+using CoreGraphics;
+using DIPS.Mobile.UI.API.Library;
+using Microsoft.Maui.Platform;
+using UIKit;
+
+// ReSharper disable once CheckNamespace
+namespace DIPS.Mobile.UI.Components.ContextMenus.iOS;
+
+internal class ContextMenuPreviewViewController : UIViewController
+{
+ private readonly View m_previewView;
+
+ internal ContextMenuPreviewViewController(View previewView)
+ {
+ m_previewView = previewView;
+ }
+
+ public override void ViewDidLoad()
+ {
+ base.ViewDidLoad();
+
+ var mauiContext = DUI.GetCurrentMauiContext;
+ if (mauiContext is null || View is null)
+ return;
+
+ var nativeView = m_previewView.ToPlatform(mauiContext);
+ View.AddSubview(nativeView);
+
+ var maxWidth = UIScreen.MainScreen.Bounds.Width - Sizes.GetSize(SizeName.content_margin_large) * 2;
+ var measurement = m_previewView.Measure(maxWidth, double.PositiveInfinity);
+
+ var width = Math.Min(measurement.Width, maxWidth);
+ var height = measurement.Height;
+
+ PreferredContentSize = new CGSize(width, height);
+ nativeView.Frame = new CGRect(0, 0, width, height);
+ }
+}
From 42c36c6f73378b96168de63a6bcdf12e2a8a3ae2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?H=C3=A5vard=20Mo=C3=A5s?=
Date: Tue, 3 Mar 2026 09:10:57 +0100
Subject: [PATCH 3/4] fixed compilation
---
.../ContextMenus/ContextMenuSamples.xaml | 19 ++++++++++---------
.../Android/ContextMenuPlatformEffect.cs | 2 +-
.../ContextMenuPlatformEffect.LongPressed.cs | 6 ++----
3 files changed, 13 insertions(+), 14 deletions(-)
diff --git a/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml b/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml
index 72a2b72cd..e20ec67d6 100644
--- a/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml
+++ b/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml
@@ -216,15 +216,16 @@
-
-
-
-
+
+
+
+
+
k.Value).ToArray());
var previewView = contextMenu.PreviewView;
- Func? previewProvider = previewView != null
- ? () => new ContextMenuPreviewViewController(previewView)
- : null;
- return UIContextMenuConfiguration.Create(null, previewProvider, actions => menu);
+ return UIContextMenuConfiguration.Create(null, () => previewView != null ? new ContextMenuPreviewViewController(previewView)
+ : null, actions => menu);
}
public Element? Element { get; set; }
From 1f1f110c3a36dc4805ece99a62d6caa4c82f3eb3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 08:19:56 +0000
Subject: [PATCH 4/4] feat(android): add dim background and shadow to
ContextMenu preview popup
- Wrap preview in FrameLayout with MaterialShapeDrawable (rounded corners,
color_surface_default fill) and Elevation for shadow effect
- Apply FLAG_DIM_BEHIND with 0.5 dimAmount on the PopupWindow after showing
so the background dims behind the preview popup
Co-authored-by: haavamoa <2527084+haavamoa@users.noreply.github.com>
---
.../Android/ContextMenuPlatformEffect.cs | 32 ++++++++++++++++---
1 file changed, 28 insertions(+), 4 deletions(-)
diff --git a/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs b/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs
index 38904b8d5..ee6722123 100644
--- a/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs
+++ b/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs
@@ -4,6 +4,9 @@
using AndroidX.AppCompat.Widget;
using DIPS.Mobile.UI.API.Library;
using DIPS.Mobile.UI.Components.ContextMenus.Android;
+using DIPS.Mobile.UI.Effects.Layout;
+using DIPS.Mobile.UI.Extensions.Android;
+using Google.Android.Material.Shape;
using Microsoft.Maui.Platform;
using Object = Java.Lang.Object;
using PopupMenu = Android.Widget.PopupMenu;
@@ -105,21 +108,42 @@ private void ShowPreview()
var previewNativeView = m_contextMenu.PreviewView.ToPlatform(mauiContext);
- previewNativeView.Measure(
+ // Wrap in a container with rounded corners and shadow elevation
+ var container = new FrameLayout(activity);
+ var background = MaterialShapeDrawableHelper.CreateDrawable(new CornerRadius(
+ Sizes.GetSize(SizeName.size_2)));
+ background.FillColor = Resources.Colors.Colors.GetColor(ColorName.color_surface_default)
+ .ToDefaultColorStateList();
+ container.Background = background;
+ container.ClipToOutline = true;
+ container.OutlineProvider = ViewOutlineProvider.Background;
+ container.Elevation = Sizes.GetSize(SizeName.size_3).ToMauiPixel();
+ container.AddView(previewNativeView);
+
+ container.Measure(
View.MeasureSpec.MakeMeasureSpec(m_control.Width, MeasureSpecMode.AtMost),
View.MeasureSpec.MakeMeasureSpec(0, MeasureSpecMode.Unspecified));
m_previewPopupWindow = new PopupWindow(
- previewNativeView,
+ container,
m_control.Width,
ViewGroup.LayoutParams.WrapContent,
false);
m_previewPopupWindow.SetBackgroundDrawable(new ColorDrawable(global::Android.Graphics.Color.Transparent));
- m_previewPopupWindow.Elevation = (float)Sizes.GetSize(SizeName.size_1);
- var previewHeight = previewNativeView.MeasuredHeight;
+ var previewHeight = container.MeasuredHeight;
m_previewPopupWindow.ShowAsDropDown(m_control, 0, -(m_control.Height + previewHeight));
+
+ // Apply dim behind the preview popup
+ var rootView = m_previewPopupWindow.ContentView?.RootView;
+ if (rootView?.LayoutParameters is WindowManagerLayoutParams layoutParams
+ && activity.GetSystemService(global::Android.Content.Context.WindowService) is IWindowManager windowManager)
+ {
+ layoutParams.Flags |= WindowManagerFlags.DimBehind;
+ layoutParams.DimAmount = 0.5f;
+ windowManager.UpdateViewLayout(rootView, layoutParams);
+ }
}
private void DismissPreview()