@sanity/ui 2.8.13 → 2.9.0-canary.1

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.
Files changed (34) hide show
  1. package/dist/_chunks-cjs/getTheme_v2.js +84 -28
  2. package/dist/_chunks-cjs/getTheme_v2.js.map +1 -1
  3. package/dist/_chunks-es/getTheme_v2.mjs +84 -28
  4. package/dist/_chunks-es/getTheme_v2.mjs.map +1 -1
  5. package/dist/_legacy/getTheme_v2.esm.js +84 -28
  6. package/dist/_legacy/getTheme_v2.esm.js.map +1 -1
  7. package/dist/index.esm.js +3398 -3050
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/index.js +3399 -3052
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +3398 -3050
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/theme.esm.js +1197 -448
  14. package/dist/theme.esm.js.map +1 -1
  15. package/dist/theme.js +1197 -448
  16. package/dist/theme.js.map +1 -1
  17. package/dist/theme.mjs +1197 -448
  18. package/dist/theme.mjs.map +1 -1
  19. package/package.json +27 -34
  20. package/src/core/components/autocomplete/autocomplete.tsx +42 -31
  21. package/src/core/components/dialog/dialog.tsx +4 -2
  22. package/src/core/components/menu/menu.tsx +3 -1
  23. package/src/core/components/menu/menuGroup.tsx +2 -1
  24. package/src/core/components/menu/menuItem.tsx +4 -2
  25. package/src/core/components/menu/useMenuController.ts +18 -21
  26. package/src/core/components/tree/treeItem.tsx +13 -13
  27. package/src/core/hooks/useArrayProp.ts +14 -9
  28. package/src/core/hooks/useMatchMedia.ts +12 -22
  29. package/src/core/primitives/avatar/avatarStack.tsx +2 -5
  30. package/src/core/primitives/popover/popover.tsx +22 -26
  31. package/src/core/primitives/tooltip/tooltip.tsx +7 -4
  32. package/src/core/theme/themeProvider.tsx +4 -6
  33. package/src/core/utils/elementQuery/elementQuery.tsx +2 -1
  34. package/src/core/utils/portal/portalProvider.tsx +5 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ui",
3
- "version": "2.8.13",
3
+ "version": "2.9.0-canary.1",
4
4
  "keywords": [
5
5
  "sanity",
6
6
  "ui",
@@ -51,32 +51,6 @@
51
51
  "src",
52
52
  "theme.js"
53
53
  ],
54
- "scripts": {
55
- "build": "run-s clean pkg:build pkg:check figma:pkg:build",
56
- "clean": "rimraf .workshop dist",
57
- "commit": "cz",
58
- "cypress:dev": "run-p dev cypress:open",
59
- "cypress:open": "cypress open",
60
- "cypress:run": "cypress run",
61
- "dev": "run-p storybook:dev workshop:dev",
62
- "figma:pkg:build": "cd figma && pnpm build",
63
- "format": "prettier --write --cache --ignore-unknown .",
64
- "lint": "eslint . --ext .cjs,.js,.jsx,.mjs,.ts,.tsx",
65
- "pkg:build": "pkg build --strict",
66
- "pkg:check": "pkg --strict",
67
- "prepare": "husky install",
68
- "prepack": "pnpm build",
69
- "release": "semantic-release",
70
- "storybook:build": "storybook build",
71
- "storybook:dev": "storybook dev -p 6006",
72
- "test": "jest",
73
- "test:browser": "start-server-and-test 'run-s workshop:build workshop:start' http://localhost:1337 'run-s cypress:run'",
74
- "ts:check": "tsc",
75
- "watch": "pkg watch --strict",
76
- "workshop:build": "workshop build",
77
- "workshop:dev": "workshop dev",
78
- "workshop:start": "http-server -a localhost -c-0 -p 1337 -s -P http://localhost:1337/index.html? dist"
79
- },
80
54
  "commitlint": {
81
55
  "extends": [
82
56
  "@commitlint/config-conventional"
@@ -110,6 +84,7 @@
110
84
  "@sanity/icons": "^3.4.0",
111
85
  "csstype": "^3.1.3",
112
86
  "framer-motion": "11.0.8",
87
+ "react-compiler-runtime": "19.0.0-beta-6fc168f-20241025",
113
88
  "react-refractor": "^2.2.0",
114
89
  "use-effect-event": "^1.0.2"
115
90
  },
@@ -198,19 +173,37 @@
198
173
  "react-is": "^18",
199
174
  "styled-components": "^5.2 || ^6"
200
175
  },
201
- "packageManager": "pnpm@9.12.3",
202
176
  "engines": {
203
177
  "node": ">=14.0.0"
204
178
  },
205
179
  "publishConfig": {
206
180
  "access": "public"
207
181
  },
208
- "pnpm": {
209
- "overrides": {
210
- "conventional-changelog-conventionalcommits": ">= 8.0.0"
211
- }
212
- },
213
182
  "esm.sh": {
214
183
  "bundle": false
184
+ },
185
+ "scripts": {
186
+ "build": "run-s clean pkg:build pkg:check figma:pkg:build",
187
+ "clean": "rimraf .workshop dist",
188
+ "commit": "cz",
189
+ "cypress:dev": "run-p dev cypress:open",
190
+ "cypress:open": "cypress open",
191
+ "cypress:run": "cypress run",
192
+ "dev": "run-p storybook:dev workshop:dev",
193
+ "figma:pkg:build": "cd figma && pnpm build",
194
+ "format": "prettier --write --cache --ignore-unknown .",
195
+ "lint": "eslint . --ext .cjs,.js,.jsx,.mjs,.ts,.tsx",
196
+ "pkg:build": "pkg build --strict",
197
+ "pkg:check": "pkg --strict",
198
+ "release": "semantic-release",
199
+ "storybook:build": "storybook build",
200
+ "storybook:dev": "storybook dev -p 6006",
201
+ "test": "jest",
202
+ "test:browser": "start-server-and-test 'run-s workshop:build workshop:start' http://localhost:1337 'run-s cypress:run'",
203
+ "ts:check": "tsc",
204
+ "watch": "pkg watch --strict",
205
+ "workshop:build": "workshop build",
206
+ "workshop:dev": "workshop dev",
207
+ "workshop:start": "http-server -a localhost -c-0 -p 1337 -s -P http://localhost:1337/index.html? dist"
215
208
  }
216
- }
209
+ }
@@ -17,6 +17,7 @@ import {
17
17
  useMemo,
18
18
  useReducer,
19
19
  useRef,
20
+ useState,
20
21
  } from 'react'
21
22
  import {EMPTY_ARRAY, EMPTY_RECORD} from '../../constants'
22
23
  import {_hasFocus, _raf, focusFirstDescendant} from '../../helpers'
@@ -184,21 +185,22 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
184
185
  typeof filterOptionProp === 'function' ? filterOptionProp : DEFAULT_FILTER_OPTION
185
186
 
186
187
  // Element refs
187
- const rootElementRef = useRef<HTMLDivElement | null>(null)
188
- const resultsPopoverElementRef = useRef<HTMLDivElement | null>(null)
189
- const inputElementRef = useRef<HTMLInputElement | null>(null)
188
+ const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
189
+ const [resultsPopoverElement, setResultsPopoverElement] = useState<HTMLDivElement | null>(null)
190
+ const [inputElement, setInputElement] = useState<HTMLInputElement | null>(null)
190
191
  const listBoxElementRef = useRef<HTMLDivElement | null>(null)
191
192
 
192
193
  // Value refs
193
194
  const listFocusedRef = useRef(false)
194
195
  const valueRef = useRef(value)
195
196
  const valuePropRef = useRef(valueProp)
196
- const popoverMouseWithinRef = useRef(false)
197
+ const [popoverMouseWithin, setPopoverMouseWithin] = useState(false)
197
198
 
198
199
  // Forward ref to parent
199
200
  useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(
200
201
  forwardedRef,
201
- () => inputElementRef.current,
202
+ () => inputElement,
203
+ [inputElement],
202
204
  )
203
205
 
204
206
  const listBoxId = `${id}-listbox`
@@ -222,13 +224,13 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
222
224
  // NOTE: This is a workaround for a bug that may happen in Chrome (clicking the scrollbar
223
225
  // closes the results in certain situations):
224
226
  // - Do not handle blur if the mouse is within the popover
225
- if (popoverMouseWithinRef.current) {
227
+ if (popoverMouseWithin) {
226
228
  return
227
229
  }
228
230
 
229
231
  const elements: HTMLElement[] = (relatedElements || []).concat(
230
- rootElementRef.current ? [rootElementRef.current] : [],
231
- resultsPopoverElementRef.current ? [resultsPopoverElementRef.current] : [],
232
+ rootElement ? [rootElement] : [],
233
+ resultsPopoverElement ? [resultsPopoverElement] : [],
232
234
  )
233
235
 
234
236
  let focusInside = false
@@ -244,13 +246,20 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
244
246
 
245
247
  if (focusInside === false) {
246
248
  dispatch({type: 'root/blur'})
247
- popoverMouseWithinRef.current = false
249
+ setPopoverMouseWithin(false)
248
250
  if (onQueryChange) onQueryChange(null)
249
251
  if (onBlur) onBlur(event)
250
252
  }
251
253
  }, 0)
252
254
  },
253
- [onBlur, onQueryChange, relatedElements],
255
+ [
256
+ onBlur,
257
+ onQueryChange,
258
+ popoverMouseWithin,
259
+ relatedElements,
260
+ resultsPopoverElement,
261
+ rootElement,
262
+ ],
254
263
  )
255
264
 
256
265
  const handleRootFocus = useCallback((event: FocusEvent<HTMLDivElement>) => {
@@ -269,7 +278,7 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
269
278
  (v: string) => {
270
279
  dispatch({type: 'value/change', value: v})
271
280
 
272
- popoverMouseWithinRef.current = false
281
+ setPopoverMouseWithin(false)
273
282
 
274
283
  if (onSelect) onSelect(v)
275
284
 
@@ -278,9 +287,9 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
278
287
  if (onChange) onChange(v)
279
288
  if (onQueryChange) onQueryChange(null)
280
289
 
281
- inputElementRef.current?.focus()
290
+ inputElement?.focus()
282
291
  },
283
- [onChange, onSelect, onQueryChange],
292
+ [onSelect, onChange, onQueryChange, inputElement],
284
293
  )
285
294
 
286
295
  const handleRootKeyDown = useCallback(
@@ -324,9 +333,9 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
324
333
 
325
334
  if (event.key === 'Escape') {
326
335
  dispatch({type: 'root/escape'})
327
- popoverMouseWithinRef.current = false
336
+ setPopoverMouseWithin(false)
328
337
  if (onQueryChange) onQueryChange(null)
329
- inputElementRef.current?.focus()
338
+ inputElement?.focus()
330
339
 
331
340
  return
332
341
  }
@@ -338,12 +347,12 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
338
347
  (listEl === target || listEl?.contains(target)) &&
339
348
  !AUTOCOMPLETE_LISTBOX_IGNORE_KEYS.includes(event.key)
340
349
  ) {
341
- inputElementRef.current?.focus()
350
+ inputElement?.focus()
342
351
 
343
352
  return
344
353
  }
345
354
  },
346
- [activeValue, filteredOptions, filteredOptionsLen, onQueryChange],
355
+ [activeValue, filteredOptions, filteredOptionsLen, inputElement, onQueryChange],
347
356
  )
348
357
 
349
358
  const handleInputChange = useCallback(
@@ -377,11 +386,11 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
377
386
  )
378
387
 
379
388
  const handlePopoverMouseEnter = useCallback(() => {
380
- popoverMouseWithinRef.current = true
389
+ setPopoverMouseWithin(true)
381
390
  }, [])
382
391
 
383
392
  const handlePopoverMouseLeave = useCallback(() => {
384
- popoverMouseWithinRef.current = false
393
+ setPopoverMouseWithin(false)
385
394
  }, [])
386
395
 
387
396
  const handleClearButtonClick = useCallback(() => {
@@ -389,8 +398,8 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
389
398
  valueRef.current = ''
390
399
  if (onChange) onChange('')
391
400
  if (onQueryChange) onQueryChange(null)
392
- inputElementRef.current?.focus()
393
- }, [onChange, onQueryChange])
401
+ inputElement?.focus()
402
+ }, [inputElement, onChange, onQueryChange])
394
403
 
395
404
  const handleClearButtonFocus = useCallback(() => {
396
405
  dispatch({type: 'input/focus'})
@@ -482,9 +491,9 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
482
491
 
483
492
  if (openButtonProps.onClick) openButtonProps.onClick(event)
484
493
 
485
- _raf(() => inputElementRef.current?.focus())
494
+ _raf(() => inputElement?.focus())
486
495
  },
487
- [openButtonProps, dispatchOpen],
496
+ [dispatchOpen, openButtonProps, inputElement],
488
497
  )
489
498
 
490
499
  const openButtonNode = useMemo(
@@ -554,7 +563,7 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
554
563
  prefix={prefix}
555
564
  radius={radius}
556
565
  readOnly={readOnly}
557
- ref={inputElementRef}
566
+ ref={setInputElement}
558
567
  role="combobox"
559
568
  spellCheck={false}
560
569
  suffix={suffix || openButtonNode}
@@ -566,10 +575,10 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
566
575
  (event: KeyboardEvent<HTMLDivElement>) => {
567
576
  // If the focus is currently in the list, move focus to the input element
568
577
  if (event.key === 'Tab') {
569
- if (listFocused) inputElementRef.current?.focus()
578
+ if (listFocused) inputElement?.focus()
570
579
  }
571
580
  },
572
- [listFocused],
581
+ [inputElement, listFocused],
573
582
  )
574
583
 
575
584
  const content = useMemo(() => {
@@ -635,11 +644,11 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
635
644
  {
636
645
  content,
637
646
  hidden: !expanded,
638
- inputElement: inputElementRef.current,
647
+ inputElement,
639
648
  onMouseEnter: handlePopoverMouseEnter,
640
649
  onMouseLeave: handlePopoverMouseLeave,
641
650
  },
642
- resultsPopoverElementRef,
651
+ {current: resultsPopoverElement},
643
652
  )
644
653
  }
645
654
 
@@ -661,8 +670,8 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
661
670
  placement={AUTOCOMPLETE_POPOVER_PLACEMENT}
662
671
  portal
663
672
  radius={radius}
664
- ref={resultsPopoverElementRef}
665
- referenceElement={inputElementRef.current}
673
+ ref={setResultsPopoverElement}
674
+ referenceElement={inputElement}
666
675
  {...popover}
667
676
  />
668
677
  )
@@ -672,9 +681,11 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
672
681
  filteredOptionsLen,
673
682
  handlePopoverMouseEnter,
674
683
  handlePopoverMouseLeave,
684
+ inputElement,
675
685
  popover,
676
686
  radius,
677
687
  renderPopover,
688
+ resultsPopoverElement,
678
689
  ])
679
690
 
680
691
  return (
@@ -683,7 +694,7 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
683
694
  onBlur={handleRootBlur}
684
695
  onFocus={handleRootFocus}
685
696
  onKeyDown={handleRootKeyDown}
686
- ref={rootElementRef}
697
+ ref={setRootElement}
687
698
  >
688
699
  {input}
689
700
  {results}
@@ -305,13 +305,15 @@ export const Dialog = forwardRef(function Dialog(
305
305
  onFocus,
306
306
  padding: paddingProp = 3,
307
307
  portal: portalProp,
308
- position: positionProp = dialog.position || 'fixed',
308
+ position: _positionProp,
309
309
  scheme,
310
310
  width: widthProp = 0,
311
- zOffset: zOffsetProp = dialog.zOffset || layer.dialog.zOffset,
311
+ zOffset: _zOffsetProp,
312
312
  animate: _animate = false,
313
313
  ...restProps
314
314
  } = props
315
+ const positionProp = _positionProp ?? (dialog.position || 'fixed')
316
+ const zOffsetProp = _zOffsetProp ?? (dialog.zOffset || layer.dialog.zOffset)
315
317
  const prefersReducedMotion = usePrefersReducedMotion()
316
318
  const animate = prefersReducedMotion ? false : _animate
317
319
  const portal = usePortal()
@@ -57,10 +57,12 @@ export const Menu = forwardRef(function Menu(
57
57
  originElement,
58
58
  padding = 1,
59
59
  registerElement,
60
- shouldFocus = (props.focusFirst && 'first') || (props.focusLast && 'last') || null,
60
+ shouldFocus: _shouldFocus,
61
61
  space = 1,
62
62
  ...restProps
63
63
  } = props
64
+ const shouldFocus =
65
+ _shouldFocus ?? ((props.focusFirst && 'first') || (props.focusLast && 'last') || null)
64
66
 
65
67
  const ref = useRef<HTMLDivElement | null>(null)
66
68
 
@@ -53,9 +53,10 @@ export function MenuGroup(
53
53
  onClickOutside,
54
54
  onEscape,
55
55
  onItemClick,
56
- onItemMouseEnter = menu.onMouseEnter,
56
+ onItemMouseEnter: _onItemMouseEnter,
57
57
  registerElement,
58
58
  } = menu
59
+ const onItemMouseEnter = _onItemMouseEnter ?? menu.onMouseEnter
59
60
  const [rootElement, setRootElement] = useState<HTMLButtonElement | HTMLDivElement | null>(null)
60
61
  const [open, setOpen] = useState(false)
61
62
  const [shouldFocus, setShouldFocus] = useState<'first' | 'last' | null>(null)
@@ -72,9 +72,11 @@ export const MenuItem = forwardRef(function MenuItem(
72
72
  activeElement,
73
73
  mount,
74
74
  onItemClick,
75
- onItemMouseEnter = menu.onMouseEnter,
76
- onItemMouseLeave = menu.onMouseLeave,
75
+ onItemMouseEnter: _onItemMouseEnter,
76
+ onItemMouseLeave: _onItemMouseLeave,
77
77
  } = menu
78
+ const onItemMouseEnter = _onItemMouseEnter ?? menu.onMouseEnter
79
+ const onItemMouseLeave = _onItemMouseLeave ?? menu.onMouseLeave
78
80
  const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
79
81
  const active = Boolean(activeElement) && activeElement === rootElement
80
82
  const ref = useRef<HTMLDivElement | null>(null)
@@ -36,31 +36,28 @@ export function useMenuController(props: {
36
36
  activeIndexRef.current = nextActiveIndex
37
37
  }, [])
38
38
 
39
- const mount = useCallback(
40
- (element: HTMLElement | null, selected?: boolean): (() => void) => {
41
- if (!element) return () => undefined
39
+ const mount = (element: HTMLElement | null, selected?: boolean): (() => void) => {
40
+ if (!element) return () => undefined
42
41
 
43
- if (elementsRef.current.indexOf(element) === -1) {
44
- elementsRef.current.push(element)
45
- _sortElements(rootElementRef.current, elementsRef.current)
46
- }
42
+ if (elementsRef.current.indexOf(element) === -1) {
43
+ elementsRef.current.push(element)
44
+ _sortElements(rootElementRef.current, elementsRef.current)
45
+ }
47
46
 
48
- if (selected) {
49
- const selectedIndex = elementsRef.current.indexOf(element)
47
+ if (selected) {
48
+ const selectedIndex = elementsRef.current.indexOf(element)
50
49
 
51
- setActiveIndex(selectedIndex)
52
- }
50
+ setActiveIndex(selectedIndex)
51
+ }
53
52
 
54
- return () => {
55
- const idx = elementsRef.current.indexOf(element)
53
+ return () => {
54
+ const idx = elementsRef.current.indexOf(element)
56
55
 
57
- if (idx > -1) {
58
- elementsRef.current.splice(idx, 1)
59
- }
56
+ if (idx > -1) {
57
+ elementsRef.current.splice(idx, 1)
60
58
  }
61
- },
62
- [rootElementRef, setActiveIndex],
63
- )
59
+ }
60
+ }
64
61
 
65
62
  const handleKeyDown = useCallback(
66
63
  (event: React.KeyboardEvent<HTMLDivElement>) => {
@@ -170,7 +167,7 @@ export function useMenuController(props: {
170
167
  [setActiveIndex],
171
168
  )
172
169
 
173
- const handleItemMouseLeave = useCallback(() => {
170
+ const handleItemMouseLeave = () => {
174
171
  // Set the active index to -2 to deactivate all menu items
175
172
  // when the user moves the mouse away from the menu item.
176
173
  // We avoid using -1 because it would focus the first menu item,
@@ -178,7 +175,7 @@ export function useMenuController(props: {
178
175
  // between two menu items or a menu divider.
179
176
  setActiveIndex(-2)
180
177
  rootElementRef.current?.focus()
181
- }, [rootElementRef, setActiveIndex])
178
+ }
182
179
 
183
180
  // Set focus on the currently active element
184
181
  useEffect(() => {
@@ -1,6 +1,6 @@
1
1
  import {ToggleArrowRightIcon} from '@sanity/icons'
2
2
  import {ThemeFontWeightKey} from '@sanity/ui/theme'
3
- import {memo, useCallback, useEffect, useId, useMemo, useRef} from 'react'
3
+ import {memo, useCallback, useEffect, useId, useMemo, useRef, useState} from 'react'
4
4
  import {styled} from 'styled-components'
5
5
  import {Box, BoxProps, Flex, Text} from '../../primitives'
6
6
  import {
@@ -64,7 +64,7 @@ export const TreeItem = memo(function TreeItem(
64
64
  weight,
65
65
  ...restProps
66
66
  } = props
67
- const rootRef = useRef<HTMLLIElement | null>(null)
67
+ const [rootElement, setRootElement] = useState<HTMLLIElement | null>(null)
68
68
  const treeitemRef = useRef<HTMLDivElement | null>(null)
69
69
  const tree = useTree()
70
70
  const {path, registerItem, setExpanded, setFocusedElement} = tree
@@ -73,9 +73,9 @@ export const TreeItem = memo(function TreeItem(
73
73
  const itemPath = useMemo(() => path.concat([id || '']), [id, path])
74
74
  const itemKey = itemPath.join('/')
75
75
  const itemState = tree.state[itemKey]
76
- const focused = tree.focusedElement === rootRef.current
76
+ const focused = tree.focusedElement === rootElement
77
77
  const expanded = itemState?.expanded === undefined ? expandedProp : itemState?.expanded || false
78
- const tabIndex = tree.focusedElement && tree.focusedElement === rootRef.current ? 0 : -1
78
+ const tabIndex = tree.focusedElement && tree.focusedElement === rootElement ? 0 : -1
79
79
  const contextValue = useMemo(
80
80
  () => ({...tree, level: tree.level + 1, path: itemPath}),
81
81
  [itemPath, tree],
@@ -94,28 +94,28 @@ export const TreeItem = memo(function TreeItem(
94
94
  ) {
95
95
  event.stopPropagation()
96
96
  setExpanded(itemKey, !expanded)
97
- setFocusedElement(rootRef.current)
97
+ setFocusedElement(rootElement)
98
98
  }
99
99
  },
100
- [expanded, itemKey, onClick, setExpanded, setFocusedElement],
100
+ [expanded, itemKey, onClick, rootElement, setExpanded, setFocusedElement],
101
101
  )
102
102
 
103
103
  const handleKeyDown = useCallback(
104
104
  (event: React.KeyboardEvent<HTMLElement>) => {
105
105
  if (focused && event.key === 'Enter') {
106
- const el = treeitemRef.current || rootRef.current
106
+ const el = treeitemRef.current || rootElement
107
107
 
108
108
  el?.click()
109
109
  }
110
110
  },
111
- [focused],
111
+ [focused, rootElement],
112
112
  )
113
113
 
114
114
  useEffect(() => {
115
- if (!rootRef.current) return
115
+ if (!rootElement) return
116
116
 
117
- return registerItem(rootRef.current, itemPath.join('/'), expanded, selected)
118
- }, [expanded, itemPath, registerItem, selected])
117
+ return registerItem(rootElement, itemPath.join('/'), expanded, selected)
118
+ }, [expanded, itemPath, registerItem, rootElement, selected])
119
119
 
120
120
  const content = (
121
121
  <Flex padding={padding}>
@@ -154,7 +154,7 @@ export const TreeItem = memo(function TreeItem(
154
154
  data-ui="TreeItem"
155
155
  {...restProps}
156
156
  onClick={handleClick}
157
- ref={rootRef}
157
+ ref={setRootElement}
158
158
  role="none"
159
159
  >
160
160
  <TreeItemBox
@@ -187,7 +187,7 @@ export const TreeItem = memo(function TreeItem(
187
187
  aria-expanded={expanded}
188
188
  onClick={handleClick}
189
189
  onKeyDown={handleKeyDown}
190
- ref={rootRef}
190
+ ref={setRootElement}
191
191
  role="treeitem"
192
192
  tabIndex={tabIndex}
193
193
  >
@@ -1,4 +1,4 @@
1
- import {useMemo} from 'react'
1
+ import {useState} from 'react'
2
2
  import {_getArrayProp} from '../styles'
3
3
 
4
4
  /** @beta */
@@ -12,14 +12,19 @@ export function useArrayProp<T extends ArrayPropPrimitive = ArrayPropPrimitive>(
12
12
  val: T | T[] | undefined,
13
13
  defaultVal?: T[],
14
14
  ): T[] {
15
- // JSON.stringify is fast, but it's not faster than useMemo's referencial equality check
16
- const __perf_hash__ = useMemo(() => JSON.stringify(val ?? defaultVal), [defaultVal, val])
15
+ const [[cachedVal, cachedHash], setCache] = useState<[T[], string]>(() => [
16
+ _getArrayProp(val, defaultVal),
17
+ JSON.stringify(val ?? defaultVal),
18
+ ])
17
19
 
18
- return useMemo(
19
- () => _getArrayProp(val, defaultVal),
20
+ const hash = JSON.stringify(val ?? defaultVal)
20
21
 
21
- // Improve performance: Keep object identify for a given hash of the value
22
- // eslint-disable-next-line react-hooks/exhaustive-deps
23
- [__perf_hash__],
24
- )
22
+ if (hash !== cachedHash) {
23
+ // If the cached hash has changed, update the cache right away.
24
+ // Calling setState during render is fine in this case, and preferred over a useEffect loop
25
+ // https://19.react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
26
+ setCache([_getArrayProp(val, defaultVal), hash])
27
+ }
28
+
29
+ return cachedVal
25
30
  }
@@ -11,34 +11,24 @@ export function useMatchMedia(
11
11
  mediaQueryString: `(${string})`,
12
12
  getServerSnapshot?: () => boolean,
13
13
  ): boolean {
14
+ /**
15
+ * `subscribe` and `getSnapshot` are only called on the client and both need access to the same `matchMedia` instance
16
+ * we don't want to eagerly instantiate it to ensure it's only created when actually used
17
+ */
18
+ const cachedMatchMedia = useMemo(
19
+ () => (typeof window === 'undefined' ? null : window.matchMedia(mediaQueryString)),
20
+ [mediaQueryString],
21
+ )
14
22
  const {subscribe, getSnapshot} = useMemo(() => {
15
- /**
16
- * `subscribe` and `getSnapshot` are only called on the client and both need access to the same `matchMedia` instance
17
- * we don't want to eagerly instantiate it to ensure it's only created when actually used
18
- */
19
- let MEDIA_QUERY_CACHE: MediaQueryList | undefined
20
-
21
- const getMatchMedia = (): MediaQueryList => {
22
- if (!MEDIA_QUERY_CACHE) {
23
- // As this function is only called during `subscribe` and `getSnapshot`, we can assume that the
24
- // the `window` global is available and we're in a browser environment
25
- MEDIA_QUERY_CACHE = window.matchMedia(mediaQueryString)
26
- }
27
-
28
- return MEDIA_QUERY_CACHE
29
- }
30
-
31
23
  return {
32
24
  subscribe: (onStoreChange: () => void): (() => void) => {
33
- const matchMedia = getMatchMedia()
34
-
35
- matchMedia.addEventListener('change', onStoreChange)
25
+ cachedMatchMedia!.addEventListener('change', onStoreChange)
36
26
 
37
- return () => matchMedia.removeEventListener('change', onStoreChange)
27
+ return () => cachedMatchMedia!.removeEventListener('change', onStoreChange)
38
28
  },
39
- getSnapshot: () => getMatchMedia().matches,
29
+ getSnapshot: () => cachedMatchMedia!.matches,
40
30
  }
41
- }, [mediaQueryString])
31
+ }, [cachedMatchMedia])
42
32
 
43
33
  useDebugValue(mediaQueryString)
44
34
 
@@ -1,5 +1,5 @@
1
1
  import {getTheme_v2} from '@sanity/ui/theme'
2
- import {Children, cloneElement, forwardRef, isValidElement, useMemo} from 'react'
2
+ import {Children, cloneElement, forwardRef, isValidElement} from 'react'
3
3
  import {styled, css} from 'styled-components'
4
4
  import {EMPTY_RECORD} from '../../constants'
5
5
  import {useArrayProp} from '../../hooks'
@@ -65,10 +65,7 @@ export const AvatarStack = forwardRef(function AvatarStack(
65
65
  size: sizeProp = 1,
66
66
  ...restProps
67
67
  } = props
68
- const children = useMemo<React.ReactElement[]>(
69
- () => Children.toArray(childrenProp).filter(isValidElement),
70
- [childrenProp],
71
- )
68
+ const children: React.ReactElement[] = Children.toArray(childrenProp).filter(isValidElement)
72
69
  const maxLength = Math.max(maxLengthProp, 0)
73
70
  const size = useArrayProp(sizeProp)
74
71