@@ -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 : {
@@ -408,93 +427,208 @@ export function SingleActivityRenderer({
408427 < IconButton
409428 size = "small"
410429 title = { t ( 'Window' ) }
411- onMouseEnter = { event => setSnapMenuAnchor ( event . currentTarget ) }
412- onClick = { event => setSnapMenuAnchor ( event . currentTarget ) }
430+ aria-haspopup = "menu"
431+ aria-expanded = { isSnapMenuOpen }
432+ aria-controls = { isSnapMenuOpen ? 'snap-menu' : undefined }
433+ onMouseEnter = { event => {
434+ // Clear any existing close timer
435+ if ( closeTimerRef . current ) {
436+ clearTimeout ( closeTimerRef . current ) ;
437+ closeTimerRef . current = null ;
438+ }
439+ // Set 350ms timer to open on hover
440+ if ( openTimerRef . current ) {
441+ clearTimeout ( openTimerRef . current ) ;
442+ }
443+ const target = event . currentTarget ;
444+ openTimerRef . current = setTimeout ( ( ) => {
445+ setSnapMenuAnchor ( target ) ;
446+ setSnapMenuOpenReason ( 'hover' ) ;
447+ openTimerRef . current = null ;
448+ } , 350 ) ;
449+ } }
450+ onMouseLeave = { ( ) => {
451+ // Cancel open timer if pointer leaves before 350ms
452+ if ( openTimerRef . current ) {
453+ clearTimeout ( openTimerRef . current ) ;
454+ openTimerRef . current = null ;
455+ }
456+ // If menu is open due to hover, start close timer
457+ if ( snapMenuOpenReason === 'hover' && isSnapMenuOpen ) {
458+ if ( closeTimerRef . current ) {
459+ clearTimeout ( closeTimerRef . current ) ;
460+ }
461+ closeTimerRef . current = setTimeout ( ( ) => {
462+ setSnapMenuAnchor ( null ) ;
463+ setSnapMenuOpenReason ( null ) ;
464+ closeTimerRef . current = null ;
465+ } , 150 ) ;
466+ }
467+ } }
468+ onClick = { event => {
469+ // Clear any timers
470+ if ( openTimerRef . current ) {
471+ clearTimeout ( openTimerRef . current ) ;
472+ openTimerRef . current = null ;
473+ }
474+ if ( closeTimerRef . current ) {
475+ clearTimeout ( closeTimerRef . current ) ;
476+ closeTimerRef . current = null ;
477+ }
478+ // Toggle menu on click
479+ if ( isSnapMenuOpen && snapMenuOpenReason === 'click' ) {
480+ setSnapMenuAnchor ( null ) ;
481+ setSnapMenuOpenReason ( null ) ;
482+ } else {
483+ setSnapMenuAnchor ( event . currentTarget ) ;
484+ setSnapMenuOpenReason ( 'click' ) ;
485+ }
486+ } }
487+ onKeyDown = { event => {
488+ if ( event . key === 'Enter' || event . key === ' ' || event . key === 'ArrowDown' ) {
489+ event . preventDefault ( ) ;
490+ if ( openTimerRef . current ) {
491+ clearTimeout ( openTimerRef . current ) ;
492+ openTimerRef . current = null ;
493+ }
494+ if ( ! isSnapMenuOpen ) {
495+ setSnapMenuAnchor ( event . currentTarget as HTMLElement ) ;
496+ setSnapMenuOpenReason ( 'click' ) ;
497+ }
498+ }
499+ } }
413500 >
414501 < Icon icon = "mdi:dock-window" />
415502 </ IconButton >
416- < Menu
417- anchorEl = { snapMenuAnchor }
503+ < Popper
504+ id = "snap-menu"
418505 open = { isSnapMenuOpen }
419- onClose = { ( ) => setSnapMenuAnchor ( null ) }
420- anchorOrigin = { { vertical : 'bottom' , horizontal : 'center' } }
421- transformOrigin = { { vertical : 'top' , horizontal : 'center' } }
422- MenuListProps = { {
423- onMouseLeave : ( ) => setSnapMenuAnchor ( null ) ,
424- 'aria-label' : t ( 'Window' ) ,
425- } }
506+ anchorEl = { snapMenuAnchor }
507+ placement = "bottom-end"
508+ sx = { { zIndex : theme . zIndex . modal } }
426509 >
427- < MenuItem
428- selected = { location === 'full' }
429- aria-label = { t ( 'Fullscreen' ) }
430- title = { t ( 'Fullscreen' ) }
431- onClick = { ( ) => {
432- Activity . update ( id , { location : 'full' } ) ;
433- setSnapMenuAnchor ( null ) ;
434- } }
435- >
436- < ListItemIcon >
437- < Icon icon = "mdi:fullscreen" />
438- </ ListItemIcon >
439- < ListItemText > { t ( 'Fullscreen' ) } </ ListItemText >
440- </ MenuItem >
441- < MenuItem
442- selected = { location === 'split-left' }
443- aria-label = { t ( 'Snap Left' ) }
444- title = { t ( 'Snap Left' ) }
445- onClick = { ( ) => {
446- Activity . update ( id , { location : 'split-left' } ) ;
447- setSnapMenuAnchor ( null ) ;
448- } }
449- >
450- < ListItemIcon >
451- < Icon icon = "mdi:dock-left" />
452- </ ListItemIcon >
453- < ListItemText > { t ( 'Snap Left' ) } </ ListItemText >
454- </ MenuItem >
455- < MenuItem
456- selected = { location === 'split-right' }
457- aria-label = { t ( 'Snap Right' ) }
458- title = { t ( 'Snap Right' ) }
459- onClick = { ( ) => {
460- Activity . update ( id , { location : 'split-right' } ) ;
461- setSnapMenuAnchor ( null ) ;
462- } }
463- >
464- < ListItemIcon >
465- < Icon icon = "mdi:dock-right" />
466- </ ListItemIcon >
467- < ListItemText > { t ( 'Snap Right' ) } </ ListItemText >
468- </ MenuItem >
469- < MenuItem
470- selected = { location === 'split-top' }
471- aria-label = { t ( 'Snap Top' ) }
472- title = { t ( 'Snap Top' ) }
473- onClick = { ( ) => {
474- Activity . update ( id , { location : 'split-top' } ) ;
475- setSnapMenuAnchor ( null ) ;
476- } }
477- >
478- < ListItemIcon >
479- < Icon icon = "mdi:dock-top" />
480- </ ListItemIcon >
481- < ListItemText > { t ( 'Snap Top' ) } </ ListItemText >
482- </ MenuItem >
483- < MenuItem
484- selected = { location === 'split-bottom' }
485- aria-label = { t ( 'Snap Bottom' ) }
486- title = { t ( 'Snap Bottom' ) }
487- onClick = { ( ) => {
488- Activity . update ( id , { location : 'split-bottom' } ) ;
489- setSnapMenuAnchor ( null ) ;
510+ < ClickAwayListener
511+ onClickAway = { ( ) => {
512+ if ( snapMenuOpenReason === 'click' ) {
513+ setSnapMenuAnchor ( null ) ;
514+ setSnapMenuOpenReason ( null ) ;
515+ }
490516 } }
491517 >
492- < ListItemIcon >
493- < Icon icon = "mdi:dock-bottom" />
494- </ ListItemIcon >
495- < ListItemText > { t ( 'Snap Bottom' ) } </ ListItemText >
496- </ MenuItem >
497- </ Menu >
518+ < Paper
519+ elevation = { 8 }
520+ onMouseEnter = { ( ) => {
521+ // Cancel close timer when entering menu (for hover-open case)
522+ if ( closeTimerRef . current && snapMenuOpenReason === 'hover' ) {
523+ clearTimeout ( closeTimerRef . current ) ;
524+ closeTimerRef . current = null ;
525+ }
526+ } }
527+ onMouseLeave = { ( ) => {
528+ // Start close timer when leaving menu (for hover-open case)
529+ if ( snapMenuOpenReason === 'hover' ) {
530+ if ( closeTimerRef . current ) {
531+ clearTimeout ( closeTimerRef . current ) ;
532+ }
533+ closeTimerRef . current = setTimeout ( ( ) => {
534+ setSnapMenuAnchor ( null ) ;
535+ setSnapMenuOpenReason ( null ) ;
536+ closeTimerRef . current = null ;
537+ } , 150 ) ;
538+ }
539+ } }
540+ onKeyDown = { event => {
541+ if ( event . key === 'Escape' ) {
542+ setSnapMenuAnchor ( null ) ;
543+ setSnapMenuOpenReason ( null ) ;
544+ snapMenuAnchor ?. focus ( ) ;
545+ }
546+ } }
547+ >
548+ < MenuList
549+ ref = { menuListRef }
550+ aria-label = { t ( 'Window' ) }
551+ autoFocusItem = { isSnapMenuOpen }
552+ >
553+ < MenuItem
554+ selected = { location === 'full' }
555+ aria-label = { t ( 'Fullscreen' ) }
556+ title = { t ( 'Fullscreen' ) }
557+ onClick = { ( ) => {
558+ Activity . update ( id , { location : 'full' } ) ;
559+ setSnapMenuAnchor ( null ) ;
560+ setSnapMenuOpenReason ( null ) ;
561+ } }
562+ >
563+ < ListItemIcon >
564+ < Icon icon = "mdi:fullscreen" />
565+ </ ListItemIcon >
566+ < ListItemText > { t ( 'Fullscreen' ) } </ ListItemText >
567+ </ MenuItem >
568+ < MenuItem
569+ selected = { location === 'split-left' }
570+ aria-label = { t ( 'Snap Left' ) }
571+ title = { t ( 'Snap Left' ) }
572+ onClick = { ( ) => {
573+ Activity . update ( id , { location : 'split-left' } ) ;
574+ setSnapMenuAnchor ( null ) ;
575+ setSnapMenuOpenReason ( null ) ;
576+ } }
577+ >
578+ < ListItemIcon >
579+ < Icon icon = "mdi:dock-left" />
580+ </ ListItemIcon >
581+ < ListItemText > { t ( 'Snap Left' ) } </ ListItemText >
582+ </ MenuItem >
583+ < MenuItem
584+ selected = { location === 'split-right' }
585+ aria-label = { t ( 'Snap Right' ) }
586+ title = { t ( 'Snap Right' ) }
587+ onClick = { ( ) => {
588+ Activity . update ( id , { location : 'split-right' } ) ;
589+ setSnapMenuAnchor ( null ) ;
590+ setSnapMenuOpenReason ( null ) ;
591+ } }
592+ >
593+ < ListItemIcon >
594+ < Icon icon = "mdi:dock-right" />
595+ </ ListItemIcon >
596+ < ListItemText > { t ( 'Snap Right' ) } </ ListItemText >
597+ </ MenuItem >
598+ < MenuItem
599+ selected = { location === 'split-top' }
600+ aria-label = { t ( 'Snap Top' ) }
601+ title = { t ( 'Snap Top' ) }
602+ onClick = { ( ) => {
603+ Activity . update ( id , { location : 'split-top' } ) ;
604+ setSnapMenuAnchor ( null ) ;
605+ setSnapMenuOpenReason ( null ) ;
606+ } }
607+ >
608+ < ListItemIcon >
609+ < Icon icon = "mdi:dock-top" />
610+ </ ListItemIcon >
611+ < ListItemText > { t ( 'Snap Top' ) } </ ListItemText >
612+ </ MenuItem >
613+ < MenuItem
614+ selected = { location === 'split-bottom' }
615+ aria-label = { t ( 'Snap Bottom' ) }
616+ title = { t ( 'Snap Bottom' ) }
617+ onClick = { ( ) => {
618+ Activity . update ( id , { location : 'split-bottom' } ) ;
619+ setSnapMenuAnchor ( null ) ;
620+ setSnapMenuOpenReason ( null ) ;
621+ } }
622+ >
623+ < ListItemIcon >
624+ < Icon icon = "mdi:dock-bottom" />
625+ </ ListItemIcon >
626+ < ListItemText > { t ( 'Snap Bottom' ) } </ ListItemText >
627+ </ MenuItem >
628+ </ MenuList >
629+ </ Paper >
630+ </ ClickAwayListener >
631+ </ Popper >
498632 < IconButton
499633 onClick = { ( ) => {
500634 Activity . update ( id , { minimized : true } ) ;
0 commit comments