fix(core): fix menu bugs (#8074)

This commit is contained in:
EYHN
2024-09-04 07:19:08 +00:00
parent 01e6370dd2
commit 51f3566bec
12 changed files with 188 additions and 79 deletions
@@ -1,20 +1,24 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useState } from 'react';
import { useRefEffect } from '../../../hooks';
export const useMenuContentController = ({
onOpenChange,
side,
defaultOpen,
sideOffset,
open: controlledOpen,
}: {
defaultOpen?: boolean;
side?: 'top' | 'bottom' | 'left' | 'right';
onOpenChange?: (open: boolean) => void;
open?: boolean;
sideOffset?: number;
} = {}) => {
const [open, setOpen] = useState(defaultOpen ?? false);
const actualOpen = controlledOpen ?? open;
const contentSide = side ?? 'bottom';
const [contentOffset, setContentOffset] = useState<number>(0);
const contentRef = useRef<HTMLDivElement>(null);
const handleOpenChange = useCallback(
(open: boolean) => {
@@ -23,65 +27,69 @@ export const useMenuContentController = ({
},
[onOpenChange]
);
useEffect(() => {
if (!open || !contentRef.current) return;
const contentRef = useRefEffect<HTMLDivElement>(
contentElement => {
if (!actualOpen) return;
const wrapperElement = contentRef.current.parentNode as HTMLDivElement;
const wrapperElement = contentElement.parentNode as HTMLDivElement;
const updateContentOffset = () => {
if (!contentRef.current) return;
const contentRect = wrapperElement.getBoundingClientRect();
if (contentSide === 'bottom') {
setContentOffset(prev => {
const viewportHeight = window.innerHeight;
const newOffset = Math.min(
viewportHeight - (contentRect.bottom - prev),
0
);
return newOffset;
});
} else if (contentSide === 'top') {
setContentOffset(prev => {
const newOffset = Math.max(contentRect.top - prev, 0);
return newOffset;
});
} else if (contentSide === 'left') {
setContentOffset(prev => {
const newOffset = Math.max(contentRect.left - prev, 0);
return newOffset;
});
} else if (contentSide === 'right') {
setContentOffset(prev => {
const viewportWidth = window.innerWidth;
const newOffset = Math.min(
viewportWidth - (contentRect.right - prev),
0
);
return newOffset;
});
}
};
let animationFrame: number = 0;
const requestUpdateContentOffset = () => {
cancelAnimationFrame(animationFrame);
animationFrame = requestAnimationFrame(updateContentOffset);
};
const updateContentOffset = () => {
if (!contentElement) return;
const contentRect = wrapperElement.getBoundingClientRect();
if (contentSide === 'bottom') {
setContentOffset(prev => {
const viewportHeight = window.innerHeight;
const newOffset = Math.min(
viewportHeight - (contentRect.bottom - prev),
0
);
return newOffset;
});
} else if (contentSide === 'top') {
setContentOffset(prev => {
const newOffset = Math.min(contentRect.top + prev, 0);
return newOffset;
});
} else if (contentSide === 'left') {
setContentOffset(prev => {
const newOffset = Math.min(contentRect.left + prev, 0);
return newOffset;
});
} else if (contentSide === 'right') {
setContentOffset(prev => {
const viewportWidth = window.innerWidth;
const newOffset = Math.min(
viewportWidth - (contentRect.right - prev),
0
);
return newOffset;
});
}
};
let animationFrame: number = 0;
const requestUpdateContentOffset = () => {
cancelAnimationFrame(animationFrame);
animationFrame = requestAnimationFrame(updateContentOffset);
};
const observer = new ResizeObserver(requestUpdateContentOffset);
observer.observe(wrapperElement);
window.addEventListener('resize', requestUpdateContentOffset);
requestUpdateContentOffset();
return () => {
observer.disconnect();
window.removeEventListener('resize', requestUpdateContentOffset);
cancelAnimationFrame(animationFrame);
};
}, [contentSide, open]);
const observer = new ResizeObserver(requestUpdateContentOffset);
observer.observe(wrapperElement);
window.addEventListener('resize', requestUpdateContentOffset);
requestUpdateContentOffset();
return () => {
observer.disconnect();
window.removeEventListener('resize', requestUpdateContentOffset);
cancelAnimationFrame(animationFrame);
};
},
[actualOpen, contentSide]
);
return {
handleOpenChange,
contentSide,
contentOffset: (sideOffset ?? 0) + contentOffset,
contentRef,
open: actualOpen,
};
};
@@ -10,7 +10,13 @@ export const DesktopMenu = ({
children,
items,
portalOptions,
rootOptions: { onOpenChange, defaultOpen, modal, ...rootOptions } = {},
rootOptions: {
onOpenChange,
defaultOpen,
modal,
open: rootOpen,
...rootOptions
} = {},
contentOptions: {
className = '',
style: contentStyle = {},
@@ -19,8 +25,9 @@ export const DesktopMenu = ({
...otherContentOptions
} = {},
}: MenuProps) => {
const { handleOpenChange, contentSide, contentOffset, contentRef } =
const { handleOpenChange, contentSide, contentOffset, contentRef, open } =
useMenuContentController({
open: rootOpen,
defaultOpen,
onOpenChange,
side,
@@ -31,6 +38,7 @@ export const DesktopMenu = ({
onOpenChange={handleOpenChange}
defaultOpen={defaultOpen}
modal={modal ?? false}
open={open}
{...rootOptions}
>
<DropdownMenu.Trigger
@@ -12,7 +12,12 @@ export const DesktopMenuSub = ({
children: propsChildren,
items,
portalOptions,
subOptions: { defaultOpen, onOpenChange, ...otherSubOptions } = {},
subOptions: {
defaultOpen,
onOpenChange,
open: rootOpen,
...otherSubOptions
} = {},
triggerOptions,
subContentOptions: {
className: subContentClassName = '',
@@ -27,9 +32,10 @@ export const DesktopMenuSub = ({
suffixIcon: <ArrowRightSmallIcon />,
});
const { handleOpenChange, contentOffset, contentRef } =
const { handleOpenChange, contentOffset, contentRef, open } =
useMenuContentController({
defaultOpen,
open: rootOpen,
onOpenChange,
side: 'right',
sideOffset: (sideOffset ?? 0) + 12,
@@ -39,6 +45,7 @@ export const DesktopMenuSub = ({
<DropdownMenu.Sub
defaultOpen={defaultOpen}
onOpenChange={handleOpenChange}
open={open}
{...otherSubOptions}
>
<DropdownMenu.SubTrigger className={className} {...otherProps}>