fix(core): fix menu shaking (#8187)
This commit is contained in:
@@ -1,95 +0,0 @@
|
|||||||
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 handleOpenChange = useCallback(
|
|
||||||
(open: boolean) => {
|
|
||||||
setOpen(open);
|
|
||||||
onOpenChange?.(open);
|
|
||||||
},
|
|
||||||
[onOpenChange]
|
|
||||||
);
|
|
||||||
const contentRef = useRefEffect<HTMLDivElement>(
|
|
||||||
contentElement => {
|
|
||||||
if (!actualOpen) return;
|
|
||||||
|
|
||||||
const wrapperElement = contentElement.parentNode as HTMLDivElement;
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[actualOpen, contentSide]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
handleOpenChange,
|
|
||||||
contentSide,
|
|
||||||
contentOffset: (sideOffset ?? 0) + contentOffset,
|
|
||||||
contentRef,
|
|
||||||
open: actualOpen,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -4,7 +4,6 @@ import React from 'react';
|
|||||||
|
|
||||||
import type { MenuProps } from '../menu.types';
|
import type { MenuProps } from '../menu.types';
|
||||||
import * as styles from '../styles.css';
|
import * as styles from '../styles.css';
|
||||||
import { useMenuContentController } from './controller';
|
|
||||||
import * as desktopStyles from './styles.css';
|
import * as desktopStyles from './styles.css';
|
||||||
|
|
||||||
export const DesktopMenu = ({
|
export const DesktopMenu = ({
|
||||||
@@ -12,36 +11,18 @@ export const DesktopMenu = ({
|
|||||||
items,
|
items,
|
||||||
noPortal,
|
noPortal,
|
||||||
portalOptions,
|
portalOptions,
|
||||||
rootOptions: {
|
rootOptions: { defaultOpen, modal, ...rootOptions } = {},
|
||||||
onOpenChange,
|
|
||||||
defaultOpen,
|
|
||||||
modal,
|
|
||||||
open: rootOpen,
|
|
||||||
...rootOptions
|
|
||||||
} = {},
|
|
||||||
contentOptions: {
|
contentOptions: {
|
||||||
className = '',
|
className = '',
|
||||||
style: contentStyle = {},
|
style: contentStyle = {},
|
||||||
side,
|
|
||||||
sideOffset,
|
|
||||||
...otherContentOptions
|
...otherContentOptions
|
||||||
} = {},
|
} = {},
|
||||||
}: MenuProps) => {
|
}: MenuProps) => {
|
||||||
const { handleOpenChange, contentSide, contentOffset, contentRef, open } =
|
|
||||||
useMenuContentController({
|
|
||||||
open: rootOpen,
|
|
||||||
defaultOpen,
|
|
||||||
onOpenChange,
|
|
||||||
side,
|
|
||||||
sideOffset: (sideOffset ?? 0) + 5,
|
|
||||||
});
|
|
||||||
const ContentWrapper = noPortal ? React.Fragment : DropdownMenu.Portal;
|
const ContentWrapper = noPortal ? React.Fragment : DropdownMenu.Portal;
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Root
|
<DropdownMenu.Root
|
||||||
onOpenChange={handleOpenChange}
|
|
||||||
defaultOpen={defaultOpen}
|
defaultOpen={defaultOpen}
|
||||||
modal={modal ?? false}
|
modal={modal ?? false}
|
||||||
open={open}
|
|
||||||
{...rootOptions}
|
{...rootOptions}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
@@ -62,11 +43,7 @@ export const DesktopMenu = ({
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
align="start"
|
align="start"
|
||||||
ref={contentRef}
|
|
||||||
side={contentSide}
|
|
||||||
style={{ zIndex: 'var(--affine-z-index-popover)', ...contentStyle }}
|
style={{ zIndex: 'var(--affine-z-index-popover)', ...contentStyle }}
|
||||||
avoidCollisions={false}
|
|
||||||
sideOffset={contentOffset}
|
|
||||||
{...otherContentOptions}
|
{...otherContentOptions}
|
||||||
>
|
>
|
||||||
{items}
|
{items}
|
||||||
|
|||||||
@@ -6,22 +6,15 @@ import { useMemo } from 'react';
|
|||||||
import type { MenuSubProps } from '../menu.types';
|
import type { MenuSubProps } from '../menu.types';
|
||||||
import * as styles from '../styles.css';
|
import * as styles from '../styles.css';
|
||||||
import { useMenuItem } from '../use-menu-item';
|
import { useMenuItem } from '../use-menu-item';
|
||||||
import { useMenuContentController } from './controller';
|
|
||||||
|
|
||||||
export const DesktopMenuSub = ({
|
export const DesktopMenuSub = ({
|
||||||
children: propsChildren,
|
children: propsChildren,
|
||||||
items,
|
items,
|
||||||
portalOptions,
|
portalOptions,
|
||||||
subOptions: {
|
subOptions: { defaultOpen, ...otherSubOptions } = {},
|
||||||
defaultOpen,
|
|
||||||
onOpenChange,
|
|
||||||
open: rootOpen,
|
|
||||||
...otherSubOptions
|
|
||||||
} = {},
|
|
||||||
triggerOptions,
|
triggerOptions,
|
||||||
subContentOptions: {
|
subContentOptions: {
|
||||||
className: subContentClassName = '',
|
className: subContentClassName = '',
|
||||||
sideOffset,
|
|
||||||
style: contentStyle,
|
style: contentStyle,
|
||||||
...otherSubContentOptions
|
...otherSubContentOptions
|
||||||
} = {},
|
} = {},
|
||||||
@@ -32,35 +25,18 @@ export const DesktopMenuSub = ({
|
|||||||
suffixIcon: <ArrowRightSmallIcon />,
|
suffixIcon: <ArrowRightSmallIcon />,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { handleOpenChange, contentOffset, contentRef, open } =
|
|
||||||
useMenuContentController({
|
|
||||||
defaultOpen,
|
|
||||||
open: rootOpen,
|
|
||||||
onOpenChange,
|
|
||||||
side: 'right',
|
|
||||||
sideOffset: (sideOffset ?? 0) + 12,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Sub
|
<DropdownMenu.Sub defaultOpen={defaultOpen} {...otherSubOptions}>
|
||||||
defaultOpen={defaultOpen}
|
|
||||||
onOpenChange={handleOpenChange}
|
|
||||||
open={open}
|
|
||||||
{...otherSubOptions}
|
|
||||||
>
|
|
||||||
<DropdownMenu.SubTrigger className={className} {...otherProps}>
|
<DropdownMenu.SubTrigger className={className} {...otherProps}>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenu.SubTrigger>
|
</DropdownMenu.SubTrigger>
|
||||||
<DropdownMenu.Portal {...portalOptions}>
|
<DropdownMenu.Portal {...portalOptions}>
|
||||||
<DropdownMenu.SubContent
|
<DropdownMenu.SubContent
|
||||||
sideOffset={contentOffset}
|
|
||||||
ref={contentRef}
|
|
||||||
className={useMemo(
|
className={useMemo(
|
||||||
() => clsx(styles.menuContent, subContentClassName),
|
() => clsx(styles.menuContent, subContentClassName),
|
||||||
[subContentClassName]
|
[subContentClassName]
|
||||||
)}
|
)}
|
||||||
style={{ zIndex: 'var(--affine-z-index-popover)', ...contentStyle }}
|
style={{ zIndex: 'var(--affine-z-index-popover)', ...contentStyle }}
|
||||||
avoidCollisions={false}
|
|
||||||
{...otherSubContentOptions}
|
{...otherSubContentOptions}
|
||||||
>
|
>
|
||||||
{items}
|
{items}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export const workspaceTypeIcon = style({
|
|||||||
color: cssVar('iconSecondary'),
|
color: cssVar('iconSecondary'),
|
||||||
});
|
});
|
||||||
export const scrollbar = style({
|
export const scrollbar = style({
|
||||||
transform: 'translateX(8px)',
|
|
||||||
width: '4px',
|
width: '4px',
|
||||||
});
|
});
|
||||||
export const workspaceCard = style({
|
export const workspaceCard = style({
|
||||||
|
|||||||
Reference in New Issue
Block a user