@sanity/ui 2.8.5 → 2.8.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ui",
3
- "version": "2.8.5",
3
+ "version": "2.8.6",
4
4
  "keywords": [
5
5
  "sanity",
6
6
  "ui",
@@ -121,7 +121,7 @@
121
121
  "@commitlint/cli": "^19.3.0",
122
122
  "@commitlint/config-conventional": "^19.2.2",
123
123
  "@juggle/resize-observer": "^3.4.0",
124
- "@sanity/pkg-utils": "^6.10.4",
124
+ "@sanity/pkg-utils": "^6.10.6",
125
125
  "@sanity/prettier-config": "^1.0.2",
126
126
  "@sanity/semantic-release-preset": "^5.0.0",
127
127
  "@sanity/ui-workshop": "^2.0.15",
@@ -1,6 +1,6 @@
1
1
  import {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef} from 'react'
2
2
  import {styled} from 'styled-components'
3
- import {useClickOutside, useGlobalKeyDown} from '../../hooks'
3
+ import {useClickOutsideEvent, useGlobalKeyDown} from '../../hooks'
4
4
  import {Box, Stack} from '../../primitives'
5
5
  import {ResponsivePaddingProps} from '../../primitives/types'
6
6
  import {useLayer} from '../../utils'
@@ -75,16 +75,29 @@ export const Menu = forwardRef(function Menu(
75
75
  handleItemMouseLeave,
76
76
  handleKeyDown,
77
77
  mount,
78
- rootElement,
79
- setRootElement,
80
- } = useMenuController({onKeyDown, originElement, shouldFocus})
78
+ } = useMenuController({onKeyDown, originElement, shouldFocus, rootElementRef: ref})
81
79
 
80
+ const unregisterElementRef = useRef<(() => void) | null>(null)
82
81
  const handleRefChange = useCallback(
83
82
  (el: HTMLDivElement | null) => {
84
- setRootElement(el)
83
+ // Run cleanup of previously registered elements
84
+ if (unregisterElementRef.current) {
85
+ // The `registerElement` callback were originally used in a `useEffect`, so it returns a cleanup function that is a bit gnarly to handle in a ref callback.
86
+ // Since we can't change the `registerElement` implementation itself without making breaking change,
87
+ // that is explained in the code comments for createGlobalScopedContext.tsx,
88
+ // we need to handle with a ref that holds on to the cleanup function last returned when the ref callback is called.
89
+ unregisterElementRef.current()
90
+ unregisterElementRef.current = null
91
+ }
92
+
85
93
  ref.current = el
94
+
95
+ // Register root element (for nested menus)
96
+ if (ref.current && registerElement) {
97
+ unregisterElementRef.current = registerElement(ref.current)
98
+ }
86
99
  },
87
- [setRootElement],
100
+ [registerElement],
88
101
  )
89
102
 
90
103
  // Trigger `onItemSelect` when active index changes
@@ -93,13 +106,7 @@ export const Menu = forwardRef(function Menu(
93
106
  }, [activeIndex, onItemSelect])
94
107
 
95
108
  // Close menu when clicking outside
96
- useClickOutside(
97
- useCallback(
98
- (event) => isTopLayer && onClickOutside && onClickOutside(event),
99
- [isTopLayer, onClickOutside],
100
- ),
101
- [rootElement],
102
- )
109
+ useClickOutsideEvent(isTopLayer && onClickOutside, () => [ref.current])
103
110
 
104
111
  // Close menu when pressing Escape
105
112
  useGlobalKeyDown(
@@ -116,13 +123,6 @@ export const Menu = forwardRef(function Menu(
116
123
  ),
117
124
  )
118
125
 
119
- // Register root element (for nested menus)
120
- useEffect(() => {
121
- if (!rootElement || !registerElement) return
122
-
123
- return registerElement(rootElement)
124
- }, [registerElement, rootElement])
125
-
126
126
  const value: MenuContextValue = useMemo(
127
127
  () => ({
128
128
  version: 0.0,
@@ -196,11 +196,7 @@ export const MenuButton = forwardRef(function MenuButton(
196
196
  const registerElement = useCallback((el: HTMLElement) => {
197
197
  setChildMenuElements((els) => els.concat([el]))
198
198
 
199
- return () => {
200
- setChildMenuElements((els) => {
201
- return els.filter((_el) => _el !== el)
202
- })
203
- }
199
+ return () => setChildMenuElements((els) => els.filter((_el) => _el !== el))
204
200
  }, [])
205
201
 
206
202
  const menuProps: MenuProps = useMemo(
@@ -1,5 +1,5 @@
1
1
  import {ChevronRightIcon} from '@sanity/icons'
2
- import {isValidElement, useCallback, useEffect, useRef, useState} from 'react'
2
+ import {isValidElement, useCallback, useEffect, useState} from 'react'
3
3
  import {isValidElementType} from 'react-is'
4
4
  import {useArrayProp} from '../../hooks'
5
5
  import {Box, Flex, Popover, PopoverProps, Text} from '../../primitives'
@@ -58,7 +58,7 @@ export function MenuGroup(
58
58
  } = menu
59
59
  const [rootElement, setRootElement] = useState<HTMLButtonElement | HTMLDivElement | null>(null)
60
60
  const [open, setOpen] = useState(false)
61
- const shouldFocusRef = useRef<'first' | 'last' | null>(null)
61
+ const [shouldFocus, setShouldFocus] = useState<'first' | 'last' | null>(null)
62
62
  const active = Boolean(activeElement) && activeElement === rootElement
63
63
  const [withinMenu, setWithinMenu] = useState(false)
64
64
 
@@ -88,22 +88,17 @@ export function MenuGroup(
88
88
 
89
89
  const handleClick = useCallback(
90
90
  (event: React.MouseEvent<HTMLDivElement>) => {
91
- if (onClick) onClick(event)
92
-
93
- shouldFocusRef.current = 'first'
91
+ onClick?.(event)
94
92
 
93
+ setShouldFocus('first')
95
94
  setOpen(true)
96
-
97
- requestAnimationFrame(() => {
98
- shouldFocusRef.current = null
99
- })
100
95
  },
101
96
  [onClick],
102
97
  )
103
98
 
104
99
  const handleChildItemClick = useCallback(() => {
105
100
  setOpen(false)
106
- if (onItemClick) onItemClick()
101
+ onItemClick?.()
107
102
  }, [onItemClick])
108
103
 
109
104
  const handleMenuMouseEnter = useCallback(() => setWithinMenu(true), [])
@@ -121,6 +116,16 @@ export function MenuGroup(
121
116
  if (!open) setWithinMenu(false)
122
117
  }, [open])
123
118
 
119
+ // Reset the shouldFocus state after it has been used
120
+ useEffect(() => {
121
+ if (!shouldFocus) return
122
+ // The useMenuController effect that handles `shouldFocus` schedules a request animation frame where it's processed.
123
+ // By doing the same here, we ensure that the reset is processed after the focus change.
124
+ const rafId = requestAnimationFrame(() => setShouldFocus(null))
125
+
126
+ return () => cancelAnimationFrame(rafId)
127
+ }, [shouldFocus])
128
+
124
129
  const childMenu = (
125
130
  <Menu
126
131
  onClickOutside={onClickOutside}
@@ -129,7 +134,7 @@ export function MenuGroup(
129
134
  onKeyDown={handleMenuKeyDown}
130
135
  onMouseEnter={handleMenuMouseEnter}
131
136
  registerElement={registerElement}
132
- shouldFocus={shouldFocusRef.current}
137
+ shouldFocus={shouldFocus}
133
138
  >
134
139
  {children}
135
140
  </Menu>
@@ -143,15 +148,10 @@ export function MenuGroup(
143
148
  }
144
149
 
145
150
  if (event.key === 'ArrowRight') {
146
- shouldFocusRef.current = 'first'
147
-
151
+ setShouldFocus('first')
148
152
  setOpen(true)
149
153
  setWithinMenu(true)
150
154
 
151
- requestAnimationFrame(() => {
152
- shouldFocusRef.current = null
153
- })
154
-
155
155
  return
156
156
  }
157
157
  }, [])
@@ -1,4 +1,4 @@
1
- import {useCallback, useEffect, useRef, useState} from 'react'
1
+ import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
2
2
  import {_getFocusableElements, _sortElements} from './helpers'
3
3
 
4
4
  /**
@@ -11,8 +11,6 @@ export interface MenuController {
11
11
  handleItemMouseLeave: () => void
12
12
  handleKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void
13
13
  mount: (element: HTMLElement | null, selected?: boolean) => () => void
14
- rootElement: HTMLDivElement | null
15
- setRootElement: (el: HTMLDivElement | null) => void
16
14
  }
17
15
 
18
16
  /**
@@ -24,14 +22,14 @@ export function useMenuController(props: {
24
22
  onKeyDown?: React.KeyboardEventHandler
25
23
  originElement?: HTMLElement | null
26
24
  shouldFocus: 'first' | 'last' | null
25
+ rootElementRef: React.MutableRefObject<HTMLDivElement | null>
27
26
  }): MenuController {
28
- const {onKeyDown, originElement, shouldFocus} = props
27
+ const {onKeyDown, originElement, shouldFocus, rootElementRef} = props
29
28
  const elementsRef = useRef<HTMLElement[]>([])
30
- const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
31
29
  const [activeIndex, _setActiveIndex] = useState(-1)
32
30
  const activeIndexRef = useRef(activeIndex)
33
- const activeElement = elementsRef.current[activeIndex] || null
34
- const mounted = Boolean(rootElement)
31
+ const activeElement = useMemo(() => elementsRef.current[activeIndex] || null, [activeIndex])
32
+ const mounted = Boolean(rootElementRef.current)
35
33
 
36
34
  const setActiveIndex = useCallback((nextActiveIndex: number) => {
37
35
  _setActiveIndex(nextActiveIndex)
@@ -44,7 +42,7 @@ export function useMenuController(props: {
44
42
 
45
43
  if (elementsRef.current.indexOf(element) === -1) {
46
44
  elementsRef.current.push(element)
47
- _sortElements(rootElement, elementsRef.current)
45
+ _sortElements(rootElementRef.current, elementsRef.current)
48
46
  }
49
47
 
50
48
  if (selected) {
@@ -61,7 +59,7 @@ export function useMenuController(props: {
61
59
  }
62
60
  }
63
61
  },
64
- [rootElement, setActiveIndex],
62
+ [rootElementRef, setActiveIndex],
65
63
  )
66
64
 
67
65
  const handleKeyDown = useCallback(
@@ -179,17 +177,15 @@ export function useMenuController(props: {
179
177
  // which would be incorrect when the user hovers over a gap
180
178
  // between two menu items or a menu divider.
181
179
  setActiveIndex(-2)
182
- rootElement?.focus()
183
- }, [setActiveIndex, rootElement])
180
+ rootElementRef.current?.focus()
181
+ }, [rootElementRef, setActiveIndex])
184
182
 
185
183
  // Set focus on the currently active element
186
184
  useEffect(() => {
187
185
  if (!mounted) return
188
186
 
189
- const rafId = window.requestAnimationFrame(() => {
190
- const _activeIndex = activeIndexRef.current
191
-
192
- if (_activeIndex === -1) {
187
+ const rafId = requestAnimationFrame(() => {
188
+ if (activeIndex === -1) {
193
189
  if (shouldFocus === 'first') {
194
190
  const focusableElements = _getFocusableElements(elementsRef.current)
195
191
  const el = focusableElements[0]
@@ -198,7 +194,6 @@ export function useMenuController(props: {
198
194
  const currentIndex = elementsRef.current.indexOf(el)
199
195
 
200
196
  setActiveIndex(currentIndex)
201
- activeIndexRef.current = currentIndex
202
197
  }
203
198
  }
204
199
 
@@ -210,21 +205,18 @@ export function useMenuController(props: {
210
205
  const currentIndex = elementsRef.current.indexOf(el)
211
206
 
212
207
  setActiveIndex(currentIndex)
213
- activeIndexRef.current = currentIndex
214
208
  }
215
209
  }
216
210
 
217
211
  return
218
212
  }
219
213
 
220
- const element = elementsRef.current[_activeIndex] || null
214
+ const element = elementsRef.current[activeIndex] || null
221
215
 
222
216
  element?.focus()
223
217
  })
224
218
 
225
- return () => {
226
- window.cancelAnimationFrame(rafId)
227
- }
219
+ return () => cancelAnimationFrame(rafId)
228
220
  }, [activeIndex, mounted, setActiveIndex, shouldFocus])
229
221
 
230
222
  return {
@@ -234,7 +226,5 @@ export function useMenuController(props: {
234
226
  handleItemMouseLeave,
235
227
  handleKeyDown,
236
228
  mount,
237
- rootElement,
238
- setRootElement,
239
229
  }
240
230
  }
@@ -14,7 +14,7 @@ const loadingAnimation = keyframes`
14
14
  }
15
15
  100% {
16
16
  width: 100%;
17
- }
17
+ }
18
18
  `
19
19
 
20
20
  const LOADING_BAR_HEIGHT = 2
@@ -35,15 +35,13 @@ export function useClickOutsideEvent(
35
35
  return
36
36
  }
37
37
 
38
- const resolvedBoundaryElement =
39
- typeof boundaryElement === 'function' ? boundaryElement() : boundaryElement
38
+ const resolvedBoundaryElement = boundaryElement?.()
40
39
 
41
40
  if (resolvedBoundaryElement && !resolvedBoundaryElement.contains(target)) {
42
41
  return
43
42
  }
44
43
 
45
- const resolvedElements = Array.isArray(elementsArg) ? elementsArg : elementsArg()
46
- const elements = resolvedElements.flat()
44
+ const elements = elementsArg().flat()
47
45
 
48
46
  for (const el of elements) {
49
47
  if (!el) continue