1
- import { useState } from 'react' ;
1
+ import { useCallback , useContext , useEffect , useRef } from 'react' ;
2
+ import { useHover } from '@react-aria/interactions' ;
3
+ import { type OverlayTriggerState } from '@react-stately/overlays' ;
2
4
3
- import DropdownAutoCompleteMenu from 'sentry/components/dropdownAutoComplete/menu' ;
4
- import type { Item } from 'sentry/components/dropdownAutoComplete/types' ;
5
+ import {
6
+ CompactSelect ,
7
+ type SingleSelectProps ,
8
+ } from 'sentry/components/core/compactSelect' ;
9
+ import { SelectContext } from 'sentry/components/core/compactSelect/control' ;
5
10
6
11
import Crumb from './crumb' ;
7
12
import Divider from './divider' ;
8
13
import type { RouteWithName } from './types' ;
9
14
10
- interface AdditionalDropdownProps
11
- extends Pick <
12
- React . ComponentProps < typeof DropdownAutoCompleteMenu > ,
13
- 'onChange' | 'busyItemsStillVisible'
14
- > { }
15
-
16
- interface BreadcrumbDropdownProps extends AdditionalDropdownProps {
17
- items : Item [ ] ;
15
+ interface BreadcrumbDropdownProps extends SingleSelectProps < string > {
18
16
name : React . ReactNode ;
19
- onSelect : ( item : Item ) => void ;
17
+ onCrumbSelect : ( value : string ) => void ;
20
18
route : RouteWithName ;
21
19
hasMenu ?: boolean ;
22
20
isLast ?: boolean ;
@@ -27,39 +25,104 @@ function BreadcrumbDropdown({
27
25
route,
28
26
isLast,
29
27
name,
30
- onSelect,
31
- ...dropdownProps
28
+ onCrumbSelect,
29
+ options,
30
+ value,
31
+ ...props
32
32
} : BreadcrumbDropdownProps ) {
33
- const [ isActive , setIsActive ] = useState ( false ) ;
33
+ const {
34
+ hoverProps : { onPointerEnter, onPointerLeave} ,
35
+ isHovered,
36
+ } = useHover ( { } ) ;
37
+
38
+ if ( ! hasMenu ) {
39
+ return (
40
+ < Crumb >
41
+ < span > { name || route . name } </ span >
42
+ { isLast ? null : < Divider /> }
43
+ </ Crumb >
44
+ ) ;
45
+ }
34
46
35
47
return (
36
- < DropdownAutoCompleteMenu
37
- blendCorner = { false }
38
- isOpen = { isActive }
39
- virtualizedHeight = { 41 }
40
- onSelect = { item => {
41
- setIsActive ( false ) ;
42
- onSelect ( item ) ;
43
- } }
44
- menuProps = { {
45
- onMouseEnter : ( ) => setIsActive ( true ) ,
46
- onMouseLeave : ( ) => setIsActive ( false ) ,
48
+ < CompactSelect < string >
49
+ searchable
50
+ options = { options . map ( item => ( { ...item , hideCheck : true } ) ) }
51
+ onChange = { selected => {
52
+ onCrumbSelect ( selected . value ) ;
47
53
} }
48
- { ...dropdownProps }
49
- >
50
- { ( { getActorProps, isOpen} ) => (
51
- < Crumb
52
- { ...getActorProps ( {
53
- onClick : ( ) => setIsActive ( false ) ,
54
- onMouseEnter : ( ) => setIsActive ( true ) ,
55
- onMouseLeave : ( ) => setIsActive ( false ) ,
56
- } ) }
57
- >
58
- < span > { name || route . name } </ span >
59
- { isLast ? null : < Divider isHover = { hasMenu && isOpen } /> }
60
- </ Crumb >
54
+ closeOnSelect
55
+ onPointerEnter = { onPointerEnter }
56
+ onPointerLeave = { onPointerLeave }
57
+ value = { value }
58
+ trigger = { triggerProps => (
59
+ < MenuCrumb
60
+ crumbLabel = { name || route . name }
61
+ menuHasHover = { isHovered }
62
+ { ...triggerProps }
63
+ />
61
64
) }
62
- </ DropdownAutoCompleteMenu >
65
+ { ...props }
66
+ />
67
+ ) ;
68
+ }
69
+
70
+ interface MenuCrumbProps extends React . ComponentProps < typeof Crumb > {
71
+ crumbLabel : React . ReactNode ;
72
+ menuHasHover : boolean ;
73
+ isLast ?: boolean ;
74
+ }
75
+
76
+ // XXX(epurkhiser): We have a couple hacks in place to get hover-activation of
77
+ // our CompactSelect working well for these breadcrumbs.
78
+ //
79
+ // 1. We're using the SelectContext to retrieve the OverlayTriggerState object
80
+ // for the CompactSelect that will be rendered upon hover. We need this so
81
+ // we can activate the menu. Using the `isOpen` controlled prop on
82
+ // CompactSelect does not work since it will not actually focus the menu.
83
+ //
84
+ // 2. We track the active crumb OverlayTriggerState objects so that when
85
+ // activating a second crumb the first one can be immediately closed,
86
+ // instead of being closed after the PointerLeave timemout.
87
+ const activeCrumbStates = new Set < OverlayTriggerState | undefined > ( ) ;
88
+
89
+ const CLOSE_MENU_TIMEOUT = 250 ;
90
+
91
+ function MenuCrumb ( { crumbLabel, menuHasHover, isLast, ...props } : MenuCrumbProps ) {
92
+ const { overlayState, overlayIsOpen} = useContext ( SelectContext ) ;
93
+ const { open, close} = overlayState ?? { } ;
94
+
95
+ const closeTimeoutRef = useRef < number > ( undefined ) ;
96
+
97
+ useEffect ( ( ) => {
98
+ activeCrumbStates . add ( overlayState ) ;
99
+ return ( ) => void activeCrumbStates . delete ( overlayState ) ;
100
+ } , [ overlayState ] ) ;
101
+
102
+ const queueMenuClose = useCallback ( ( ) => {
103
+ window . clearTimeout ( closeTimeoutRef . current ) ;
104
+ closeTimeoutRef . current = window . setTimeout ( ( ) => close ?.( ) , CLOSE_MENU_TIMEOUT ) ;
105
+ } , [ close ] ) ;
106
+
107
+ const handleOpen = useCallback ( ( ) => {
108
+ activeCrumbStates . forEach ( state => state ?. close ( ) ) ;
109
+ window . clearTimeout ( closeTimeoutRef . current ) ;
110
+ open ?.( ) ;
111
+ } , [ open ] ) ;
112
+
113
+ useEffect ( ( ) => {
114
+ if ( menuHasHover ) {
115
+ window . clearTimeout ( closeTimeoutRef . current ) ;
116
+ } else {
117
+ queueMenuClose ( ) ;
118
+ }
119
+ } , [ menuHasHover , queueMenuClose ] ) ;
120
+
121
+ return (
122
+ < Crumb { ...props } onPointerEnter = { handleOpen } onPointerLeave = { queueMenuClose } >
123
+ < span > { crumbLabel } </ span >
124
+ { isLast ? null : < Divider isHover = { overlayIsOpen } /> }
125
+ </ Crumb >
63
126
) ;
64
127
}
65
128
0 commit comments