1- import React , { useEffect , useRef } from 'react' ;
1+ import React , { useCallback , useEffect , useLayoutEffect , useRef , useState } from 'react' ;
22import ReactDOM from 'react-dom' ;
33
44export type MenuItem = {
@@ -16,8 +16,62 @@ interface ContextMenuProps {
1616 onClose : ( ) => void ;
1717}
1818
19+ const EDGE_MARGIN = 8 ;
20+
1921const ContextMenu : React . FC < ContextMenuProps > = ( { isOpen, position, items, onClose } ) => {
2022 const menuRef = useRef < HTMLDivElement > ( null ) ;
23+ const [ menuStyle , setMenuStyle ] = useState < { top : number ; left : number ; maxHeight : number ; overflowY : React . CSSProperties [ 'overflowY' ] } > ( {
24+ top : position . y ,
25+ left : position . x ,
26+ maxHeight : 0 ,
27+ overflowY : 'visible' ,
28+ } ) ;
29+
30+ const recalculatePosition = useCallback ( ( ) => {
31+ const menu = menuRef . current ;
32+ if ( ! menu ) return ;
33+
34+ const { innerWidth, innerHeight } = window ;
35+ const rect = menu . getBoundingClientRect ( ) ;
36+ const maxHeight = Math . max ( innerHeight - EDGE_MARGIN * 2 , 0 ) ;
37+
38+ let left = rect . left ;
39+ let top = rect . top ;
40+
41+ if ( rect . right > innerWidth - EDGE_MARGIN ) {
42+ left = Math . max ( EDGE_MARGIN , innerWidth - rect . width - EDGE_MARGIN ) ;
43+ }
44+ if ( left < EDGE_MARGIN ) {
45+ left = EDGE_MARGIN ;
46+ }
47+
48+ if ( rect . bottom > innerHeight - EDGE_MARGIN ) {
49+ top = Math . max ( EDGE_MARGIN , innerHeight - rect . height - EDGE_MARGIN ) ;
50+ }
51+ if ( top < EDGE_MARGIN ) {
52+ top = EDGE_MARGIN ;
53+ }
54+
55+ const overflowY : React . CSSProperties [ 'overflowY' ] = rect . height > maxHeight ? 'auto' : 'visible' ;
56+
57+ setMenuStyle ( ( previous ) => {
58+ if (
59+ previous . top === top &&
60+ previous . left === left &&
61+ previous . maxHeight === maxHeight &&
62+ previous . overflowY === overflowY
63+ ) {
64+ return previous ;
65+ }
66+
67+ return {
68+ top,
69+ left,
70+ maxHeight,
71+ overflowY,
72+ } ;
73+ } ) ;
74+ } , [ ] ) ;
2175
2276 useEffect ( ( ) => {
2377 if ( ! isOpen ) return ;
@@ -41,20 +95,66 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ isOpen, position, items, onCl
4195 } ;
4296 } , [ isOpen , onClose ] ) ;
4397
98+ useLayoutEffect ( ( ) => {
99+ if ( ! isOpen ) return ;
100+
101+ setMenuStyle ( ( previous ) => {
102+ if ( previous . top === position . y && previous . left === position . x ) {
103+ return previous ;
104+ }
105+
106+ return {
107+ top : position . y ,
108+ left : position . x ,
109+ maxHeight : previous . maxHeight ,
110+ overflowY : previous . overflowY ,
111+ } ;
112+ } ) ;
113+
114+ const frame = requestAnimationFrame ( ( ) => {
115+ recalculatePosition ( ) ;
116+ } ) ;
117+
118+ return ( ) => cancelAnimationFrame ( frame ) ;
119+ } , [ isOpen , position . x , position . y , recalculatePosition ] ) ;
120+
121+ useLayoutEffect ( ( ) => {
122+ if ( ! isOpen ) return ;
123+
124+ const frame = requestAnimationFrame ( ( ) => {
125+ recalculatePosition ( ) ;
126+ } ) ;
127+
128+ return ( ) => cancelAnimationFrame ( frame ) ;
129+ } , [ isOpen , items , recalculatePosition ] ) ;
130+
131+ useEffect ( ( ) => {
132+ if ( ! isOpen ) return ;
133+
134+ const handleResize = ( ) => {
135+ recalculatePosition ( ) ;
136+ } ;
137+
138+ window . addEventListener ( 'resize' , handleResize ) ;
139+ return ( ) => {
140+ window . removeEventListener ( 'resize' , handleResize ) ;
141+ } ;
142+ } , [ isOpen , recalculatePosition ] ) ;
143+
44144 if ( ! isOpen ) return null ;
45-
46- const menuStyle : React . CSSProperties = {
47- top : position . y ,
48- left : position . x ,
49- } ;
50145
51146 const overlayRoot = document . getElementById ( 'overlay-root' ) ;
52147 if ( ! overlayRoot ) return null ;
53148
54149 return ReactDOM . createPortal (
55150 < div
56151 ref = { menuRef }
57- style = { menuStyle }
152+ style = { {
153+ top : menuStyle . top ,
154+ left : menuStyle . left ,
155+ maxHeight : menuStyle . maxHeight ? menuStyle . maxHeight : undefined ,
156+ overflowY : menuStyle . overflowY ,
157+ } }
58158 className = "fixed z-50 w-56 rounded-md bg-secondary p-1.5 shadow-2xl border border-border-color animate-fade-in-fast"
59159 >
60160 < ul className = "space-y-1" >
0 commit comments