@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.
@@ -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
+ }