diff --git a/doc/controls-styles.md b/doc/controls-styles.md
index 638b08b22..c9cc70297 100644
--- a/doc/controls-styles.md
+++ b/doc/controls-styles.md
@@ -5,6 +5,7 @@ The `Uno.Toolkit.UI` library adds the following controls:
- [`Chip` and `ChipGroup`](controls\ChipAndChipGroup.md): \[Material control\] Chips are compact elements that represent an input, attribute, or action.
- `Divider`: \[Material control\] A divider is a thin line that groups content in lists and layouts.
- [`DrawerControl`](controls\DrawerControl.md): A container to display additional content, in a hidden pane that can be revealed using a swipe gesture, like a drawer.
+- [`DrawerFlyoutPresenter`](controls\DrawerFlyoutPresenter.md): A specialized `ContentPresenter` to be used in the template of a `FlyoutPresenter` to enable gesture support.
- [`TabBar` and `TabBarItem`](controls\TabBarAndTabBarItem.md): A list of selectable items that can be used to facilitate lateral navigation within an application.
- [`NavigationBar`](controls\NavigationBar.md): A custom control that helps implement navigation logic for your application.
@@ -15,9 +16,9 @@ The `Uno.Toolkit.UI` library adds the following helper classes:
- [`StatusBar`](helpers\StatusBar-extensions.md): Provides two attached properties on `Page` to controls the visual of the status bar on mobile platforms.
## Control Styles
-Control|Style Key|IsDefaultStyle*|
+Control|Style Key|IsDefaultStyle*
-|-|-
-AppBarButton|MainCommandStyle|True|
+AppBarButton|MainCommandStyle|True
AppBarButton|ModalMainCommandStyle|
AppBarButton|PrimaryMainCommandStyle|
AppBarButton|PrimaryModalMainCommandStyle|
@@ -48,8 +49,12 @@ utu:ChipGroup|ElevatedFilterChipGroupStyle|
utu:ChipGroup|FilterChipGroupStyle|
utu:ChipGroup|ElevatedAssistChipGroupStyle|
utu:ChipGroup|AssistChipGroupStyle|
-utu:Divider|DividerStyle|True|
-utu:NavigationBar|NavigationBarStyle|True|
+utu:Divider|DividerStyle|True
+FlyoutPresenter|LeftDrawerFlyoutPresenterStyle|
+FlyoutPresenter|TopDrawerFlyoutPresenterStyle|
+FlyoutPresenter|RightDrawerFlyoutPresenterStyle|
+FlyoutPresenter|BottomDrawerFlyoutPresenterStyle|
+utu:NavigationBar|NavigationBarStyle|True
utu:NavigationBar|ModalNavigationBarStyle|
utu:NavigationBar|PrimaryNavigationBarStyle|
utu:NavigationBar|PrimaryModalNavigationBarStyle|
diff --git a/doc/controls/DrawerFlyoutPresenter.md b/doc/controls/DrawerFlyoutPresenter.md
new file mode 100644
index 000000000..19e25d064
--- /dev/null
+++ b/doc/controls/DrawerFlyoutPresenter.md
@@ -0,0 +1,56 @@
+# DrawerFlyoutPresenter
+## Summary
+`DrawerFlyoutPresenter` is a special `ContentPresenter` to be used in the template of a `FlyoutPresenter` to enable gesture support.
+
+## Properties
+### Remarks
+All of the properties below can be used both as a dependency property or as an attached property, much like the `ScrollViewer` properties:
+```xml
+xmlns:utu="using:Uno.Toolkit.UI.Controls"
+
+
+
+
+```
+
+### Properties
+Property|Type|Description
+-|-|-
+OpenDirection|DrawerOpenDirection|Gets or sets the direction in which the drawer opens toward.
note: The position of drawer when opened is the opposite of this value.
+LightDismissOverlayBackground|Brush|Gets or sets the brush used to paint the light dismiss overlay. The default value is `#80808080` (from the default style).
+IsGestureEnabled|bool|Get or sets a value that indicates whether the user can interact with the control using gesture. The default value is `true`.
+
+## Usage
+
+To use this, simply use a `Flyout` with `Placement="Full"` and one of the followings as the `FlyoutPresenterStyle`:
+> note: The direction here indicates the initial position of the drawer. The open direction is the opposite.
+- `LeftDrawerFlyoutPresenterStyle`
+- `TopDrawerFlyoutPresenterStyle`
+- `RightDrawerFlyoutPresenterStyle`
+- `BottomDrawerFlyoutPresenterStyle`
+
+Example:
+```xml
+
+```
+> note: Here `VisibleBoundsPadding.PaddingMask` is used to prevent the content from being placed outside of the user-interactable area on mobile devices.
diff --git a/doc/toc.yml b/doc/toc.yml
index 01515f672..c8de2b07e 100644
--- a/doc/toc.yml
+++ b/doc/toc.yml
@@ -18,6 +18,8 @@
href: controls/ChipAndChipGroup.md
- name: DrawerControl
href: controls/DrawerControl.md
+ - name: DrawerFlyoutPresenter
+ href: controls/DrawerFlyoutPresenter.md
- name: NavigationBar
href: controls/NavigationBar.md
- name: TabBar and TabBarItem
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/DrawerFlyoutSamplePage.xaml b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/DrawerFlyoutSamplePage.xaml
new file mode 100644
index 000000000..986e69fd7
--- /dev/null
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/DrawerFlyoutSamplePage.xaml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/DrawerFlyoutSamplePage.xaml.cs b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/DrawerFlyoutSamplePage.xaml.cs
new file mode 100644
index 000000000..52ea16259
--- /dev/null
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Content/Controls/DrawerFlyoutSamplePage.xaml.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices.WindowsRuntime;
+using Uno.Toolkit.Samples.Entities;
+using Uno.Toolkit.UI.Controls;
+using Windows.Foundation;
+using Windows.Foundation.Collections;
+
+#if IS_WINUI
+using Microsoft.UI;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Controls.Primitives;
+using Microsoft.UI.Xaml.Data;
+using Microsoft.UI.Xaml.Input;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Navigation;
+#else
+using Windows.UI;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Controls.Primitives;
+using Windows.UI.Xaml.Data;
+using Windows.UI.Xaml.Input;
+using Windows.UI.Xaml.Media;
+using Windows.UI.Xaml.Navigation;
+#endif
+
+namespace Uno.Toolkit.Samples.Content.Controls
+{
+ [SamplePage(SampleCategory.Controls, "DrawerFlyout")]
+ public sealed partial class DrawerFlyoutSamplePage : Page
+ {
+ public DrawerFlyoutSamplePage()
+ {
+ this.InitializeComponent();
+ }
+ }
+}
diff --git a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Uno.Toolkit.Samples.Shared.projitems b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Uno.Toolkit.Samples.Shared.projitems
index 541fffe84..3c94fd8d7 100644
--- a/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Uno.Toolkit.Samples.Shared.projitems
+++ b/samples/Uno.Toolkit.Samples/Uno.Toolkit.Samples.Shared/Uno.Toolkit.Samples.Shared.projitems
@@ -25,6 +25,9 @@
ChipSamplePage.xaml
+
+ DrawerFlyoutSamplePage.xaml
+
StatusBarSamplePage.xaml
@@ -159,6 +162,10 @@
Designer
MSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
Designer
MSBuild:Compile
@@ -352,4 +359,4 @@
-
+
\ No newline at end of file
diff --git a/src/Uno.Toolkit.UI/Controls/DrawerControl/DrawerControl.cs b/src/Uno.Toolkit.UI/Controls/DrawerControl/DrawerControl.cs
index 49ba81646..ba0c7ee89 100644
--- a/src/Uno.Toolkit.UI/Controls/DrawerControl/DrawerControl.cs
+++ b/src/Uno.Toolkit.UI/Controls/DrawerControl/DrawerControl.cs
@@ -1,5 +1,5 @@
#if HAS_UNO
-#define STORYBOARD_RETARGET_ISSUE // https://github.com/unoplatform/uno/issues/6960
+//#define STORYBOARD_RETARGET_ISSUE // https://github.com/unoplatform/uno/issues/6960
#define MANIPULATION_ABSOLUTE_COORD_ISSUE // https://github.com/unoplatform/uno/issues/6964
#endif
diff --git a/src/Uno.Toolkit.UI/Controls/DrawerFlyout/DrawerFlyoutPresenter.Properties.cs b/src/Uno.Toolkit.UI/Controls/DrawerFlyout/DrawerFlyoutPresenter.Properties.cs
new file mode 100644
index 000000000..c1fa38d41
--- /dev/null
+++ b/src/Uno.Toolkit.UI/Controls/DrawerFlyout/DrawerFlyoutPresenter.Properties.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+#if IS_WINUI
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Media;
+#else
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Media;
+#endif
+
+namespace Uno.Toolkit.UI.Controls
+{
+ public partial class DrawerFlyoutPresenter
+ {
+ internal static class DefaultValues
+ {
+ public const DrawerOpenDirection OpenDirection = DrawerOpenDirection.Up;
+ public const bool IsGestureEnabled = true;
+ }
+
+ #region DependencyProperty: [Private] IsOpen
+
+ private static DependencyProperty IsOpenProperty { get; } = DependencyProperty.Register(
+ nameof(IsOpen),
+ typeof(bool),
+ typeof(DrawerFlyoutPresenter),
+ new PropertyMetadata(default(bool), OnIsOpenChanged));
+
+ ///
+ /// Gets or sets a value that specifies whether the drawer is open.
+ ///
+ private bool IsOpen
+ {
+ get => (bool)GetValue(IsOpenProperty);
+ set => SetValue(IsOpenProperty, value);
+ }
+
+ #endregion
+
+ // note: These properties below can function both as a direct DP (from the owner) and as an attached DP (from any dependency object),
+ // just like the ScrollViewer properties.
+
+ #region AttachedProperty: OpenDirection = Top
+
+ public static DependencyProperty OpenDirectionProperty { get; } = DependencyProperty.RegisterAttached(
+ nameof(OpenDirection),
+ typeof(DrawerOpenDirection),
+ typeof(DrawerFlyoutPresenter),
+ new PropertyMetadata(DefaultValues.OpenDirection, OnOpenDirectionChanged));
+
+ ///
+ /// Gets or sets the direction in which the drawer opens toward.
+ ///
+ ///
+ /// The position of drawer when opened is the opposite of this value.
+ ///
+ public DrawerOpenDirection OpenDirection
+ {
+ get => (DrawerOpenDirection)GetValue(OpenDirectionProperty);
+ set => SetValue(OpenDirectionProperty, value);
+ }
+
+ public static DrawerOpenDirection GetOpenDirection(DependencyObject obj) => (DrawerOpenDirection)obj.GetValue(OpenDirectionProperty);
+ public static void SetOpenDirection(DependencyObject obj, DrawerOpenDirection value) => obj.SetValue(OpenDirectionProperty, value);
+
+ #endregion
+ #region AttachedProperty: LightDismissOverlayBackground
+
+ public static DependencyProperty LightDismissOverlayBackgroundProperty { get; } = DependencyProperty.RegisterAttached(
+ nameof(LightDismissOverlayBackground),
+ typeof(Brush),
+ typeof(DrawerFlyoutPresenter),
+ new PropertyMetadata(default(Brush)));
+
+ ///
+ /// Gets or sets the brush used to paint the light dismiss overlay.
+ ///
+ public Brush LightDismissOverlayBackground
+ {
+ get => (Brush)GetValue(LightDismissOverlayBackgroundProperty);
+ set => SetValue(LightDismissOverlayBackgroundProperty, value);
+ }
+
+ public static Brush GetLightDismissOverlayBackground(DependencyObject obj) => (Brush)obj.GetValue(LightDismissOverlayBackgroundProperty);
+ public static void SetLightDismissOverlayBackground(DependencyObject obj, Brush value) => obj.SetValue(LightDismissOverlayBackgroundProperty, value);
+
+ #endregion
+ #region AttachedProperty: IsGestureEnabled = true
+
+ public static DependencyProperty IsGestureEnabledProperty { get; } = DependencyProperty.RegisterAttached(
+ nameof(IsGestureEnabled),
+ typeof(bool),
+ typeof(DrawerFlyoutPresenter),
+ new PropertyMetadata(DefaultValues.IsGestureEnabled));
+
+ ///
+ /// Get or sets a value that indicates whether the user can interact with the control using gesture.
+ ///
+ public bool IsGestureEnabled
+ {
+ get => (bool)GetValue(IsGestureEnabledProperty);
+ set => SetValue(IsGestureEnabledProperty, value);
+ }
+
+ public static bool GetIsGestureEnabled(DependencyObject obj) => (bool)obj.GetValue(IsGestureEnabledProperty);
+ public static void SetIsGestureEnabled(DependencyObject obj, bool value) => obj.SetValue(IsGestureEnabledProperty, value);
+
+ #endregion
+
+ private static void OnOpenDirectionChanged(DependencyObject control, DependencyPropertyChangedEventArgs e) => (control as DrawerFlyoutPresenter)?.OnOpenDirectionChanged(e);
+ private static void OnIsOpenChanged(DependencyObject control, DependencyPropertyChangedEventArgs e) => (control as DrawerFlyoutPresenter)?.OnIsOpenChanged(e);
+ }
+}
diff --git a/src/Uno.Toolkit.UI/Controls/DrawerFlyout/DrawerFlyoutPresenter.cs b/src/Uno.Toolkit.UI/Controls/DrawerFlyout/DrawerFlyoutPresenter.cs
new file mode 100644
index 000000000..a0e445531
--- /dev/null
+++ b/src/Uno.Toolkit.UI/Controls/DrawerFlyout/DrawerFlyoutPresenter.cs
@@ -0,0 +1,449 @@
+#if HAS_UNO
+//#define STORYBOARD_RETARGET_ISSUE // PATCHED https://github.com/unoplatform/uno/issues/6960
+#define MANIPULATION_ABSOLUTE_COORD_ISSUE // https://github.com/unoplatform/uno/issues/6964
+#endif
+
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+#if IS_WINUI
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Controls.Primitives;
+using Microsoft.UI.Xaml.Input;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Media.Animation;
+
+using XamlWindow = Microsoft.UI.Xaml.Window;
+#else
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Controls.Primitives;
+using Windows.UI.Xaml.Input;
+using Windows.UI.Xaml.Media;
+using Windows.UI.Xaml.Media.Animation;
+
+using XamlWindow = Windows.UI.Xaml.Window;
+#endif
+
+namespace Uno.Toolkit.UI.Controls
+{
+ public partial class DrawerFlyoutPresenter
+ {
+ public static class TemplateParts
+ {
+ public const string LightDismissOverlay = nameof(LightDismissOverlay);
+ public const string DrawerContentPresenter = nameof(DrawerContentPresenter);
+ }
+
+ private const double DragToggleThresholdRatio = 1.0 / 3;
+ private const double AnimateSnappingThresholdRatio = 0.95;
+ private static readonly TimeSpan AnimationDuration = TimeSpan.FromMilliseconds(250);
+ }
+
+ [TemplatePart(Name = TemplateParts.LightDismissOverlay, Type = typeof(Border))]
+ [TemplatePart(Name = TemplateParts.DrawerContentPresenter, Type = typeof(ContentPresenter))]
+ public partial class DrawerFlyoutPresenter : ContentControl
+ {
+ // template parts
+ private Border _lightDismissOverlay;
+ private ContentPresenter _drawerContentPresenter;
+
+ // references
+ private TranslateTransform _drawerContentPresenterTransform;
+ private Storyboard _storyboard = new Storyboard();
+ private DoubleAnimation _translateAnimation;
+ private Popup _popup;
+
+ // states
+ private bool _isReady = false;
+ private bool _isGestureCaptured = false;
+ private bool _initOnceOnLoaded = true;
+ private bool _initOnceOnLayoutUpdated = true;
+ private double _startingTranslateOffset = 0;
+ private bool _suppressIsOpenHandler = false;
+
+ public DrawerFlyoutPresenter()
+ {
+ DefaultStyleKey = typeof(DrawerFlyoutPresenter);
+ }
+
+ protected override void OnApplyTemplate()
+ {
+ if (_isReady) throw new Exception("unexpected: Template is being re-applied.");
+
+ base.OnApplyTemplate();
+
+ T FindTemplatePart(string name) where T : class =>
+ (GetTemplateChild(name) ?? throw new Exception($"Expected template part not found: {name}"))
+ as T ?? throw new Exception($"Expected template part '{name}' to be of type: {typeof(T)}");
+
+ _lightDismissOverlay = FindTemplatePart(TemplateParts.LightDismissOverlay);
+ _drawerContentPresenter = FindTemplatePart(TemplateParts.DrawerContentPresenter);
+
+ _drawerContentPresenter.RenderTransform = _drawerContentPresenterTransform = new TranslateTransform();
+ _translateAnimation = new DoubleAnimation { Duration = new Duration(AnimationDuration) };
+ Storyboard.SetTarget(_translateAnimation, _drawerContentPresenterTransform);
+ UpdateTranslateAnimationTargetProperty();
+ _storyboard.Children.Add(_translateAnimation);
+
+ UpdateSwipeContentPresenterLayout();
+ UpdateManipulationMode();
+ ManipulationStarted += OnManipulationStarted;
+ ManipulationDelta += OnManipulationDelta;
+ ManipulationCompleted += OnManipulationCompleted;
+
+ _lightDismissOverlay.Tapped += OnLightDismissOverlayTapped;
+
+#if HAS_UNO // uno: the visual tree parent is not set, until Loaded.
+ Loaded += (s, e) =>
+#endif
+ {
+ if (_initOnceOnLoaded)
+ {
+ _initOnceOnLoaded = false;
+
+ _popup = FindHostPopup() ?? throw new Exception("Unable to find host popup.");
+ _popup.Opened += OnPopupOpened;
+ _storyboard.Completed += (s, e) =>
+ {
+ if (!IsOpen)
+ {
+ _popup.IsOpen = false;
+ }
+ };
+ }
+ };
+
+ // note: by the time we got here, the popup would be already opened, thus we will miss the first opened event.
+ // in order to catch it, we use LayoutUpdated; Loaded event cannot be used here, as the _drawerContentPresenter
+ // still don't have its Actual(Width|Height) set which are needed for changing the position.
+ LayoutUpdated += OnLayoutUpdated;
+
+ _isReady = true;
+ }
+
+ private void OnLayoutUpdated(object sender, object e)
+ {
+ if (_initOnceOnLayoutUpdated)
+ {
+ _initOnceOnLayoutUpdated = false;
+
+ // reset to close position, and animate to open position
+ UpdateOpenness(false);
+ UpdateIsOpen(true, animate: true);
+ }
+ }
+
+ private void OnPopupOpened(object sender, object e)
+ {
+ // reset to close position, and animate to open position
+ UpdateOpenness(false);
+ UpdateIsOpen(true, animate: true);
+ }
+
+ private void OnIsOpenChanged(DependencyPropertyChangedEventArgs e)
+ {
+ if (!_isReady) return;
+ if (_suppressIsOpenHandler) return;
+
+ StopRunningAnimation();
+ UpdateIsOpen((bool)e.NewValue, animate: IsLoaded);
+ }
+
+ private void OnOpenDirectionChanged(DependencyPropertyChangedEventArgs e)
+ {
+ if (!_isReady) return;
+
+ StopRunningAnimation();
+ UpdateSwipeContentPresenterLayout();
+ UpdateManipulationMode();
+ UpdateTranslateAnimationTargetProperty();
+ ResetOtherAxisTranslateOffset();
+ UpdateIsOpen(IsOpen, animate: false);
+ }
+
+ private void OnManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e)
+ {
+ if (!ShouldHandleManipulationFrom(e.OriginalSource)) return;
+ if (!IsGestureEnabled) return;
+
+ var position =
+#if MANIPULATION_ABSOLUTE_COORD_ISSUE
+ this.TransformToVisual(null).Inverse.TransformPoint(e.Position);
+#else
+ e.Position;
+#endif
+
+ _isGestureCaptured = true;
+ StopRunningAnimation();
+ _startingTranslateOffset = TranslateOffset;
+
+ e.Handled = true;
+ }
+
+ private void OnManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
+ {
+ if (!_isGestureCaptured || !IsGestureEnabled) return;
+ e.Handled = true;
+
+ var length = GetActualDrawerDepth();
+ var cumulative = IsOpenDirectionHorizontal() ? e.Cumulative.Translation.X : e.Cumulative.Translation.Y;
+ var currentOffset = UseNegativeTranslation()
+ ? Clamp(-length, _startingTranslateOffset + cumulative, 0)
+ : Clamp(0, _startingTranslateOffset + cumulative, length);
+ var ratio = Math.Abs(currentOffset) / length;
+
+ UpdateOpenness((1 - ratio));
+ }
+
+ private void OnManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
+ {
+ if (!_isGestureCaptured || !IsGestureEnabled) return;
+ _isGestureCaptured = false;
+ e.Handled = true;
+
+ StopRunningAnimation();
+ var length = GetActualDrawerDepth();
+ var cumulative = IsOpenDirectionHorizontal() ? e.Cumulative.Translation.X : e.Cumulative.Translation.Y;
+
+ var isInCorrectDirection = Math.Sign(cumulative) == (IsOpen ^ UseNegativeTranslation() ? 1 : -1);
+ var isPastThresholdRatio = Math.Abs(cumulative / length) >= DragToggleThresholdRatio;
+
+ UpdateIsOpen(IsOpen ^ (isInCorrectDirection && isPastThresholdRatio));
+ }
+
+ private void OnLightDismissOverlayTapped(object sender, TappedRoutedEventArgs e)
+ {
+ StopRunningAnimation();
+ UpdateIsOpen(false);
+ }
+
+ private void UpdateIsOpen(bool willBeOpen, bool animate = true)
+ {
+ var length = GetActualDrawerDepth();
+ var currentOffset = TranslateOffset;
+ var targetOffset = GetSnappingOffsetFor(willBeOpen);
+ var relativeDistanceRatio = Math.Abs(Math.Abs(currentOffset) - Math.Abs(targetOffset)) / length;
+
+ var shouldSkipAnimation = !animate || ((1 - relativeDistanceRatio) >= AnimateSnappingThresholdRatio);
+ if (shouldSkipAnimation)
+ {
+ UpdateOpenness(willBeOpen);
+ UpdateIsOpenWithSuppress(willBeOpen);
+ if (!willBeOpen && _popup != null)
+ {
+ _popup.IsOpen = false;
+ }
+ }
+ else
+ {
+ UpdateIsOpenWithSuppress(willBeOpen);
+ PlayAnimation(currentOffset / GetVectoredLength(), willBeOpen);
+ // note: the popup will be closed on Storyboard.Completed
+ }
+
+ void UpdateIsOpenWithSuppress(bool value)
+ {
+ try
+ {
+ _suppressIsOpenHandler = true;
+ IsOpen = value;
+ }
+ finally
+ {
+ _suppressIsOpenHandler = false;
+ }
+ }
+ }
+
+ private void UpdateOpenness(bool isOpen) => UpdateOpenness(isOpen ? 1 : 0);
+
+ private void UpdateOpenness(double ratio)
+ {
+ TranslateOffset = (1 - ratio) * GetVectoredLength();
+ }
+
+ private void PlayAnimation(double fromRatio, bool willBeOpen)
+ {
+ if (_storyboard == null) return;
+
+ var toRatio = willBeOpen ? 0 : 1;
+ if (_translateAnimation != null)
+ {
+ var vectoredLength = GetVectoredLength();
+
+ _translateAnimation.From = fromRatio * vectoredLength;
+ _translateAnimation.To = toRatio * vectoredLength;
+ }
+
+ _storyboard.Begin();
+ }
+
+ private void StopRunningAnimation()
+ {
+ if (_storyboard != null && _storyboard.GetCurrentState() != ClockState.Stopped)
+ {
+ // we want to Pause() the animation midway to avoid the jarring feeling
+ // but since paused state will still yield ClockState.Active
+ // we have to actually use Stop() in order to differentiate
+
+ // pause & snapshot the animated values in the middle of animation
+ _storyboard.Pause();
+ var offset = TranslateOffset;
+
+ // restore the values after stopping it
+ _storyboard.Stop();
+ TranslateOffset = offset;
+ }
+ }
+
+ private void UpdateManipulationMode()
+ {
+ ManipulationMode = IsOpenDirectionHorizontal() ? ManipulationModes.TranslateX : ManipulationModes.TranslateY;
+ }
+
+ private void UpdateSwipeContentPresenterLayout()
+ {
+ if (_drawerContentPresenter == null) return;
+
+ switch (OpenDirection)
+ {
+ case DrawerOpenDirection.Left:
+ _drawerContentPresenter.HorizontalAlignment = HorizontalAlignment.Right;
+ _drawerContentPresenter.VerticalAlignment = VerticalAlignment.Stretch;
+ _drawerContentPresenter.HorizontalContentAlignment = HorizontalAlignment.Right;
+ _drawerContentPresenter.VerticalContentAlignment = VerticalAlignment.Stretch;
+ break;
+
+ case DrawerOpenDirection.Down:
+ _drawerContentPresenter.HorizontalAlignment = HorizontalAlignment.Stretch;
+ _drawerContentPresenter.VerticalAlignment = VerticalAlignment.Top;
+ _drawerContentPresenter.HorizontalContentAlignment = HorizontalAlignment.Stretch;
+ _drawerContentPresenter.VerticalContentAlignment = VerticalAlignment.Top;
+ break;
+
+ case DrawerOpenDirection.Up:
+ _drawerContentPresenter.HorizontalAlignment = HorizontalAlignment.Stretch;
+ _drawerContentPresenter.VerticalAlignment = VerticalAlignment.Bottom;
+ _drawerContentPresenter.HorizontalContentAlignment = HorizontalAlignment.Stretch;
+ _drawerContentPresenter.VerticalContentAlignment = VerticalAlignment.Bottom;
+ break;
+
+ case DrawerOpenDirection.Right:
+ default:
+ _drawerContentPresenter.HorizontalAlignment = HorizontalAlignment.Left;
+ _drawerContentPresenter.VerticalAlignment = VerticalAlignment.Stretch;
+ _drawerContentPresenter.HorizontalContentAlignment = HorizontalAlignment.Left;
+ _drawerContentPresenter.VerticalContentAlignment = VerticalAlignment.Stretch;
+ break;
+ }
+ }
+
+ private void UpdateTranslateAnimationTargetProperty()
+ {
+ if (_translateAnimation == null) return;
+
+ var property = IsOpenDirectionHorizontal() ? nameof(_drawerContentPresenterTransform.X) : nameof(_drawerContentPresenterTransform.Y);
+ Storyboard.SetTargetProperty(_translateAnimation, property);
+ }
+
+ private void ResetOtherAxisTranslateOffset()
+ {
+ if (IsOpenDirectionHorizontal())
+ {
+ _drawerContentPresenterTransform.Y = 0;
+ }
+ else
+ {
+ _drawerContentPresenterTransform.X = 0;
+ }
+ }
+ }
+
+ public partial class DrawerFlyoutPresenter // helpers
+ {
+ private double TranslateOffset
+ {
+ get => IsOpenDirectionHorizontal() ? _drawerContentPresenterTransform.X : _drawerContentPresenterTransform.Y;
+ set
+ {
+ if (IsOpenDirectionHorizontal()) _drawerContentPresenterTransform.X = value;
+ else _drawerContentPresenterTransform.Y = value;
+ }
+ }
+
+ private bool ShouldHandleManipulationFrom(object source)
+ {
+ // only the content area should respond to gesture
+ if (source == _lightDismissOverlay) return false;
+
+ // note: on uwp, we cant distinguish the origin of the event, as it would always be from this DrawerFlyoutPresenter.
+ return source == this
+ || (source is DependencyObject sourceAsDO && VisualTreeHelperEx.GetAncestors(sourceAsDO).Any(x => x == this));
+ }
+
+ private double GetSnappingOffsetFor(bool isOpen)
+ {
+ return isOpen ? 0 : GetVectoredLength();
+ }
+
+ private bool IsOpenDirectionHorizontal()
+ {
+ return OpenDirection switch
+ {
+ DrawerOpenDirection.Down => false,
+ DrawerOpenDirection.Up => false,
+
+ _ => true,
+ };
+ }
+
+ private bool UseNegativeTranslation()
+ {
+ return OpenDirection switch
+ {
+ DrawerOpenDirection.Left => false,
+ DrawerOpenDirection.Up => false,
+
+ _ => true,
+ };
+ }
+
+ private double GetActualDrawerDepth()
+ {
+ if (_drawerContentPresenter == null) throw new InvalidOperationException($"{nameof(_drawerContentPresenter)} is null");
+
+ return IsOpenDirectionHorizontal()
+ ? _drawerContentPresenter.ActualWidth
+ : _drawerContentPresenter.ActualHeight;
+ }
+
+ private double GetVectoredLength()
+ {
+ return UseNegativeTranslation() ? -GetActualDrawerDepth() : GetActualDrawerDepth();
+ }
+
+ private Popup FindHostPopup()
+ {
+ if (VisualTreeHelper.GetParent(this) is FlyoutPresenter parent)
+ {
+ return VisualTreeHelper
+ .GetOpenPopups(XamlWindow.Current)
+ .FirstOrDefault(x => x.Child == parent);
+ }
+
+ return default;
+ }
+
+ private static double Clamp(double min, double value, double max)
+ {
+ return Math.Max(Math.Min(value, max), min);
+ }
+ }
+}
diff --git a/src/Uno.Toolkit.UI/Controls/DrawerFlyout/DrawerFlyoutPresenter.xaml b/src/Uno.Toolkit.UI/Controls/DrawerFlyout/DrawerFlyoutPresenter.xaml
new file mode 100644
index 000000000..2057e3157
--- /dev/null
+++ b/src/Uno.Toolkit.UI/Controls/DrawerFlyout/DrawerFlyoutPresenter.xaml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Uno.Toolkit.UI/Helpers/VisualTreeHelperEx.cs b/src/Uno.Toolkit.UI/Helpers/VisualTreeHelperEx.cs
new file mode 100644
index 000000000..0bccfc583
--- /dev/null
+++ b/src/Uno.Toolkit.UI/Helpers/VisualTreeHelperEx.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+#if IS_WINUI
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Media;
+#else
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Media;
+#endif
+
+namespace Uno.Toolkit.UI
+{
+ internal static class VisualTreeHelperEx
+ {
+ public static string TreeGraph(this DependencyObject reference) => TreeGraph(reference, DebugVTNode);
+
+ public static string TreeGraph(this DependencyObject reference, Func describe)
+ {
+ var buffer = new StringBuilder();
+ Walk(reference);
+ return buffer.ToString();
+
+ void Walk(DependencyObject o, int depth = 0)
+ {
+ Print(o, depth);
+ foreach (var child in GetChildren(o))
+ {
+ Walk(child, depth + 1);
+ }
+ }
+ void Print(DependencyObject o, int depth)
+ {
+ buffer
+ .Append(new string(' ', depth * 4))
+ .Append(describe(o))
+ .AppendLine();
+ }
+ }
+
+ private static string DebugVTNode(DependencyObject x)
+ {
+ var fe = x as FrameworkElement;
+
+ return new StringBuilder()
+ .Append(x.GetType().Name)
+ .Append(!string.IsNullOrEmpty(fe?.Name) ? $"#{fe?.Name}" : string.Empty)
+ .ToString();
+ }
+
+
+ public static T GetFirstAncestor(this DependencyObject reference) => GetAncestors(reference)
+ .OfType()
+ .FirstOrDefault();
+
+ public static T GetFirstAncestor(this DependencyObject reference, Func predicate) => GetAncestors(reference)
+ .OfType()
+ .FirstOrDefault(predicate);
+
+ public static T GetFirstDescendant(DependencyObject reference) => GetDescendants(reference)
+ .OfType()
+ .FirstOrDefault();
+
+ public static T GetFirstDescendant(DependencyObject reference, Func predicate) => GetDescendants(reference)
+ .OfType()
+ .FirstOrDefault(predicate);
+
+ public static IEnumerable GetAncestors(this DependencyObject o)
+ {
+ if (o is null) yield break;
+ while (VisualTreeHelper.GetParent(o) is { } parent)
+ {
+ yield return o = parent;
+ }
+ }
+
+ public static IEnumerable GetDescendants(DependencyObject reference)
+ {
+ foreach (var child in GetChildren(reference))
+ {
+ yield return child;
+ foreach (var grandchild in GetDescendants(child))
+ {
+ yield return grandchild;
+ }
+ }
+ }
+
+ public static IEnumerable GetChildren(DependencyObject reference)
+ {
+ return Enumerable
+ .Range(0, VisualTreeHelper.GetChildrenCount(reference))
+ .Select(x => VisualTreeHelper.GetChild(reference, x));
+ }
+ }
+}
diff --git a/src/Uno.Toolkit.UI/ToolkitResources.cs b/src/Uno.Toolkit.UI/ToolkitResources.cs
index 07349f888..1b5d09535 100644
--- a/src/Uno.Toolkit.UI/ToolkitResources.cs
+++ b/src/Uno.Toolkit.UI/ToolkitResources.cs
@@ -30,6 +30,7 @@ public ToolkitResources()
{
"DrawerControl/DrawerControl.xaml",
"DrawerControl/DrawerControl.Enhanced.xaml",
+ "DrawerFlyout/DrawerFlyoutPresenter.xaml",
"TabBar/TabBar.xaml",
"AutoLayout/AutoLayout.xaml",
#if __IOS__ || __ANDROID__