@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
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import {useCallback, useEffect, useRef, useState} from 'react'
|
|
2
|
+
import {_getFocusableElements, _sortElements} from './helpers'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export interface MenuController {
|
|
8
|
+
activeElement: HTMLElement | null
|
|
9
|
+
activeIndex: number
|
|
10
|
+
handleItemMouseEnter: (event: React.MouseEvent<HTMLElement>) => void
|
|
11
|
+
handleItemMouseLeave: () => void
|
|
12
|
+
handleKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void
|
|
13
|
+
mount: (element: HTMLElement | null, selected?: boolean) => () => void
|
|
14
|
+
rootElement: HTMLDivElement | null
|
|
15
|
+
setRootElement: (el: HTMLDivElement | null) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* This controller is responsible for controlling UI menu state.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export function useMenuController(props: {
|
|
24
|
+
onKeyDown?: React.KeyboardEventHandler
|
|
25
|
+
originElement?: HTMLElement | null
|
|
26
|
+
shouldFocus: 'first' | 'last' | null
|
|
27
|
+
}): MenuController {
|
|
28
|
+
const {onKeyDown, originElement, shouldFocus} = props
|
|
29
|
+
|
|
30
|
+
const elementsRef = useRef<HTMLElement[]>([])
|
|
31
|
+
|
|
32
|
+
const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
|
|
33
|
+
const [activeIndex, _setActiveIndex] = useState(-1)
|
|
34
|
+
|
|
35
|
+
const activeElement = elementsRef.current[activeIndex] || null
|
|
36
|
+
const activeIndexRef = useRef(activeIndex)
|
|
37
|
+
|
|
38
|
+
const mounted = Boolean(rootElement)
|
|
39
|
+
|
|
40
|
+
const setActiveIndex = useCallback((nextActiveIndex) => {
|
|
41
|
+
_setActiveIndex(nextActiveIndex)
|
|
42
|
+
activeIndexRef.current = nextActiveIndex
|
|
43
|
+
}, [])
|
|
44
|
+
|
|
45
|
+
const mount = useCallback(
|
|
46
|
+
(element: HTMLElement | null, selected?: boolean): (() => void) => {
|
|
47
|
+
if (!element) return () => undefined
|
|
48
|
+
|
|
49
|
+
if (elementsRef.current.indexOf(element) === -1) {
|
|
50
|
+
elementsRef.current.push(element)
|
|
51
|
+
_sortElements(rootElement, elementsRef.current)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (selected) {
|
|
55
|
+
const selectedIndex = elementsRef.current.indexOf(element)
|
|
56
|
+
|
|
57
|
+
setActiveIndex(selectedIndex)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
const idx = elementsRef.current.indexOf(element)
|
|
62
|
+
|
|
63
|
+
if (idx > -1) {
|
|
64
|
+
elementsRef.current.splice(idx, 1)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
[rootElement, setActiveIndex]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const handleKeyDown = useCallback(
|
|
72
|
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
73
|
+
// Move focus to the element that opened the menu before handling the `Tab` press
|
|
74
|
+
if (event.key === 'Tab') {
|
|
75
|
+
if (originElement) {
|
|
76
|
+
originElement.focus()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Move focus to the first focusable menuitem
|
|
83
|
+
if (event.key === 'Home') {
|
|
84
|
+
event.preventDefault()
|
|
85
|
+
event.stopPropagation()
|
|
86
|
+
|
|
87
|
+
const focusableElements = _getFocusableElements(elementsRef.current)
|
|
88
|
+
const el = focusableElements[0]
|
|
89
|
+
|
|
90
|
+
if (!el) return
|
|
91
|
+
|
|
92
|
+
const currentIndex = elementsRef.current.indexOf(el)
|
|
93
|
+
|
|
94
|
+
setActiveIndex(currentIndex)
|
|
95
|
+
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Move focus to the last focusable menuitem
|
|
100
|
+
if (event.key === 'End') {
|
|
101
|
+
event.preventDefault()
|
|
102
|
+
event.stopPropagation()
|
|
103
|
+
|
|
104
|
+
const focusableElements = _getFocusableElements(elementsRef.current)
|
|
105
|
+
const el = focusableElements[focusableElements.length - 1]
|
|
106
|
+
|
|
107
|
+
if (!el) return
|
|
108
|
+
|
|
109
|
+
const currentIndex = elementsRef.current.indexOf(el)
|
|
110
|
+
|
|
111
|
+
setActiveIndex(currentIndex)
|
|
112
|
+
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (event.key === 'ArrowUp') {
|
|
117
|
+
event.preventDefault()
|
|
118
|
+
event.stopPropagation()
|
|
119
|
+
|
|
120
|
+
const focusableElements = _getFocusableElements(elementsRef.current)
|
|
121
|
+
const focusableLen = focusableElements.length
|
|
122
|
+
|
|
123
|
+
if (focusableLen === 0) return
|
|
124
|
+
|
|
125
|
+
const focusedElement = elementsRef.current[activeIndex]
|
|
126
|
+
|
|
127
|
+
let focusedIndex = focusableElements.indexOf(focusedElement)
|
|
128
|
+
|
|
129
|
+
focusedIndex = (focusedIndex - 1 + focusableLen) % focusableLen
|
|
130
|
+
|
|
131
|
+
const el = focusableElements[focusedIndex]
|
|
132
|
+
const currentIndex = elementsRef.current.indexOf(el)
|
|
133
|
+
|
|
134
|
+
setActiveIndex(currentIndex)
|
|
135
|
+
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (event.key === 'ArrowDown') {
|
|
140
|
+
event.preventDefault()
|
|
141
|
+
event.stopPropagation()
|
|
142
|
+
|
|
143
|
+
const focusableElements = _getFocusableElements(elementsRef.current)
|
|
144
|
+
const focusableLen = focusableElements.length
|
|
145
|
+
|
|
146
|
+
if (focusableLen === 0) return
|
|
147
|
+
|
|
148
|
+
const focusedElement = elementsRef.current[activeIndex]
|
|
149
|
+
|
|
150
|
+
let focusedIndex = focusableElements.indexOf(focusedElement)
|
|
151
|
+
|
|
152
|
+
focusedIndex = (focusedIndex + 1) % focusableLen
|
|
153
|
+
|
|
154
|
+
const el = focusableElements[focusedIndex]
|
|
155
|
+
const currentIndex = elementsRef.current.indexOf(el)
|
|
156
|
+
|
|
157
|
+
setActiveIndex(currentIndex)
|
|
158
|
+
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (onKeyDown) {
|
|
163
|
+
onKeyDown(event)
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
[activeIndex, onKeyDown, originElement, setActiveIndex]
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
const handleItemMouseEnter = useCallback(
|
|
170
|
+
(event: React.MouseEvent<HTMLElement>) => {
|
|
171
|
+
const element = event.currentTarget
|
|
172
|
+
const currentIndex = elementsRef.current.indexOf(element)
|
|
173
|
+
|
|
174
|
+
setActiveIndex(currentIndex)
|
|
175
|
+
},
|
|
176
|
+
[setActiveIndex]
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
const handleItemMouseLeave = useCallback(() => {
|
|
180
|
+
rootElement?.focus()
|
|
181
|
+
setActiveIndex(-1)
|
|
182
|
+
}, [rootElement, setActiveIndex])
|
|
183
|
+
|
|
184
|
+
// Set focus on the currently active element
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (!mounted) return
|
|
187
|
+
|
|
188
|
+
const rafId = window.requestAnimationFrame(() => {
|
|
189
|
+
const _activeIndex = activeIndexRef.current
|
|
190
|
+
|
|
191
|
+
if (_activeIndex === -1) {
|
|
192
|
+
if (shouldFocus === 'first') {
|
|
193
|
+
const focusableElements = _getFocusableElements(elementsRef.current)
|
|
194
|
+
const el = focusableElements[0]
|
|
195
|
+
|
|
196
|
+
if (el) {
|
|
197
|
+
const currentIndex = elementsRef.current.indexOf(el)
|
|
198
|
+
|
|
199
|
+
setActiveIndex(currentIndex)
|
|
200
|
+
activeIndexRef.current = currentIndex
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (shouldFocus === 'last') {
|
|
205
|
+
const focusableElements = _getFocusableElements(elementsRef.current)
|
|
206
|
+
const el = focusableElements[focusableElements.length - 1]
|
|
207
|
+
|
|
208
|
+
if (el) {
|
|
209
|
+
const currentIndex = elementsRef.current.indexOf(el)
|
|
210
|
+
|
|
211
|
+
setActiveIndex(currentIndex)
|
|
212
|
+
activeIndexRef.current = currentIndex
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const element = elementsRef.current[_activeIndex] || null
|
|
220
|
+
|
|
221
|
+
element?.focus()
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
return () => {
|
|
225
|
+
window.cancelAnimationFrame(rafId)
|
|
226
|
+
}
|
|
227
|
+
}, [activeIndex, mounted, setActiveIndex, shouldFocus])
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
activeElement,
|
|
231
|
+
activeIndex,
|
|
232
|
+
handleItemMouseEnter,
|
|
233
|
+
handleItemMouseLeave,
|
|
234
|
+
handleKeyDown,
|
|
235
|
+
mount,
|
|
236
|
+
rootElement,
|
|
237
|
+
setRootElement,
|
|
238
|
+
}
|
|
239
|
+
}
|