Skip to content

Commit 02f0d7a

Browse files
committed
1 parent 197996d commit 02f0d7a

File tree

6 files changed

+136
-13
lines changed

6 files changed

+136
-13
lines changed

packages/frontend/component/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
"@vanilla-extract/css": "^1.14.2",
106106
"fake-indexeddb": "^6.0.0",
107107
"storybook": "^8.2.9",
108-
"storybook-dark-mode": "4.0.2",
108+
"storybook-dark-mode": "4.0.1",
109109
"typescript": "^5.4.5",
110110
"unplugin-swc": "^1.5.1",
111111
"vite": "^5.2.8",
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { useCallback, useEffect, useRef, useState } from 'react';
2+
3+
export const useMenuContentController = ({
4+
onOpenChange,
5+
side,
6+
defaultOpen,
7+
sideOffset,
8+
}: {
9+
defaultOpen?: boolean;
10+
side?: 'top' | 'bottom' | 'left' | 'right';
11+
onOpenChange?: (open: boolean) => void;
12+
sideOffset?: number;
13+
} = {}) => {
14+
const [open, setOpen] = useState(defaultOpen ?? false);
15+
const contentSide = side ?? 'bottom';
16+
const [contentOffset, setContentOffset] = useState<number>(0);
17+
const contentRef = useRef<HTMLDivElement>(null);
18+
19+
const handleOpenChange = useCallback(
20+
(open: boolean) => {
21+
setOpen(open);
22+
onOpenChange?.(open);
23+
},
24+
[onOpenChange]
25+
);
26+
useEffect(() => {
27+
if (!open || !contentRef.current) return;
28+
29+
const wrapperElement = contentRef.current.parentNode as HTMLDivElement;
30+
31+
const updateContentOffset = () => {
32+
if (!contentRef.current) return;
33+
const contentRect = wrapperElement.getBoundingClientRect();
34+
if (contentSide === 'bottom') {
35+
setContentOffset(prev => {
36+
const viewportHeight = window.innerHeight;
37+
const newOffset = Math.min(
38+
viewportHeight - (contentRect.bottom - prev),
39+
0
40+
);
41+
return newOffset;
42+
});
43+
} else if (contentSide === 'top') {
44+
setContentOffset(prev => {
45+
const newOffset = Math.max(contentRect.top - prev, 0);
46+
return newOffset;
47+
});
48+
} else if (contentSide === 'left') {
49+
setContentOffset(prev => {
50+
const newOffset = Math.max(contentRect.left - prev, 0);
51+
return newOffset;
52+
});
53+
} else if (contentSide === 'right') {
54+
setContentOffset(prev => {
55+
const viewportWidth = window.innerWidth;
56+
const newOffset = Math.min(
57+
viewportWidth - (contentRect.right - prev),
58+
0
59+
);
60+
return newOffset;
61+
});
62+
}
63+
};
64+
let animationFrame: number = 0;
65+
const requestUpdateContentOffset = () => {
66+
cancelAnimationFrame(animationFrame);
67+
animationFrame = requestAnimationFrame(updateContentOffset);
68+
};
69+
70+
const observer = new ResizeObserver(requestUpdateContentOffset);
71+
observer.observe(wrapperElement);
72+
window.addEventListener('resize', requestUpdateContentOffset);
73+
requestUpdateContentOffset();
74+
return () => {
75+
observer.disconnect();
76+
window.removeEventListener('resize', requestUpdateContentOffset);
77+
cancelAnimationFrame(animationFrame);
78+
};
79+
}, [contentSide, open]);
80+
81+
return {
82+
handleOpenChange,
83+
contentSide,
84+
contentOffset: (sideOffset ?? 0) + contentOffset,
85+
contentRef,
86+
};
87+
};

packages/frontend/component/src/ui/menu/desktop/root.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,35 @@ import clsx from 'clsx';
33

44
import type { MenuProps } from '../menu.types';
55
import * as styles from '../styles.css';
6+
import { useMenuContentController } from './controller';
67
import * as desktopStyles from './styles.css';
78

89
export const DesktopMenu = ({
910
children,
1011
items,
1112
portalOptions,
12-
rootOptions,
13+
rootOptions: { onOpenChange, defaultOpen, ...rootOptions } = {},
1314
contentOptions: {
1415
className = '',
1516
style: contentStyle = {},
17+
side,
18+
sideOffset,
1619
...otherContentOptions
1720
} = {},
1821
}: MenuProps) => {
22+
const { handleOpenChange, contentSide, contentOffset, contentRef } =
23+
useMenuContentController({
24+
defaultOpen,
25+
onOpenChange,
26+
side,
27+
sideOffset: (sideOffset ?? 0) + 5,
28+
});
1929
return (
20-
<DropdownMenu.Root {...rootOptions}>
30+
<DropdownMenu.Root
31+
onOpenChange={handleOpenChange}
32+
defaultOpen={defaultOpen}
33+
{...rootOptions}
34+
>
2135
<DropdownMenu.Trigger asChild>{children}</DropdownMenu.Trigger>
2236

2337
<DropdownMenu.Portal {...portalOptions}>
@@ -27,11 +41,13 @@ export const DesktopMenu = ({
2741
desktopStyles.contentAnimation,
2842
className
2943
)}
30-
sideOffset={5}
3144
align="start"
45+
ref={contentRef}
46+
side={contentSide}
3247
style={{ zIndex: 'var(--affine-z-index-popover)', ...contentStyle }}
48+
avoidCollisions={false}
49+
sideOffset={contentOffset}
3350
{...otherContentOptions}
34-
side="bottom"
3551
>
3652
{items}
3753
</DropdownMenu.Content>

packages/frontend/component/src/ui/menu/desktop/styles.css.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ const slideDown = keyframes({
44
from: {
55
opacity: 0,
66
transform: 'translateY(-10px)',
7+
pointerEvents: 'none',
78
},
89
to: {
910
opacity: 1,
1011
transform: 'translateY(0)',
12+
pointerEvents: 'none',
1113
},
1214
});
1315

packages/frontend/component/src/ui/menu/desktop/sub.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@ import { useMemo } from 'react';
66
import type { MenuSubProps } from '../menu.types';
77
import * as styles from '../styles.css';
88
import { useMenuItem } from '../use-menu-item';
9+
import { useMenuContentController } from './controller';
910

1011
export const DesktopMenuSub = ({
1112
children: propsChildren,
1213
items,
1314
portalOptions,
14-
subOptions,
15+
subOptions: { defaultOpen, onOpenChange, ...otherSubOptions } = {},
1516
triggerOptions,
1617
subContentOptions: {
1718
className: subContentClassName = '',
19+
sideOffset,
20+
style: contentStyle,
1821
...otherSubContentOptions
1922
} = {},
2023
}: MenuSubProps) => {
@@ -24,18 +27,33 @@ export const DesktopMenuSub = ({
2427
suffixIcon: <ArrowRightSmallIcon />,
2528
});
2629

30+
const { handleOpenChange, contentOffset, contentRef } =
31+
useMenuContentController({
32+
defaultOpen,
33+
onOpenChange,
34+
side: 'right',
35+
sideOffset: (sideOffset ?? 0) + 12,
36+
});
37+
2738
return (
28-
<DropdownMenu.Sub {...subOptions}>
39+
<DropdownMenu.Sub
40+
defaultOpen={defaultOpen}
41+
onOpenChange={handleOpenChange}
42+
{...otherSubOptions}
43+
>
2944
<DropdownMenu.SubTrigger className={className} {...otherProps}>
3045
{children}
3146
</DropdownMenu.SubTrigger>
3247
<DropdownMenu.Portal {...portalOptions}>
3348
<DropdownMenu.SubContent
34-
sideOffset={12}
49+
sideOffset={contentOffset}
50+
ref={contentRef}
3551
className={useMemo(
3652
() => clsx(styles.menuContent, subContentClassName),
3753
[subContentClassName]
3854
)}
55+
style={{ zIndex: 'var(--affine-z-index-popover)', ...contentStyle }}
56+
avoidCollisions={false}
3957
{...otherSubContentOptions}
4058
>
4159
{items}

yarn.lock

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ __metadata:
384384
rxjs: "npm:^7.8.1"
385385
sonner: "npm:^1.4.41"
386386
storybook: "npm:^8.2.9"
387-
storybook-dark-mode: "npm:4.0.2"
387+
storybook-dark-mode: "npm:4.0.1"
388388
swr: "npm:^2.2.5"
389389
typescript: "npm:^5.4.5"
390390
unplugin-swc: "npm:^1.5.1"
@@ -33028,9 +33028,9 @@ __metadata:
3302833028
languageName: node
3302933029
linkType: hard
3303033030

33031-
"storybook-dark-mode@npm:4.0.2":
33032-
version: 4.0.2
33033-
resolution: "storybook-dark-mode@npm:4.0.2"
33031+
"storybook-dark-mode@npm:4.0.1":
33032+
version: 4.0.1
33033+
resolution: "storybook-dark-mode@npm:4.0.1"
3303433034
dependencies:
3303533035
"@storybook/components": "npm:^8.0.0"
3303633036
"@storybook/core-events": "npm:^8.0.0"
@@ -33040,7 +33040,7 @@ __metadata:
3304033040
"@storybook/theming": "npm:^8.0.0"
3304133041
fast-deep-equal: "npm:^3.1.3"
3304233042
memoizerific: "npm:^1.11.3"
33043-
checksum: 10/c9ef7bc6734df7486ff763c9da3c69505269eaf5fd7b5b489553f023b363ea892862241e6d701ad647ca5d1e64fd9a2646b8985c7ea8ac97a3bca87891db6fe5
33043+
checksum: 10/3225e5bdaba0ea76b65d642202d9712d7de234e3b5673fb46e444892ab114be207dd287778e2002b662ec35bb8153d2624ff280ce51c5299fb13c711431dad40
3304433044
languageName: node
3304533045
linkType: hard
3304633046

0 commit comments

Comments
 (0)