@@ -7,16 +7,10 @@ import {
77 DropdownMenuTrigger ,
88 cn ,
99} from "@fluffylabs/shared-ui" ;
10- import {
11- type MouseEvent ,
12- type MouseEventHandler ,
13- type PropsWithChildren ,
14- type ReactNode ,
15- useEffect ,
16- useState ,
17- } from "react" ;
10+ import { type MouseEvent as ReactMouseEvent , useEffect , useRef , useState } from "react" ;
1811import { useNoteContext } from "./NoteContext" ;
1912import { DropdownMenuItemCopyButton } from "./SimpleComponents/DropdownMenuItemCopyButton" ;
13+ import { TwoStepDropdownMenuItem } from "./SimpleComponents/TwoStepDropdownMenuItem" ;
2014
2115export const NoteDropdown = ( {
2216 buttonClassName,
@@ -34,43 +28,65 @@ export const NoteDropdown = ({
3428 originalVersionLink,
3529 } = useNoteContext ( ) ;
3630
37- const handleOpenClose = ( e : MouseEvent < HTMLAnchorElement > ) => {
31+ const handleOpenClose = ( e : ReactMouseEvent < HTMLAnchorElement > ) => {
3832 e . stopPropagation ( ) ;
3933 handleSelectNote ( { type : active ? "close" : "currentVersion" } ) ;
4034 } ;
4135
42- const openInDifferentVersion = ( e : MouseEvent < HTMLAnchorElement > ) => {
36+ const openInDifferentVersion = ( e : ReactMouseEvent < HTMLAnchorElement > ) => {
4337 e . stopPropagation ( ) ;
4438 handleSelectNote ( { type : "originalVersion" } ) ;
4539 } ;
4640
47- const removeNote = ( e : MouseEvent < HTMLDivElement > ) => {
41+ const removeNote = ( e : ReactMouseEvent < HTMLDivElement > ) => {
4842 e . stopPropagation ( ) ;
4943 onDelete ?.( ) ;
5044 } ;
5145
52- const editNode = ( e : MouseEvent < HTMLDivElement > ) => {
46+ const editNode = ( e : ReactMouseEvent < HTMLDivElement > ) => {
5347 e . stopPropagation ( ) ;
5448 if ( ! active ) {
5549 handleSelectNote ( ) ;
5650 }
5751 handleEditClick ( ) ;
5852 } ;
5953
54+ const contentRef = useRef < HTMLDivElement > ( null ) ;
55+ const buttonRef = useRef < HTMLButtonElement > ( null ) ;
56+ const { setIsTracked : setTrackMousePosition , mousePositionRef } = useToggleableMousePositionTracking ( false ) ;
57+
58+ const handleCopyInitiated = ( ) => {
59+ setTrackMousePosition ( true ) ;
60+ } ;
61+
6062 const handleCopyComplete = ( ) => {
61- const escapeEvent = new KeyboardEvent ( "keydown" , {
62- key : "Escape" ,
63- code : "Escape" ,
64- keyCode : 27 ,
65- bubbles : true ,
66- } ) ;
67- document . dispatchEvent ( escapeEvent ) ;
63+ const isMouseOverButton =
64+ buttonRef . current && mousePositionRef . current
65+ ? isMouseOverElement ( mousePositionRef . current , buttonRef . current )
66+ : false ;
67+ const isMouseOverContent =
68+ contentRef . current && mousePositionRef . current
69+ ? isMouseOverElement ( mousePositionRef . current , contentRef . current )
70+ : false ;
71+
72+ const shouldDropdownBeClosed = ! isMouseOverButton && ! isMouseOverContent ;
73+
74+ if ( shouldDropdownBeClosed ) {
75+ const escapeEvent = new KeyboardEvent ( "keydown" , {
76+ key : "Escape" ,
77+ code : "Escape" ,
78+ keyCode : 27 ,
79+ bubbles : true ,
80+ } ) ;
81+ document . dispatchEvent ( escapeEvent ) ;
82+ }
6883 } ;
6984
7085 return (
7186 < DropdownMenu onOpenChange = { onOpenChange } >
7287 < DropdownMenuTrigger asChild >
7388 < Button
89+ ref = { buttonRef }
7490 variant = "ghost"
7591 intent = "neutralMedium"
7692 className = { cn ( "p-2 h-6" , buttonClassName ) }
@@ -92,11 +108,15 @@ export const NoteDropdown = ({
92108 </ svg >
93109 </ Button >
94110 </ DropdownMenuTrigger >
95- < DropdownMenuContent className = "w-56" align = "end" >
111+ < DropdownMenuContent className = "w-56" align = "end" ref = { contentRef } >
96112 < DropdownMenuItem asChild >
97113 < a href = { `#${ currentVersionLink } ` } onClick = { handleOpenClose } className = "flex justify-between items-center" >
98114 < span > Open</ span >
99- < DropdownMenuItemCopyButton href = { `/#${ currentVersionLink } ` } onCopyComplete = { handleCopyComplete } />
115+ < DropdownMenuItemCopyButton
116+ href = { `/#${ currentVersionLink } ` }
117+ onCopyComplete = { handleCopyComplete }
118+ onCopyInitiated = { handleCopyInitiated }
119+ />
100120 </ a >
101121 </ DropdownMenuItem >
102122 { ! note . current . isUpToDate && (
@@ -109,7 +129,11 @@ export const NoteDropdown = ({
109129 className = "flex justify-between items-center"
110130 >
111131 < span > Open in v{ noteOriginalVersionShort } </ span >
112- < DropdownMenuItemCopyButton href = { `/#${ originalVersionLink } ` } onCopyComplete = { handleCopyComplete } />
132+ < DropdownMenuItemCopyButton
133+ href = { `/#${ originalVersionLink } ` }
134+ onCopyComplete = { handleCopyComplete }
135+ onCopyInitiated = { handleCopyInitiated }
136+ />
113137 </ a >
114138 </ DropdownMenuItem >
115139 </ >
@@ -130,44 +154,30 @@ export const NoteDropdown = ({
130154 ) ;
131155} ;
132156
133- const TwoStepDropdownMenuItem = ( {
134- children,
135- confirmationSlot,
136- onClick,
137- } : PropsWithChildren < { confirmationSlot : ReactNode ; onClick : MouseEventHandler < HTMLDivElement > } > ) => {
138- const [ isConfirmation , setIsConfirmation ] = useState ( false ) ;
157+ const useToggleableMousePositionTracking = ( initialIsTracked : boolean ) => {
158+ const [ isTracked , setIsTracked ] = useState ( initialIsTracked ) ;
159+ const mousePositionRef = useRef < { x : number ; y : number } > ( { x : 0 , y : 0 } ) ;
139160
140161 useEffect ( ( ) => {
141- if ( ! isConfirmation ) {
162+ if ( ! isTracked ) {
142163 return ;
143164 }
144165
145- const timeoutHandle = setTimeout ( ( ) => {
146- setIsConfirmation ( false ) ;
147- } , 2000 ) ;
166+ const handler = ( e : MouseEvent ) => {
167+ mousePositionRef . current = { x : e . clientX , y : e . clientY } ;
168+ } ;
169+
170+ document . addEventListener ( "mousemove" , handler ) ;
148171
149172 return ( ) => {
150- clearTimeout ( timeoutHandle ) ;
173+ document . removeEventListener ( "mousemove" , handler ) ;
151174 } ;
152- } , [ isConfirmation ] ) ;
153-
154- const handleOnClick : MouseEventHandler < HTMLDivElement > = ( e ) => {
155- if ( ! isConfirmation ) {
156- e . preventDefault ( ) ;
157- e . stopPropagation ( ) ;
158- setIsConfirmation ( true ) ;
159- } else {
160- onClick ( e ) ;
161- }
162- } ;
175+ } , [ isTracked ] ) ;
163176
164- return (
165- < DropdownMenuItem
166- onClick = { handleOnClick }
167- className = { cn ( isConfirmation ? "text-destructive hover:bg-destructive/20 hover:text-destructive" : "" ) }
168- >
169- { ! isConfirmation && children }
170- { isConfirmation && confirmationSlot }
171- </ DropdownMenuItem >
172- ) ;
177+ return { isTracked, setIsTracked, mousePositionRef } ;
178+ } ;
179+
180+ const isMouseOverElement = ( mousePos : { x : number ; y : number } , element : HTMLElement ) => {
181+ const rect = element . getBoundingClientRect ( ) ;
182+ return mousePos . x >= rect . left && mousePos . x <= rect . right && mousePos . y >= rect . top && mousePos . y <= rect . bottom ;
173183} ;
0 commit comments