@sanity/ui 0.36.16 → 0.37.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/sanity-ui.js +242 -209
- package/lib/sanity-ui.js.map +1 -1
- package/lib/sanity-ui.modern.js +297 -281
- package/lib/sanity-ui.modern.js.map +1 -1
- package/lib/sanity-ui.module.js +242 -209
- package/lib/sanity-ui.module.js.map +1 -1
- package/lib/src/components/autocomplete/autocomplete.d.ts +2 -0
- package/lib/src/components/autocomplete/autocomplete.d.ts.map +1 -1
- package/lib/src/components/menu/menu.d.ts +1 -1
- package/lib/src/components/menu/menu.d.ts.map +1 -1
- package/lib/src/components/menu/menuContext.d.ts +9 -1
- package/lib/src/components/menu/menuContext.d.ts.map +1 -1
- package/lib/src/components/menu/menuGroup.d.ts +2 -0
- package/lib/src/components/menu/menuGroup.d.ts.map +1 -1
- package/lib/src/components/menu/menuItem.d.ts.map +1 -1
- package/lib/src/components/menu/useMenuController.d.ts +25 -0
- package/lib/src/components/menu/useMenuController.d.ts.map +1 -0
- package/package.json +6 -6
- package/src/components/autocomplete/__workshop__/fullscreen.tsx +24 -5
- package/src/components/autocomplete/autocomplete.tsx +9 -1
- package/src/components/menu/__workshop__/index.ts +5 -0
- package/src/components/menu/__workshop__/menuGroupRight.tsx +59 -0
- package/src/components/menu/menu.test.tsx +39 -20
- package/src/components/menu/menu.tsx +30 -210
- package/src/components/menu/menuContext.ts +11 -1
- package/src/components/menu/menuGroup.tsx +56 -78
- package/src/components/menu/menuItem.tsx +12 -5
- package/src/components/menu/useMenuController.ts +239 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import React, {forwardRef, useCallback, useEffect, useMemo
|
|
1
|
+
import React, {forwardRef, useCallback, useEffect, useMemo} from 'react'
|
|
2
2
|
import styled from 'styled-components'
|
|
3
|
-
import {useClickOutside, useGlobalKeyDown} from '../../hooks'
|
|
3
|
+
import {useClickOutside, useForwardedRef, useGlobalKeyDown} from '../../hooks'
|
|
4
4
|
import {Box, Stack} from '../../primitives'
|
|
5
5
|
import {ResponsivePaddingProps} from '../../primitives/types'
|
|
6
6
|
import {useLayer} from '../../utils'
|
|
7
|
-
import {_getFocusableElements, _sortElements} from './helpers'
|
|
8
7
|
import {MenuContext, MenuContextValue} from './menuContext'
|
|
8
|
+
import {useMenuController} from './useMenuController'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* @public
|
|
@@ -39,7 +39,7 @@ const Root = styled(Box)`
|
|
|
39
39
|
*/
|
|
40
40
|
export const Menu = forwardRef(function Menu(
|
|
41
41
|
props: MenuProps & Omit<React.HTMLProps<HTMLDivElement>, 'as' | 'height' | 'role' | 'tabIndex'>,
|
|
42
|
-
ref
|
|
42
|
+
ref: React.ForwardedRef<HTMLDivElement>
|
|
43
43
|
) {
|
|
44
44
|
const {
|
|
45
45
|
children,
|
|
@@ -60,221 +60,35 @@ export const Menu = forwardRef(function Menu(
|
|
|
60
60
|
...restProps
|
|
61
61
|
} = props
|
|
62
62
|
|
|
63
|
+
const forwardedRef = useForwardedRef(ref)
|
|
64
|
+
|
|
63
65
|
const {isTopLayer} = useLayer()
|
|
64
|
-
const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
|
|
65
|
-
const elementsRef = useRef<HTMLElement[]>([])
|
|
66
|
-
const [activeIndex, setActiveIndex] = useState(-1)
|
|
67
|
-
const activeIndexRef = useRef(activeIndex)
|
|
68
|
-
const activeElement = elementsRef.current[activeIndex] || null
|
|
69
|
-
const activeElementRef = useRef<HTMLElement | null>(activeElement)
|
|
70
|
-
const mounted = Boolean(rootElement)
|
|
71
66
|
|
|
72
|
-
const
|
|
67
|
+
const {
|
|
68
|
+
activeElement,
|
|
69
|
+
activeIndex,
|
|
70
|
+
handleItemMouseEnter,
|
|
71
|
+
handleItemMouseLeave,
|
|
72
|
+
handleKeyDown,
|
|
73
|
+
mount,
|
|
74
|
+
rootElement,
|
|
75
|
+
setRootElement,
|
|
76
|
+
} = useMenuController({onKeyDown, originElement, shouldFocus})
|
|
77
|
+
|
|
78
|
+
const handleRefChange = useCallback(
|
|
73
79
|
(el: HTMLDivElement | null) => {
|
|
74
80
|
setRootElement(el)
|
|
75
|
-
|
|
76
|
-
else if (ref) ref.current = el
|
|
81
|
+
forwardedRef.current = el
|
|
77
82
|
},
|
|
78
|
-
[
|
|
83
|
+
[forwardedRef, setRootElement]
|
|
79
84
|
)
|
|
80
85
|
|
|
81
|
-
const mount = useCallback(
|
|
82
|
-
(element: HTMLElement | null, selected?: boolean): (() => void) => {
|
|
83
|
-
if (!element) return () => undefined
|
|
84
|
-
|
|
85
|
-
if (elementsRef.current.indexOf(element) === -1) {
|
|
86
|
-
elementsRef.current.push(element)
|
|
87
|
-
_sortElements(rootElement, elementsRef.current)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (selected) {
|
|
91
|
-
const selectedIndex = elementsRef.current.indexOf(element)
|
|
92
|
-
|
|
93
|
-
setActiveIndex(selectedIndex)
|
|
94
|
-
activeIndexRef.current = selectedIndex
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return () => {
|
|
98
|
-
const idx = elementsRef.current.indexOf(element)
|
|
99
|
-
|
|
100
|
-
if (idx > -1) {
|
|
101
|
-
elementsRef.current.splice(idx, 1)
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
[rootElement]
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
const handleKeyDown = useCallback(
|
|
109
|
-
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
110
|
-
// Move focus to the element that opened the menu before handling the `Tab` press
|
|
111
|
-
if (event.key === 'Tab') {
|
|
112
|
-
if (originElement) {
|
|
113
|
-
originElement.focus()
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Move focus to the first focusable menuitem
|
|
120
|
-
if (event.key === 'Home') {
|
|
121
|
-
event.preventDefault()
|
|
122
|
-
event.stopPropagation()
|
|
123
|
-
|
|
124
|
-
const focusableElements = _getFocusableElements(elementsRef.current)
|
|
125
|
-
const el = focusableElements[0]
|
|
126
|
-
|
|
127
|
-
if (!el) return
|
|
128
|
-
|
|
129
|
-
const currentIndex = elementsRef.current.indexOf(el)
|
|
130
|
-
|
|
131
|
-
setActiveIndex(currentIndex)
|
|
132
|
-
activeIndexRef.current = currentIndex
|
|
133
|
-
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Move focus to the last focusable menuitem
|
|
138
|
-
if (event.key === 'End') {
|
|
139
|
-
event.preventDefault()
|
|
140
|
-
event.stopPropagation()
|
|
141
|
-
|
|
142
|
-
const focusableElements = _getFocusableElements(elementsRef.current)
|
|
143
|
-
const el = focusableElements[focusableElements.length - 1]
|
|
144
|
-
|
|
145
|
-
if (!el) return
|
|
146
|
-
|
|
147
|
-
const currentIndex = elementsRef.current.indexOf(el)
|
|
148
|
-
|
|
149
|
-
setActiveIndex(currentIndex)
|
|
150
|
-
activeIndexRef.current = currentIndex
|
|
151
|
-
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (event.key === 'ArrowUp') {
|
|
156
|
-
event.preventDefault()
|
|
157
|
-
event.stopPropagation()
|
|
158
|
-
|
|
159
|
-
const focusableElements = _getFocusableElements(elementsRef.current)
|
|
160
|
-
const focusableLen = focusableElements.length
|
|
161
|
-
|
|
162
|
-
if (focusableLen === 0) return
|
|
163
|
-
|
|
164
|
-
const focusedElement = elementsRef.current[activeIndex]
|
|
165
|
-
|
|
166
|
-
let focusedIndex = focusableElements.indexOf(focusedElement)
|
|
167
|
-
|
|
168
|
-
focusedIndex = (focusedIndex - 1 + focusableLen) % focusableLen
|
|
169
|
-
|
|
170
|
-
const el = focusableElements[focusedIndex]
|
|
171
|
-
const currentIndex = elementsRef.current.indexOf(el)
|
|
172
|
-
|
|
173
|
-
setActiveIndex(currentIndex)
|
|
174
|
-
activeIndexRef.current = currentIndex
|
|
175
|
-
|
|
176
|
-
return
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (event.key === 'ArrowDown') {
|
|
180
|
-
event.preventDefault()
|
|
181
|
-
event.stopPropagation()
|
|
182
|
-
|
|
183
|
-
const focusableElements = _getFocusableElements(elementsRef.current)
|
|
184
|
-
const focusableLen = focusableElements.length
|
|
185
|
-
|
|
186
|
-
if (focusableLen === 0) return
|
|
187
|
-
|
|
188
|
-
const focusedElement = elementsRef.current[activeIndex]
|
|
189
|
-
|
|
190
|
-
let focusedIndex = focusableElements.indexOf(focusedElement)
|
|
191
|
-
|
|
192
|
-
focusedIndex = (focusedIndex + 1) % focusableLen
|
|
193
|
-
|
|
194
|
-
const el = focusableElements[focusedIndex]
|
|
195
|
-
const currentIndex = elementsRef.current.indexOf(el)
|
|
196
|
-
|
|
197
|
-
setActiveIndex(currentIndex)
|
|
198
|
-
activeIndexRef.current = currentIndex
|
|
199
|
-
|
|
200
|
-
return
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (onKeyDown) {
|
|
204
|
-
onKeyDown(event)
|
|
205
|
-
}
|
|
206
|
-
},
|
|
207
|
-
[activeIndex, onKeyDown, originElement]
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
const handleItemMouseEnter = useCallback((event: React.MouseEvent<HTMLElement>) => {
|
|
211
|
-
const element = event.currentTarget
|
|
212
|
-
const currentIndex = elementsRef.current.indexOf(element)
|
|
213
|
-
|
|
214
|
-
setActiveIndex(currentIndex)
|
|
215
|
-
activeIndexRef.current = currentIndex
|
|
216
|
-
}, [])
|
|
217
|
-
|
|
218
|
-
const handleItemMouseLeave = useCallback(() => {
|
|
219
|
-
rootElement?.focus()
|
|
220
|
-
setActiveIndex(-1)
|
|
221
|
-
activeIndexRef.current = -1
|
|
222
|
-
}, [rootElement])
|
|
223
|
-
|
|
224
86
|
// Trigger `onItemSelect` when active index changes
|
|
225
87
|
useEffect(() => {
|
|
226
88
|
if (onItemSelect) onItemSelect(activeIndex)
|
|
227
89
|
}, [activeIndex, onItemSelect])
|
|
228
90
|
|
|
229
|
-
|
|
230
|
-
activeElementRef.current = activeElement
|
|
231
|
-
}, [activeElement])
|
|
232
|
-
|
|
233
|
-
// Set focus on the currently active element
|
|
234
|
-
useEffect(() => {
|
|
235
|
-
if (!mounted) return
|
|
236
|
-
|
|
237
|
-
const rafId = window.requestAnimationFrame(() => {
|
|
238
|
-
const _activeIndex = activeIndexRef.current
|
|
239
|
-
|
|
240
|
-
if (_activeIndex === -1) {
|
|
241
|
-
if (shouldFocus === 'first') {
|
|
242
|
-
const focusableElements = _getFocusableElements(elementsRef.current)
|
|
243
|
-
const el = focusableElements[0]
|
|
244
|
-
|
|
245
|
-
if (el) {
|
|
246
|
-
const currentIndex = elementsRef.current.indexOf(el)
|
|
247
|
-
|
|
248
|
-
setActiveIndex(currentIndex)
|
|
249
|
-
activeIndexRef.current = currentIndex
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (shouldFocus === 'last') {
|
|
254
|
-
const focusableElements = _getFocusableElements(elementsRef.current)
|
|
255
|
-
const el = focusableElements[focusableElements.length - 1]
|
|
256
|
-
|
|
257
|
-
if (el) {
|
|
258
|
-
const currentIndex = elementsRef.current.indexOf(el)
|
|
259
|
-
|
|
260
|
-
setActiveIndex(currentIndex)
|
|
261
|
-
activeIndexRef.current = currentIndex
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const element = elementsRef.current[_activeIndex] || null
|
|
269
|
-
|
|
270
|
-
element?.focus()
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
return () => {
|
|
274
|
-
window.cancelAnimationFrame(rafId)
|
|
275
|
-
}
|
|
276
|
-
}, [activeIndex, mounted, shouldFocus])
|
|
277
|
-
|
|
91
|
+
// Close menu when clicking outside
|
|
278
92
|
useClickOutside(
|
|
279
93
|
useCallback(
|
|
280
94
|
(event) => isTopLayer && onClickOutside && onClickOutside(event),
|
|
@@ -283,6 +97,7 @@ export const Menu = forwardRef(function Menu(
|
|
|
283
97
|
[rootElement]
|
|
284
98
|
)
|
|
285
99
|
|
|
100
|
+
// Close menu when pressing Escape
|
|
286
101
|
useGlobalKeyDown(
|
|
287
102
|
useCallback(
|
|
288
103
|
(event) => {
|
|
@@ -297,6 +112,7 @@ export const Menu = forwardRef(function Menu(
|
|
|
297
112
|
)
|
|
298
113
|
)
|
|
299
114
|
|
|
115
|
+
// Register root element (for nested menus)
|
|
300
116
|
useEffect(() => {
|
|
301
117
|
if (!rootElement || !registerElement) return
|
|
302
118
|
|
|
@@ -311,10 +127,14 @@ export const Menu = forwardRef(function Menu(
|
|
|
311
127
|
mount,
|
|
312
128
|
onClickOutside,
|
|
313
129
|
onEscape,
|
|
314
|
-
onMouseEnter: handleItemMouseEnter,
|
|
315
|
-
onMouseLeave: handleItemMouseLeave,
|
|
316
130
|
onItemClick,
|
|
131
|
+
onItemMouseEnter: handleItemMouseEnter,
|
|
132
|
+
onItemMouseLeave: handleItemMouseLeave,
|
|
317
133
|
registerElement,
|
|
134
|
+
|
|
135
|
+
// deprecated
|
|
136
|
+
onMouseEnter: handleItemMouseEnter,
|
|
137
|
+
onMouseLeave: handleItemMouseLeave,
|
|
318
138
|
}),
|
|
319
139
|
[
|
|
320
140
|
activeElement,
|
|
@@ -336,7 +156,7 @@ export const Menu = forwardRef(function Menu(
|
|
|
336
156
|
{...restProps}
|
|
337
157
|
onKeyDown={handleKeyDown}
|
|
338
158
|
padding={padding}
|
|
339
|
-
ref={
|
|
159
|
+
ref={handleRefChange}
|
|
340
160
|
role="menu"
|
|
341
161
|
tabIndex={-1}
|
|
342
162
|
>
|
|
@@ -9,9 +9,19 @@ export interface MenuContextValue {
|
|
|
9
9
|
onClickOutside?: (event: MouseEvent) => void
|
|
10
10
|
onEscape?: () => void
|
|
11
11
|
onItemClick?: () => void
|
|
12
|
+
onItemMouseEnter?: (event: React.MouseEvent<HTMLElement>) => void
|
|
13
|
+
onItemMouseLeave?: (event: React.MouseEvent<HTMLElement>) => void
|
|
14
|
+
registerElement?: (el: HTMLElement) => () => void
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @deprecated Use `onItemMouseEnter` instead
|
|
18
|
+
*/
|
|
12
19
|
onMouseEnter: (event: React.MouseEvent<HTMLElement>) => void
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @deprecated Use `onItemMouseLeave` instead
|
|
23
|
+
*/
|
|
13
24
|
onMouseLeave: (event: React.MouseEvent<HTMLElement>) => void
|
|
14
|
-
registerElement?: (el: HTMLElement) => () => void
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
const key = Symbol.for('@sanity/ui/context/menu')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {ChevronRightIcon} from '@sanity/icons'
|
|
2
|
-
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
|
3
|
-
import
|
|
4
|
-
import {Box, Popover, PopoverProps, Text} from '../../primitives'
|
|
2
|
+
import React, {createElement, isValidElement, useCallback, useEffect, useRef, useState} from 'react'
|
|
3
|
+
import {isValidElementType} from 'react-is'
|
|
4
|
+
import {Box, Flex, Popover, PopoverProps, Text} from '../../primitives'
|
|
5
5
|
import {Selectable} from '../../primitives/_selectable'
|
|
6
6
|
import {SelectableTone} from '../../types/selectable'
|
|
7
7
|
import {Menu} from './menu'
|
|
@@ -13,26 +13,15 @@ import {useMenu} from './useMenu'
|
|
|
13
13
|
export interface MenuGroupProps {
|
|
14
14
|
as?: React.ElementType | keyof JSX.IntrinsicElements
|
|
15
15
|
fontSize?: number | number[]
|
|
16
|
+
icon?: React.ComponentType | React.ReactNode
|
|
16
17
|
padding?: number | number[]
|
|
17
18
|
popover?: Omit<PopoverProps, 'content' | 'open'>
|
|
18
19
|
radius?: number | number[]
|
|
20
|
+
space?: number | number[]
|
|
19
21
|
text: React.ReactNode
|
|
20
22
|
tone?: SelectableTone
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
const MOUSE_LEAVE_TIMEOUT = 1000
|
|
24
|
-
|
|
25
|
-
const TextContainer = styled.span`
|
|
26
|
-
&:not([hidden]) {
|
|
27
|
-
display: flex;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
& > div:first-child {
|
|
31
|
-
flex: 1;
|
|
32
|
-
min-width: 0;
|
|
33
|
-
}
|
|
34
|
-
`
|
|
35
|
-
|
|
36
25
|
/**
|
|
37
26
|
* @public
|
|
38
27
|
*/
|
|
@@ -44,56 +33,39 @@ export function MenuGroup(
|
|
|
44
33
|
as = 'button',
|
|
45
34
|
children,
|
|
46
35
|
fontSize,
|
|
36
|
+
icon,
|
|
47
37
|
onClick,
|
|
48
38
|
padding = 3,
|
|
49
39
|
popover = {},
|
|
50
40
|
radius = 2,
|
|
41
|
+
space = 3,
|
|
51
42
|
text,
|
|
52
43
|
tone = 'default',
|
|
53
44
|
...restProps
|
|
54
45
|
} = props
|
|
55
|
-
const
|
|
46
|
+
const menu = useMenu()
|
|
56
47
|
const {
|
|
57
48
|
activeElement,
|
|
58
49
|
mount,
|
|
59
50
|
onClickOutside,
|
|
60
51
|
onEscape,
|
|
61
52
|
onItemClick,
|
|
62
|
-
onMouseEnter,
|
|
63
|
-
onMouseLeave,
|
|
53
|
+
onItemMouseEnter = menu.onMouseEnter,
|
|
64
54
|
registerElement,
|
|
65
|
-
} =
|
|
55
|
+
} = menu
|
|
66
56
|
const [rootElement, setRootElement] = useState<HTMLButtonElement | null>(null)
|
|
67
|
-
const
|
|
57
|
+
const [open, setOpen] = useState(false)
|
|
68
58
|
const shouldFocusRef = useRef<'first' | 'last' | null>(null)
|
|
69
59
|
const active = Boolean(activeElement) && activeElement === rootElement
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// Register the element
|
|
73
|
-
useEffect(() => mount(rootElement), [mount, rootElement])
|
|
60
|
+
const [withinMenu, setWithinMenu] = useState(false)
|
|
74
61
|
|
|
75
62
|
const handleMouseEnter = useCallback(
|
|
76
63
|
(event: React.MouseEvent<HTMLElement>) => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
mouseLeaveTimeoutRef.current = null
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
onMouseEnter(event)
|
|
64
|
+
setWithinMenu(false)
|
|
65
|
+
onItemMouseEnter(event)
|
|
83
66
|
setOpen(true)
|
|
84
67
|
},
|
|
85
|
-
[
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
const handleMouseLeave = useCallback(
|
|
89
|
-
(event: React.MouseEvent<HTMLElement>) => {
|
|
90
|
-
onMouseLeave(event)
|
|
91
|
-
|
|
92
|
-
mouseLeaveTimeoutRef.current = setTimeout(() => {
|
|
93
|
-
setOpen(false)
|
|
94
|
-
}, MOUSE_LEAVE_TIMEOUT)
|
|
95
|
-
},
|
|
96
|
-
[onMouseLeave]
|
|
68
|
+
[onItemMouseEnter]
|
|
97
69
|
)
|
|
98
70
|
|
|
99
71
|
const handleMenuKeyDown = useCallback(
|
|
@@ -111,19 +83,6 @@ export function MenuGroup(
|
|
|
111
83
|
[rootElement]
|
|
112
84
|
)
|
|
113
85
|
|
|
114
|
-
const handleMenuMouseEnter = useCallback(() => {
|
|
115
|
-
if (mouseLeaveTimeoutRef.current) {
|
|
116
|
-
clearTimeout(mouseLeaveTimeoutRef.current)
|
|
117
|
-
mouseLeaveTimeoutRef.current = null
|
|
118
|
-
}
|
|
119
|
-
}, [])
|
|
120
|
-
|
|
121
|
-
const handleMenuMouseLeave = useCallback(() => {
|
|
122
|
-
mouseLeaveTimeoutRef.current = setTimeout(() => {
|
|
123
|
-
setOpen(false)
|
|
124
|
-
}, MOUSE_LEAVE_TIMEOUT)
|
|
125
|
-
}, [])
|
|
126
|
-
|
|
127
86
|
const handleClick = useCallback(
|
|
128
87
|
(event: React.MouseEvent<HTMLDivElement>) => {
|
|
129
88
|
if (onClick) onClick(event)
|
|
@@ -139,28 +98,33 @@ export function MenuGroup(
|
|
|
139
98
|
[onClick]
|
|
140
99
|
)
|
|
141
100
|
|
|
142
|
-
const
|
|
101
|
+
const handleChildItemClick = useCallback(() => {
|
|
143
102
|
setOpen(false)
|
|
144
103
|
if (onItemClick) onItemClick()
|
|
145
104
|
}, [onItemClick])
|
|
146
105
|
|
|
106
|
+
const handleMenuMouseEnter = useCallback(() => setWithinMenu(true), [])
|
|
107
|
+
|
|
108
|
+
// Register the menu item element
|
|
109
|
+
useEffect(() => mount(rootElement), [mount, rootElement])
|
|
110
|
+
|
|
111
|
+
// Close child menu when a sibling item becomes active
|
|
147
112
|
useEffect(() => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}, [])
|
|
113
|
+
if (!active) setOpen(false)
|
|
114
|
+
}, [active])
|
|
115
|
+
|
|
116
|
+
// Update state when child menu is no longer open
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!open) setWithinMenu(false)
|
|
119
|
+
}, [open])
|
|
155
120
|
|
|
156
|
-
const
|
|
121
|
+
const childMenu = (
|
|
157
122
|
<Menu
|
|
158
123
|
onClickOutside={onClickOutside}
|
|
159
124
|
onEscape={onEscape}
|
|
160
|
-
onItemClick={
|
|
125
|
+
onItemClick={handleChildItemClick}
|
|
161
126
|
onKeyDown={handleMenuKeyDown}
|
|
162
127
|
onMouseEnter={handleMenuMouseEnter}
|
|
163
|
-
onMouseLeave={handleMenuMouseLeave}
|
|
164
128
|
registerElement={registerElement}
|
|
165
129
|
shouldFocus={shouldFocusRef.current}
|
|
166
130
|
>
|
|
@@ -179,6 +143,7 @@ export function MenuGroup(
|
|
|
179
143
|
shouldFocusRef.current = 'first'
|
|
180
144
|
|
|
181
145
|
setOpen(true)
|
|
146
|
+
setWithinMenu(true)
|
|
182
147
|
|
|
183
148
|
requestAnimationFrame(() => {
|
|
184
149
|
shouldFocusRef.current = null
|
|
@@ -189,32 +154,45 @@ export function MenuGroup(
|
|
|
189
154
|
}, [])
|
|
190
155
|
|
|
191
156
|
return (
|
|
192
|
-
<Popover {...popover} content={
|
|
157
|
+
<Popover {...popover} content={childMenu} data-ui="MenuGroup__popover" open={open}>
|
|
193
158
|
<Selectable
|
|
194
159
|
data-as={as}
|
|
195
160
|
data-ui="MenuGroup"
|
|
196
161
|
forwardedAs={as}
|
|
197
162
|
{...restProps}
|
|
198
|
-
aria-pressed={as === 'button' ?
|
|
199
|
-
data-pressed={as !== 'button' ?
|
|
200
|
-
data-selected={active ? '' : undefined}
|
|
163
|
+
aria-pressed={as === 'button' ? withinMenu : undefined}
|
|
164
|
+
data-pressed={as !== 'button' ? withinMenu : undefined}
|
|
165
|
+
data-selected={!withinMenu && active ? '' : undefined}
|
|
201
166
|
$radius={radius}
|
|
202
167
|
$tone={tone}
|
|
203
168
|
onClick={handleClick}
|
|
204
169
|
onKeyDown={handleKeyDown}
|
|
205
170
|
onMouseEnter={handleMouseEnter}
|
|
206
|
-
onMouseLeave={handleMouseLeave}
|
|
207
171
|
ref={setRootElement}
|
|
208
172
|
tabIndex={-1}
|
|
209
173
|
type={as === 'button' ? 'button' : undefined}
|
|
210
174
|
>
|
|
211
175
|
<Box padding={padding}>
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
176
|
+
<Flex>
|
|
177
|
+
{icon && (
|
|
178
|
+
<Text size={fontSize}>
|
|
179
|
+
{isValidElement(icon) && icon}
|
|
180
|
+
{isValidElementType(icon) && createElement(icon)}
|
|
181
|
+
</Text>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
<Box flex={1} marginLeft={icon ? space : undefined}>
|
|
185
|
+
<Text size={fontSize} textOverflow="ellipsis">
|
|
186
|
+
{text}
|
|
187
|
+
</Text>
|
|
188
|
+
</Box>
|
|
189
|
+
|
|
190
|
+
<Box marginLeft={space}>
|
|
191
|
+
<Text size={fontSize}>
|
|
192
|
+
<ChevronRightIcon />
|
|
193
|
+
</Text>
|
|
194
|
+
</Box>
|
|
195
|
+
</Flex>
|
|
218
196
|
</Box>
|
|
219
197
|
</Selectable>
|
|
220
198
|
</Popover>
|
|
@@ -64,9 +64,16 @@ export const MenuItem = forwardRef(function MenuItem(
|
|
|
64
64
|
tone = 'default',
|
|
65
65
|
...restProps
|
|
66
66
|
} = props
|
|
67
|
-
const
|
|
67
|
+
const menu = useMenu()
|
|
68
|
+
const {
|
|
69
|
+
activeElement,
|
|
70
|
+
mount,
|
|
71
|
+
onItemClick,
|
|
72
|
+
onItemMouseEnter = menu.onMouseEnter,
|
|
73
|
+
onItemMouseLeave = menu.onMouseLeave,
|
|
74
|
+
} = menu
|
|
68
75
|
const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
|
|
69
|
-
const
|
|
76
|
+
const active = Boolean(activeElement) && activeElement === rootElement
|
|
70
77
|
|
|
71
78
|
useEffect(() => mount(rootElement, selectedProp), [mount, rootElement, selectedProp])
|
|
72
79
|
|
|
@@ -110,14 +117,14 @@ export const MenuItem = forwardRef(function MenuItem(
|
|
|
110
117
|
{...restProps}
|
|
111
118
|
aria-pressed={as === 'button' && pressed}
|
|
112
119
|
data-pressed={as !== 'button' && pressed ? '' : undefined}
|
|
113
|
-
data-selected={
|
|
120
|
+
data-selected={active ? '' : undefined}
|
|
114
121
|
data-disabled={disabled ? '' : undefined}
|
|
115
122
|
$radius={radius}
|
|
116
123
|
$tone={tone}
|
|
117
124
|
disabled={disabled}
|
|
118
125
|
onClick={handleClick}
|
|
119
|
-
onMouseEnter={
|
|
120
|
-
onMouseLeave={
|
|
126
|
+
onMouseEnter={onItemMouseEnter}
|
|
127
|
+
onMouseLeave={onItemMouseLeave}
|
|
121
128
|
padding={0}
|
|
122
129
|
ref={setRef}
|
|
123
130
|
role="menuitem"
|