Skip to content

Commit

Permalink
🆕 feat(Menu): add ScrollStrategy parameter with default value 'reposi…
Browse files Browse the repository at this point in the history
…tion' (#2194)

* 🆕 feat(Menu): add ScrollStrategy parameter with default value 'reposition'

* update
  • Loading branch information
capdiem authored Oct 22, 2024
1 parent a0f2f11 commit 4c6e5af
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 137 deletions.
210 changes: 122 additions & 88 deletions src/Masa.Blazor.JS/src/components/overlay/scrollStrategies.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,79 @@
import { getScrollParents, hasScrollbar } from "utils/getScrollParent";
import { convertToUnit } from "utils/helper";

export interface StrategyProps {
strategy: "none" | "block"; // | "close" | "reposition"
type StrategyProps = {
strategy: "none" | "block" | "close" | "reposition";
contained: boolean | undefined;
}

class ScrollStrategies {
root: HTMLElement;
contentEl: HTMLElement;
options: StrategyProps;

// maybe only used for block strategy
scrollElements: HTMLElement[];
scrollableParent: Element | null;

constructor(
root: HTMLElement,
contentEl: HTMLElement,
options: StrategyProps
) {
if (!root) {
return;
}

this.root = root;
this.contentEl = contentEl;
this.options = options;
}

bind() {
if (this.options.strategy === "block") {
this._prepareBlock();
this._blockScroll();
}
}

unbind() {
if (this.options.strategy === "block") {
this._unblockScroll();
}
}

_prepareBlock() {
const offsetParent = this.root.offsetParent;
this.scrollElements = [
...new Set([
...getScrollParents(
this.contentEl,
this.options.contained ? offsetParent : undefined
),
]),
];

this.scrollableParent = ((el) => hasScrollbar(el) && el)(
offsetParent || document.documentElement
};

type ScrollStrategyData = {
root: HTMLElement | undefined;
contentEl: HTMLElement | undefined;
targetEl: HTMLElement | undefined;
invoker?: DotNet.DotNetObject;
};

type ScrollStrategyResult = {
bind?: () => void;
unbind: () => void;
};

export function useScrollStrategies(
props: StrategyProps,
root: HTMLElement | undefined,
contentEl: HTMLElement | undefined,
targetEl: HTMLElement | undefined,
dotNet?: DotNet.DotNetObject
): ScrollStrategyResult {
if (props.strategy === "block") {
return useBlockScrollStrategy(
{
root,
contentEl,
targetEl,
},
props
);
} else {
return useInvokerScrollStrategy(
{
root,
contentEl,
targetEl,
invoker: dotNet,
},
props
);
}
}

_blockScroll() {
if (this.scrollableParent) {
this.root.classList.add("m-overlay--scroll-blocked");
function useBlockScrollStrategy(
data: ScrollStrategyData,
options: StrategyProps
): ScrollStrategyResult {
const offsetParent = data.root.offsetParent;
const scrollElements = [
...new Set([
...getScrollParents(
data.contentEl,
options.contained ? offsetParent : undefined
),
]),
];

const scrollableParent = ((el) => hasScrollbar(el) && el)(
offsetParent || document.documentElement
);

const bind = () => {
if (scrollableParent) {
data.root.classList.add("m-overlay--scroll-blocked");
}

const scrollbarWidth =
window.innerWidth - document.documentElement.offsetWidth;

this.scrollElements
scrollElements
.filter((el) => !el.classList.contains("m-overlay-scroll-blocked"))
.forEach((el, i) => {
el.style.setProperty(
Expand All @@ -84,37 +91,64 @@ class ScrollStrategies {

el.classList.add("m-overlay-scroll-blocked");
});
}

_unblockScroll() {
this.scrollElements
.filter((el) => el.classList.contains("m-overlay-scroll-blocked"))
.forEach((el, i) => {
const x = parseFloat(el.style.getPropertyValue("--m-body-scroll-x"));
const y = parseFloat(el.style.getPropertyValue("--m-body-scroll-y"));

const scrollBehavior = el.style.scrollBehavior;

el.style.scrollBehavior = "auto";
el.style.removeProperty("--m-body-scroll-x");
el.style.removeProperty("--m-body-scroll-y");
el.style.removeProperty("--m-scrollbar-offset");
el.classList.remove("m-overlay-scroll-blocked");

el.scrollLeft = -x;
el.scrollTop = -y;

el.style.scrollBehavior = scrollBehavior;
});

if (this.scrollableParent) {
this.root.classList.remove("m-overlay--scroll-blocked");
}
}
};

bind();

return {
bind,
unbind: () => {
scrollElements
.filter((el) => el.classList.contains("m-overlay-scroll-blocked"))
.forEach((el, i) => {
const x = parseFloat(el.style.getPropertyValue("--m-body-scroll-x"));
const y = parseFloat(el.style.getPropertyValue("--m-body-scroll-y"));

const scrollBehavior = el.style.scrollBehavior;

el.style.scrollBehavior = "auto";
el.style.removeProperty("--m-body-scroll-x");
el.style.removeProperty("--m-body-scroll-y");
el.style.removeProperty("--m-scrollbar-offset");
el.classList.remove("m-overlay-scroll-blocked");

el.scrollLeft = -x;
el.scrollTop = -y;

el.style.scrollBehavior = scrollBehavior;
});

if (scrollableParent) {
data.root.classList.remove("m-overlay--scroll-blocked");
}
},
};
}

function init(root: HTMLElement, contentEl: HTMLElement, props: StrategyProps) {
return new ScrollStrategies(root, contentEl, props);
}
function useInvokerScrollStrategy(
data: ScrollStrategyData,
options: StrategyProps
) {
const el = data.targetEl ?? data.contentEl;

export { init };
const onScroll = () => {
data.invoker?.invokeMethodAsync(
"ScrollStrategy_OnScroll",
options.strategy
);
};

const scrollElements = [document, ...getScrollParents(el)];
scrollElements.forEach((el) =>
el.addEventListener("scroll", onScroll, { passive: true })
);

return {
unbind: () => {
data.invoker?.dispose();
scrollElements.forEach((el) =>
el.removeEventListener("scroll", onScroll)
);
},
};
}
42 changes: 39 additions & 3 deletions src/Masa.Blazor/Components/Menu/MMenu.razor.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
using Masa.Blazor.Mixins;
using Masa.Blazor.Mixins.Menuable;
using Masa.Blazor.Mixins.Menuable;
using Masa.Blazor.Mixins.ScrollStrategy;

namespace Masa.Blazor
{
public partial class MMenu : MMenuable, IDependent
{
[Inject] private OutsideClickJSModule OutsideClickJSModule { get; set; } = null!;

[Inject] private ScrollStrategyJSModule ScrollStrategyJSModule { get; set; } = default!;

[CascadingParameter] public IDependent? CascadingDependent { get; set; }

[CascadingParameter(Name = "AppIsDark")]
Expand Down Expand Up @@ -35,7 +37,7 @@ public bool CloseOnContentClick
[Parameter] [MasaApiParameter("auto")] public StringNumber MaxHeight { get; set; } = "auto";

[Parameter] public EventCallback<WheelEventArgs> OnScroll { get; set; }

[Parameter] public EventCallback<MouseEventArgs> OnOutsideClick { get; set; }

[Parameter] public string? Origin { get; set; }
Expand All @@ -50,6 +52,10 @@ public bool CloseOnContentClick

[Parameter] public bool Light { get; set; }

[Parameter]
[MasaApiParameter(ReleasedOn = "v1.8.0")]
public ScrollStrategy ScrollStrategy { get; set; } = ScrollStrategy.Reposition;

private static Block _block = new("m-menu");
private ModifierBuilder _modifierBuilder = _block.CreateModifierBuilder();
private ModifierBuilder _contentModifierBuilder = _block.Element("content").CreateModifierBuilder();
Expand All @@ -58,6 +64,8 @@ public bool CloseOnContentClick
private readonly List<IDependent> _dependents = new();

private bool _isPopupEventsRegistered;
private ScrollStrategyResult? _scrollStrategyResult;
private DotNetObjectReference<MMenu>? _dotNetObjectReference;

public bool IsDark
{
Expand Down Expand Up @@ -231,6 +239,14 @@ protected override async Task WhenIsActiveUpdating(bool value)
_isPopupEventsRegistered = true;

RegisterPopupEvents(ContentElement.GetSelector()!, CloseOnContentClick);

if (ScrollStrategy != ScrollStrategy.None || (Absolute && ScrollStrategy != ScrollStrategy.Reposition))
{
_dotNetObjectReference ??= DotNetObjectReference.Create(this);

_scrollStrategyResult = await ScrollStrategyJSModule.CreateScrollStrategy(Ref, ContentElement,
new ScrollStrategyOptions(ScrollStrategy.Reposition), _dotNetObjectReference);
}
}

if (!OpenOnHover && CloseOnClick && OutsideClickJSModule is { Initialized: false })
Expand All @@ -239,6 +255,24 @@ protected override async Task WhenIsActiveUpdating(bool value)
}
}

[JSInvokable("ScrollStrategy_OnScroll")]
public async Task ScrollStrategy_OnScroll(string strategy)
{
switch (strategy)
{
case "close":
RunDirectly(false);
break;
case "reposition":
await UpdateDimensionsAsync();
StateHasChanged();
break;
default:
Logger.LogWarning("Unknown scroll strategy: {0}", strategy);
break;
}
}

private Func<ValueTask<bool>>? CloseConditional { get; set; }

private Func<Task>? Handler { get; set; }
Expand Down Expand Up @@ -273,6 +307,8 @@ private double CalcLeftAuto()
protected override async ValueTask DisposeAsyncCore()
{
await OutsideClickJSModule.UnbindAndDisposeAsync();
_scrollStrategyResult?.Unbind?.Invoke();
_scrollStrategyResult?.Dispose?.Invoke();
await base.DisposeAsyncCore();
}
}
Expand Down
23 changes: 13 additions & 10 deletions src/Masa.Blazor/Components/Overlay/MOverlay.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,27 +133,30 @@ protected override IEnumerable<string> BuildComponentStyle()
}
}

private ScrollStrategyResult? _scrollStrategyResult;

private async Task HideScroll()
{
if (!ScrollStrategyJSModule.Initialized)
if (_scrollStrategyResult is null)
{
await ScrollStrategyJSModule.InitializeAsync(Ref, ContentRef,
new(ScrollStrategy.Block, Contained));
_scrollStrategyResult =
await ScrollStrategyJSModule.CreateScrollStrategy(Ref, ContentRef,
new(ScrollStrategy.Block, Contained));
}
else
{
_scrollStrategyResult.Bind?.Invoke();
}

await ScrollStrategyJSModule.BindAsync();
}

private async Task ShowScroll()
{
await ScrollStrategyJSModule.UnbindAsync();
_scrollStrategyResult?.Unbind?.Invoke();
}

protected override async ValueTask DisposeAsyncCore()
{
if (ScrollStrategyJSModule.Initialized)
{
await ShowScroll();
}
_scrollStrategyResult?.Unbind?.Invoke();
_scrollStrategyResult?.Dispose?.Invoke();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ private static IMasaBlazorBuilder AddMasaBlazorInternal(this IServiceCollection
services.TryAddTransient<InputJSModule>();
services.TryAddTransient<TransitionJSModule>();
services.TryAddTransient<OutsideClickJSModule>();
services.TryAddTransient<ScrollStrategyJSModule>();
services.TryAddScoped<ScrollStrategyJSModule>();
services.TryAddScoped<PdfMobileViewerJSModule>();

services.TryAddScoped<IPageStackNavControllerFactory, PageStackNavControllerFactory>();
Expand Down
9 changes: 9 additions & 0 deletions src/Masa.Blazor/Mixins/ScrollStrategy/ScrollStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Masa.Blazor;

public enum ScrollStrategy
{
None,
Block,
Close,
Reposition
}
Loading

0 comments on commit 4c6e5af

Please sign in to comment.