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..e20ec67d6 100644
--- a/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml
+++ b/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml
@@ -209,6 +209,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..ee6722123 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,13 @@
+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 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;
using View = Android.Views.View;
@@ -45,13 +52,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 +89,78 @@ 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);
+
+ // 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(
+ container,
+ m_control.Width,
+ ViewGroup.LayoutParams.WrapContent,
+ false);
+
+ m_previewPopupWindow.SetBackgroundDrawable(new ColorDrawable(global::Android.Graphics.Color.Transparent));
+
+ 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()
+ {
+ 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..e76d3b163 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,10 @@ 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;
+
+ return UIContextMenuConfiguration.Create(null, () => previewView != null ? new ContextMenuPreviewViewController(previewView)
+ : null, 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);
+ }
+}