diff --git a/packages/frontend/component/package.json b/packages/frontend/component/package.json index e2ae1f2f67..e2bfb50f98 100644 --- a/packages/frontend/component/package.json +++ b/packages/frontend/component/package.json @@ -105,7 +105,7 @@ "@vanilla-extract/css": "^1.14.2", "fake-indexeddb": "^6.0.0", "storybook": "^8.2.9", - "storybook-dark-mode": "4.0.2", + "storybook-dark-mode": "4.0.1", "typescript": "^5.4.5", "unplugin-swc": "^1.5.1", "vite": "^5.2.8", diff --git a/packages/frontend/component/src/ui/menu/desktop/controller.ts b/packages/frontend/component/src/ui/menu/desktop/controller.ts new file mode 100644 index 0000000000..0a5e94f997 --- /dev/null +++ b/packages/frontend/component/src/ui/menu/desktop/controller.ts @@ -0,0 +1,87 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +export const useMenuContentController = ({ + onOpenChange, + side, + defaultOpen, + sideOffset, +}: { + defaultOpen?: boolean; + side?: 'top' | 'bottom' | 'left' | 'right'; + onOpenChange?: (open: boolean) => void; + sideOffset?: number; +} = {}) => { + const [open, setOpen] = useState(defaultOpen ?? false); + const contentSide = side ?? 'bottom'; + const [contentOffset, setContentOffset] = useState(0); + const contentRef = useRef(null); + + const handleOpenChange = useCallback( + (open: boolean) => { + setOpen(open); + onOpenChange?.(open); + }, + [onOpenChange] + ); + useEffect(() => { + if (!open || !contentRef.current) return; + + const wrapperElement = contentRef.current.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 observer = new ResizeObserver(requestUpdateContentOffset); + observer.observe(wrapperElement); + window.addEventListener('resize', requestUpdateContentOffset); + requestUpdateContentOffset(); + return () => { + observer.disconnect(); + window.removeEventListener('resize', requestUpdateContentOffset); + cancelAnimationFrame(animationFrame); + }; + }, [contentSide, open]); + + return { + handleOpenChange, + contentSide, + contentOffset: (sideOffset ?? 0) + contentOffset, + contentRef, + }; +}; diff --git a/packages/frontend/component/src/ui/menu/desktop/root.tsx b/packages/frontend/component/src/ui/menu/desktop/root.tsx index 0e486a4f81..5c91ec2190 100644 --- a/packages/frontend/component/src/ui/menu/desktop/root.tsx +++ b/packages/frontend/component/src/ui/menu/desktop/root.tsx @@ -3,21 +3,35 @@ import clsx from 'clsx'; import type { MenuProps } from '../menu.types'; import * as styles from '../styles.css'; +import { useMenuContentController } from './controller'; import * as desktopStyles from './styles.css'; export const DesktopMenu = ({ children, items, portalOptions, - rootOptions, + rootOptions: { onOpenChange, defaultOpen, ...rootOptions } = {}, contentOptions: { className = '', style: contentStyle = {}, + side, + sideOffset, ...otherContentOptions } = {}, }: MenuProps) => { + const { handleOpenChange, contentSide, contentOffset, contentRef } = + useMenuContentController({ + defaultOpen, + onOpenChange, + side, + sideOffset: (sideOffset ?? 0) + 5, + }); return ( - + {children} @@ -27,11 +41,13 @@ export const DesktopMenu = ({ desktopStyles.contentAnimation, className )} - sideOffset={5} align="start" + ref={contentRef} + side={contentSide} style={{ zIndex: 'var(--affine-z-index-popover)', ...contentStyle }} + avoidCollisions={false} + sideOffset={contentOffset} {...otherContentOptions} - side="bottom" > {items} diff --git a/packages/frontend/component/src/ui/menu/desktop/styles.css.ts b/packages/frontend/component/src/ui/menu/desktop/styles.css.ts index 1495cdebc3..57640e2b21 100644 --- a/packages/frontend/component/src/ui/menu/desktop/styles.css.ts +++ b/packages/frontend/component/src/ui/menu/desktop/styles.css.ts @@ -4,10 +4,12 @@ const slideDown = keyframes({ from: { opacity: 0, transform: 'translateY(-10px)', + pointerEvents: 'none', }, to: { opacity: 1, transform: 'translateY(0)', + pointerEvents: 'none', }, }); diff --git a/packages/frontend/component/src/ui/menu/desktop/sub.tsx b/packages/frontend/component/src/ui/menu/desktop/sub.tsx index 2d2909a1c1..802221ef70 100644 --- a/packages/frontend/component/src/ui/menu/desktop/sub.tsx +++ b/packages/frontend/component/src/ui/menu/desktop/sub.tsx @@ -6,15 +6,18 @@ import { useMemo } from 'react'; import type { MenuSubProps } from '../menu.types'; import * as styles from '../styles.css'; import { useMenuItem } from '../use-menu-item'; +import { useMenuContentController } from './controller'; export const DesktopMenuSub = ({ children: propsChildren, items, portalOptions, - subOptions, + subOptions: { defaultOpen, onOpenChange, ...otherSubOptions } = {}, triggerOptions, subContentOptions: { className: subContentClassName = '', + sideOffset, + style: contentStyle, ...otherSubContentOptions } = {}, }: MenuSubProps) => { @@ -24,18 +27,33 @@ export const DesktopMenuSub = ({ suffixIcon: , }); + const { handleOpenChange, contentOffset, contentRef } = + useMenuContentController({ + defaultOpen, + onOpenChange, + side: 'right', + sideOffset: (sideOffset ?? 0) + 12, + }); + return ( - + {children} clsx(styles.menuContent, subContentClassName), [subContentClassName] )} + style={{ zIndex: 'var(--affine-z-index-popover)', ...contentStyle }} + avoidCollisions={false} {...otherSubContentOptions} > {items} diff --git a/yarn.lock b/yarn.lock index bbdc73ab5b..c0a8bb98d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -384,7 +384,7 @@ __metadata: rxjs: "npm:^7.8.1" sonner: "npm:^1.4.41" storybook: "npm:^8.2.9" - storybook-dark-mode: "npm:4.0.2" + storybook-dark-mode: "npm:4.0.1" swr: "npm:^2.2.5" typescript: "npm:^5.4.5" unplugin-swc: "npm:^1.5.1" @@ -33028,9 +33028,9 @@ __metadata: languageName: node linkType: hard -"storybook-dark-mode@npm:4.0.2": - version: 4.0.2 - resolution: "storybook-dark-mode@npm:4.0.2" +"storybook-dark-mode@npm:4.0.1": + version: 4.0.1 + resolution: "storybook-dark-mode@npm:4.0.1" dependencies: "@storybook/components": "npm:^8.0.0" "@storybook/core-events": "npm:^8.0.0" @@ -33040,7 +33040,7 @@ __metadata: "@storybook/theming": "npm:^8.0.0" fast-deep-equal: "npm:^3.1.3" memoizerific: "npm:^1.11.3" - checksum: 10/c9ef7bc6734df7486ff763c9da3c69505269eaf5fd7b5b489553f023b363ea892862241e6d701ad647ca5d1e64fd9a2646b8985c7ea8ac97a3bca87891db6fe5 + checksum: 10/3225e5bdaba0ea76b65d642202d9712d7de234e3b5673fb46e444892ab114be207dd287778e2002b662ec35bb8153d2624ff280ce51c5299fb13c711431dad40 languageName: node linkType: hard