1
- import { forwardRef as reactForwardRef , memo , useMemo } from 'react' ;
1
+ import { forwardRef as reactForwardRef , memo , useMemo , useRef , useState } from 'react' ;
2
+ import { createPortal } from 'react-dom' ;
3
+ import { usePopper } from 'react-popper' ;
2
4
import isPropValid from '@emotion/is-prop-valid' ;
3
- import type { Theme } from '@emotion/react' ;
5
+ import { type Theme , useTheme } from '@emotion/react' ;
4
6
import styled from '@emotion/styled' ;
5
7
6
8
import InteractionStateLayer from 'sentry/components/interactionStateLayer' ;
9
+ import { Overlay , PositionWrapper } from 'sentry/components/overlay' ;
7
10
import type { TooltipProps } from 'sentry/components/tooltip' ;
8
11
import { Tooltip } from 'sentry/components/tooltip' ;
9
12
import { space } from 'sentry/styles/space' ;
10
13
import domId from 'sentry/utils/domId' ;
14
+ import mergeRefs from 'sentry/utils/mergeRefs' ;
11
15
import type { FormSize } from 'sentry/utils/theme' ;
12
16
13
17
/**
@@ -56,6 +60,10 @@ export type MenuListItemProps = {
56
60
* Accented text and background (on hover) colors.
57
61
*/
58
62
priority ?: Priority ;
63
+ /**
64
+ * Whether to show the details in an overlay when the item is hovered / focused.
65
+ */
66
+ showDetailsInOverlay ?: boolean ;
59
67
/**
60
68
* Determines the item's font sizes and internal paddings.
61
69
*/
@@ -115,11 +123,13 @@ function BaseMenuListItem({
115
123
innerWrapProps = { } ,
116
124
labelProps = { } ,
117
125
detailsProps = { } ,
126
+ showDetailsInOverlay = false ,
118
127
tooltip,
119
128
tooltipOptions = { delay : 500 } ,
120
129
forwardRef,
121
130
...props
122
131
} : Props ) {
132
+ const itemRef = useRef < HTMLLIElement > ( null ) ;
123
133
const labelId = useMemo ( ( ) => domId ( 'menuitem-label-' ) , [ ] ) ;
124
134
const detailId = useMemo ( ( ) => domId ( 'menuitem-details-' ) , [ ] ) ;
125
135
@@ -130,7 +140,7 @@ function BaseMenuListItem({
130
140
aria-labelledby = { labelId }
131
141
aria-describedby = { detailId }
132
142
as = { as }
133
- ref = { forwardRef }
143
+ ref = { mergeRefs ( [ forwardRef , itemRef ] ) }
134
144
{ ...props }
135
145
>
136
146
< Tooltip skipWrapper title = { tooltip } { ...tooltipOptions } >
@@ -167,7 +177,7 @@ function BaseMenuListItem({
167
177
>
168
178
{ label }
169
179
</ Label >
170
- { details && (
180
+ { ! showDetailsInOverlay && details && (
171
181
< Details
172
182
id = { detailId }
173
183
disabled = { disabled }
@@ -177,6 +187,11 @@ function BaseMenuListItem({
177
187
{ details }
178
188
</ Details >
179
189
) }
190
+ { showDetailsInOverlay && details && isFocused && (
191
+ < DetailsOverlay size = { size } id = { detailId } itemRef = { itemRef } >
192
+ { details }
193
+ </ DetailsOverlay >
194
+ ) }
180
195
</ LabelWrap >
181
196
{ trailingItems && (
182
197
< TrailingItems
@@ -203,6 +218,62 @@ const MenuListItem = memo(
203
218
204
219
export default MenuListItem ;
205
220
221
+ function DetailsOverlay ( {
222
+ children,
223
+ size,
224
+ id,
225
+ itemRef,
226
+ } : {
227
+ children : React . ReactNode ;
228
+ id : string ;
229
+ itemRef : React . RefObject < HTMLLIElement > ;
230
+ size : Props [ 'size' ] ;
231
+ } ) {
232
+ const theme = useTheme ( ) ;
233
+ const [ overlayElement , setOverlayElement ] = useState < HTMLDivElement | null > ( null ) ;
234
+ const popper = usePopper ( itemRef . current , overlayElement , {
235
+ placement : 'right-start' ,
236
+ modifiers : [
237
+ {
238
+ name : 'offset' ,
239
+ options : {
240
+ offset : [ 0 , 8 ] ,
241
+ } ,
242
+ } ,
243
+ ] ,
244
+ } ) ;
245
+
246
+ return createPortal (
247
+ < StyledPositionWrapper
248
+ { ...popper . attributes . popper }
249
+ ref = { setOverlayElement }
250
+ zIndex = { theme . zIndex . tooltip }
251
+ style = { popper . styles . popper }
252
+ >
253
+ < StyledOverlay id = { id } placement = "right-start" size = { size } >
254
+ { children }
255
+ </ StyledOverlay >
256
+ </ StyledPositionWrapper > ,
257
+ document . body
258
+ ) ;
259
+ }
260
+
261
+ const StyledPositionWrapper = styled ( PositionWrapper ) `
262
+ &[data-popper-reference-hidden='true'] {
263
+ opacity: 0;
264
+ pointer-events: none;
265
+ }
266
+ ` ;
267
+
268
+ const StyledOverlay = styled ( Overlay ) < {
269
+ size : Props [ 'size' ] ;
270
+ } > `
271
+ padding: ${ p => getVerticalPadding ( p . size ) } ;
272
+ font-size: ${ p => p . theme . form [ p . size ?? 'md' ] . fontSize } ;
273
+ cursor: auto;
274
+ user-select: text;
275
+ ` ;
276
+
206
277
const MenuItemWrap = styled ( 'li' ) `
207
278
position: static;
208
279
list-style-type: none;
0 commit comments