@@ -19,11 +19,14 @@ import {
1919 alpha ,
2020 Box ,
2121 Button ,
22+ ClickAwayListener ,
2223 IconButton ,
2324 ListItemIcon ,
2425 ListItemText ,
25- Menu ,
2626 MenuItem ,
27+ MenuList ,
28+ Paper ,
29+ Popper ,
2730 Tooltip ,
2831 Typography ,
2932 useMediaQuery ,
@@ -140,12 +143,28 @@ export function SingleActivityRenderer({
140143 const theme = useTheme ( ) ;
141144 const isSmallScreen = useMediaQuery ( theme . breakpoints . down ( 'lg' ) ) ;
142145 const [ snapMenuAnchor , setSnapMenuAnchor ] = useState < HTMLElement | null > ( null ) ;
146+ const [ snapMenuOpenReason , setSnapMenuOpenReason ] = useState < 'hover' | 'click' | null > ( null ) ;
147+ const openTimerRef = useRef < NodeJS . Timeout | null > ( null ) ;
148+ const closeTimerRef = useRef < NodeJS . Timeout | null > ( null ) ;
149+ const menuListRef = useRef < HTMLUListElement > ( null ) ;
143150 const isSnapMenuOpen = Boolean ( snapMenuAnchor ) ;
144151
145152 useEffect ( ( ) => {
146153 containerElementRef . current = document . getElementById ( 'main' ) ;
147154 } , [ ] ) ;
148155
156+ // Cleanup timers on unmount
157+ useEffect ( ( ) => {
158+ return ( ) => {
159+ if ( openTimerRef . current ) {
160+ clearTimeout ( openTimerRef . current ) ;
161+ }
162+ if ( closeTimerRef . current ) {
163+ clearTimeout ( closeTimerRef . current ) ;
164+ }
165+ } ;
166+ } , [ ] ) ;
167+
149168 // Styles of different activity locations
150169 const locationStyles = {
151170 full : {
@@ -409,93 +428,208 @@ export function SingleActivityRenderer({
409428 < IconButton
410429 size = "small"
411430 title = { t ( 'Window' ) }
412- onMouseEnter = { event => setSnapMenuAnchor ( event . currentTarget ) }
413- onClick = { event => setSnapMenuAnchor ( event . currentTarget ) }
431+ aria-haspopup = "menu"
432+ aria-expanded = { isSnapMenuOpen }
433+ aria-controls = { isSnapMenuOpen ? 'snap-menu' : undefined }
434+ onMouseEnter = { event => {
435+ // Clear any existing close timer
436+ if ( closeTimerRef . current ) {
437+ clearTimeout ( closeTimerRef . current ) ;
438+ closeTimerRef . current = null ;
439+ }
440+ // Set 350ms timer to open on hover
441+ if ( openTimerRef . current ) {
442+ clearTimeout ( openTimerRef . current ) ;
443+ }
444+ const target = event . currentTarget ;
445+ openTimerRef . current = setTimeout ( ( ) => {
446+ setSnapMenuAnchor ( target ) ;
447+ setSnapMenuOpenReason ( 'hover' ) ;
448+ openTimerRef . current = null ;
449+ } , 350 ) ;
450+ } }
451+ onMouseLeave = { ( ) => {
452+ // Cancel open timer if pointer leaves before 350ms
453+ if ( openTimerRef . current ) {
454+ clearTimeout ( openTimerRef . current ) ;
455+ openTimerRef . current = null ;
456+ }
457+ // If menu is open due to hover, start close timer
458+ if ( snapMenuOpenReason === 'hover' && isSnapMenuOpen ) {
459+ if ( closeTimerRef . current ) {
460+ clearTimeout ( closeTimerRef . current ) ;
461+ }
462+ closeTimerRef . current = setTimeout ( ( ) => {
463+ setSnapMenuAnchor ( null ) ;
464+ setSnapMenuOpenReason ( null ) ;
465+ closeTimerRef . current = null ;
466+ } , 150 ) ;
467+ }
468+ } }
469+ onClick = { event => {
470+ // Clear any timers
471+ if ( openTimerRef . current ) {
472+ clearTimeout ( openTimerRef . current ) ;
473+ openTimerRef . current = null ;
474+ }
475+ if ( closeTimerRef . current ) {
476+ clearTimeout ( closeTimerRef . current ) ;
477+ closeTimerRef . current = null ;
478+ }
479+ // Toggle menu on click
480+ if ( isSnapMenuOpen && snapMenuOpenReason === 'click' ) {
481+ setSnapMenuAnchor ( null ) ;
482+ setSnapMenuOpenReason ( null ) ;
483+ } else {
484+ setSnapMenuAnchor ( event . currentTarget ) ;
485+ setSnapMenuOpenReason ( 'click' ) ;
486+ }
487+ } }
488+ onKeyDown = { event => {
489+ if ( event . key === 'Enter' || event . key === 'ArrowDown' ) {
490+ event . preventDefault ( ) ;
491+ if ( openTimerRef . current ) {
492+ clearTimeout ( openTimerRef . current ) ;
493+ openTimerRef . current = null ;
494+ }
495+ if ( ! isSnapMenuOpen ) {
496+ setSnapMenuAnchor ( event . currentTarget as HTMLElement ) ;
497+ setSnapMenuOpenReason ( 'click' ) ;
498+ }
499+ }
500+ } }
414501 >
415502 < Icon icon = "mdi:dock-window" />
416503 </ IconButton >
417- < Menu
418- anchorEl = { snapMenuAnchor }
504+ < Popper
505+ id = "snap-menu"
419506 open = { isSnapMenuOpen }
420- onClose = { ( ) => setSnapMenuAnchor ( null ) }
421- anchorOrigin = { { vertical : 'bottom' , horizontal : 'center' } }
422- transformOrigin = { { vertical : 'top' , horizontal : 'center' } }
423- MenuListProps = { {
424- onMouseLeave : ( ) => setSnapMenuAnchor ( null ) ,
425- 'aria-label' : t ( 'Window' ) ,
426- } }
507+ anchorEl = { snapMenuAnchor }
508+ placement = "bottom-end"
509+ sx = { { zIndex : theme . zIndex . modal } }
427510 >
428- < MenuItem
429- selected = { location === 'full' }
430- aria-label = { t ( 'Fullscreen' ) }
431- title = { t ( 'Fullscreen' ) }
432- onClick = { ( ) => {
433- Activity . update ( id , { location : 'full' } ) ;
434- setSnapMenuAnchor ( null ) ;
435- } }
436- >
437- < ListItemIcon >
438- < Icon icon = "mdi:fullscreen" />
439- </ ListItemIcon >
440- < ListItemText > { t ( 'Fullscreen' ) } </ ListItemText >
441- </ MenuItem >
442- < MenuItem
443- selected = { location === 'split-left' }
444- aria-label = { t ( 'Snap Left' ) }
445- title = { t ( 'Snap Left' ) }
446- onClick = { ( ) => {
447- Activity . update ( id , { location : 'split-left' } ) ;
448- setSnapMenuAnchor ( null ) ;
449- } }
450- >
451- < ListItemIcon >
452- < Icon icon = "mdi:dock-left" />
453- </ ListItemIcon >
454- < ListItemText > { t ( 'Snap Left' ) } </ ListItemText >
455- </ MenuItem >
456- < MenuItem
457- selected = { location === 'split-right' }
458- aria-label = { t ( 'Snap Right' ) }
459- title = { t ( 'Snap Right' ) }
460- onClick = { ( ) => {
461- Activity . update ( id , { location : 'split-right' } ) ;
462- setSnapMenuAnchor ( null ) ;
463- } }
464- >
465- < ListItemIcon >
466- < Icon icon = "mdi:dock-right" />
467- </ ListItemIcon >
468- < ListItemText > { t ( 'Snap Right' ) } </ ListItemText >
469- </ MenuItem >
470- < MenuItem
471- selected = { location === 'split-top' }
472- aria-label = { t ( 'Snap Top' ) }
473- title = { t ( 'Snap Top' ) }
474- onClick = { ( ) => {
475- Activity . update ( id , { location : 'split-top' } ) ;
476- setSnapMenuAnchor ( null ) ;
477- } }
478- >
479- < ListItemIcon >
480- < Icon icon = "mdi:dock-top" />
481- </ ListItemIcon >
482- < ListItemText > { t ( 'Snap Top' ) } </ ListItemText >
483- </ MenuItem >
484- < MenuItem
485- selected = { location === 'split-bottom' }
486- aria-label = { t ( 'Snap Bottom' ) }
487- title = { t ( 'Snap Bottom' ) }
488- onClick = { ( ) => {
489- Activity . update ( id , { location : 'split-bottom' } ) ;
490- setSnapMenuAnchor ( null ) ;
511+ < ClickAwayListener
512+ onClickAway = { ( ) => {
513+ if ( snapMenuOpenReason === 'click' ) {
514+ setSnapMenuAnchor ( null ) ;
515+ setSnapMenuOpenReason ( null ) ;
516+ }
491517 } }
492518 >
493- < ListItemIcon >
494- < Icon icon = "mdi:dock-bottom" />
495- </ ListItemIcon >
496- < ListItemText > { t ( 'Snap Bottom' ) } </ ListItemText >
497- </ MenuItem >
498- </ Menu >
519+ < Paper
520+ elevation = { 8 }
521+ onMouseEnter = { ( ) => {
522+ // Cancel close timer when entering menu (for hover-open case)
523+ if ( closeTimerRef . current && snapMenuOpenReason === 'hover' ) {
524+ clearTimeout ( closeTimerRef . current ) ;
525+ closeTimerRef . current = null ;
526+ }
527+ } }
528+ onMouseLeave = { ( ) => {
529+ // Start close timer when leaving menu (for hover-open case)
530+ if ( snapMenuOpenReason === 'hover' ) {
531+ if ( closeTimerRef . current ) {
532+ clearTimeout ( closeTimerRef . current ) ;
533+ }
534+ closeTimerRef . current = setTimeout ( ( ) => {
535+ setSnapMenuAnchor ( null ) ;
536+ setSnapMenuOpenReason ( null ) ;
537+ closeTimerRef . current = null ;
538+ } , 150 ) ;
539+ }
540+ } }
541+ onKeyDown = { event => {
542+ if ( event . key === 'Escape' ) {
543+ setSnapMenuAnchor ( null ) ;
544+ setSnapMenuOpenReason ( null ) ;
545+ snapMenuAnchor ?. focus ( ) ;
546+ }
547+ } }
548+ >
549+ < MenuList
550+ ref = { menuListRef }
551+ aria-label = { t ( 'Window' ) }
552+ autoFocusItem = { isSnapMenuOpen }
553+ >
554+ < MenuItem
555+ selected = { location === 'full' }
556+ aria-label = { t ( 'Fullscreen' ) }
557+ title = { t ( 'Fullscreen' ) }
558+ onClick = { ( ) => {
559+ Activity . update ( id , { location : 'full' } ) ;
560+ setSnapMenuAnchor ( null ) ;
561+ setSnapMenuOpenReason ( null ) ;
562+ } }
563+ >
564+ < ListItemIcon >
565+ < Icon icon = "mdi:fullscreen" />
566+ </ ListItemIcon >
567+ < ListItemText > { t ( 'Fullscreen' ) } </ ListItemText >
568+ </ MenuItem >
569+ < MenuItem
570+ selected = { location === 'split-left' }
571+ aria-label = { t ( 'Snap Left' ) }
572+ title = { t ( 'Snap Left' ) }
573+ onClick = { ( ) => {
574+ Activity . update ( id , { location : 'split-left' } ) ;
575+ setSnapMenuAnchor ( null ) ;
576+ setSnapMenuOpenReason ( null ) ;
577+ } }
578+ >
579+ < ListItemIcon >
580+ < Icon icon = "mdi:dock-left" />
581+ </ ListItemIcon >
582+ < ListItemText > { t ( 'Snap Left' ) } </ ListItemText >
583+ </ MenuItem >
584+ < MenuItem
585+ selected = { location === 'split-right' }
586+ aria-label = { t ( 'Snap Right' ) }
587+ title = { t ( 'Snap Right' ) }
588+ onClick = { ( ) => {
589+ Activity . update ( id , { location : 'split-right' } ) ;
590+ setSnapMenuAnchor ( null ) ;
591+ setSnapMenuOpenReason ( null ) ;
592+ } }
593+ >
594+ < ListItemIcon >
595+ < Icon icon = "mdi:dock-right" />
596+ </ ListItemIcon >
597+ < ListItemText > { t ( 'Snap Right' ) } </ ListItemText >
598+ </ MenuItem >
599+ < MenuItem
600+ selected = { location === 'split-top' }
601+ aria-label = { t ( 'Snap Top' ) }
602+ title = { t ( 'Snap Top' ) }
603+ onClick = { ( ) => {
604+ Activity . update ( id , { location : 'split-top' } ) ;
605+ setSnapMenuAnchor ( null ) ;
606+ setSnapMenuOpenReason ( null ) ;
607+ } }
608+ >
609+ < ListItemIcon >
610+ < Icon icon = "mdi:dock-top" />
611+ </ ListItemIcon >
612+ < ListItemText > { t ( 'Snap Top' ) } </ ListItemText >
613+ </ MenuItem >
614+ < MenuItem
615+ selected = { location === 'split-bottom' }
616+ aria-label = { t ( 'Snap Bottom' ) }
617+ title = { t ( 'Snap Bottom' ) }
618+ onClick = { ( ) => {
619+ Activity . update ( id , { location : 'split-bottom' } ) ;
620+ setSnapMenuAnchor ( null ) ;
621+ setSnapMenuOpenReason ( null ) ;
622+ } }
623+ >
624+ < ListItemIcon >
625+ < Icon icon = "mdi:dock-bottom" />
626+ </ ListItemIcon >
627+ < ListItemText > { t ( 'Snap Bottom' ) } </ ListItemText >
628+ </ MenuItem >
629+ </ MenuList >
630+ </ Paper >
631+ </ ClickAwayListener >
632+ </ Popper >
499633 < IconButton
500634 onClick = { ( ) => {
501635 Activity . update ( id , { minimized : true } ) ;
0 commit comments