@sanity/ui 4.0.0-static.31 → 4.0.0-static.32

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 (47) hide show
  1. package/dist/index.d.ts +8 -1
  2. package/dist/index.js +1076 -510
  3. package/dist/index.js.map +1 -1
  4. package/package.json +9 -10
  5. package/src/core/components/autocomplete/Autocomplete.tsx +80 -48
  6. package/src/core/components/breadcrumbs/Breadcrumbs.tsx +74 -41
  7. package/src/core/components/dialog/DialogContext.ts +1 -1
  8. package/src/core/components/dialog/__workshop__/Panes.tsx +5 -21
  9. package/src/core/components/dialog/__workshop__/styles.css.ts +18 -0
  10. package/src/core/components/menu/Menu.test.tsx +2 -6
  11. package/src/core/components/menu/Menu.tsx +13 -14
  12. package/src/core/components/menu/MenuContext.ts +2 -3
  13. package/src/core/components/menu/MenuGroup.tsx +2 -0
  14. package/src/core/components/menu/useMenu.ts +2 -2
  15. package/src/core/components/menu/useMenuController.ts +5 -5
  16. package/src/core/components/toast/ToastContext.ts +1 -1
  17. package/src/core/components/tree/Tree.tsx +1 -0
  18. package/src/core/components/tree/TreeContext.ts +1 -1
  19. package/src/core/components/tree/TreeItem.tsx +17 -14
  20. package/src/core/components/virtualList/VirtualList.tsx +86 -44
  21. package/src/core/hooks/useGlobalKeyDown.ts +11 -4
  22. package/src/core/lib/createGlobalScopedContext.ts +2 -2
  23. package/src/core/primitives/avatar/Avatar.tsx +1 -0
  24. package/src/core/primitives/button/Button.tsx +2 -1
  25. package/src/core/primitives/button/__workshop__/SanityUploadButton.tsx +10 -25
  26. package/src/core/primitives/button/__workshop__/Styled1.tsx +2 -8
  27. package/src/core/primitives/button/__workshop__/index.ts +0 -5
  28. package/src/core/primitives/button/__workshop__/styles.css.ts +30 -0
  29. package/src/core/primitives/card/CardContext.ts +1 -1
  30. package/src/core/primitives/card/__workshop__/Styled.tsx +2 -5
  31. package/src/core/primitives/flex/__workshop__/Example.tsx +7 -15
  32. package/src/core/primitives/flex/__workshop__/styles.css.ts +12 -0
  33. package/src/core/primitives/inline/Inline.tsx +1 -0
  34. package/src/core/primitives/layer/LayerContext.ts +1 -1
  35. package/src/core/primitives/popover/Popover.tsx +146 -130
  36. package/src/core/primitives/popover/floating-ui/size.ts +17 -16
  37. package/src/core/primitives/skeleton/Skeleton.tsx +2 -2
  38. package/src/core/primitives/text/__workshop__/Colored.tsx +5 -10
  39. package/src/core/primitives/tooltip/Tooltip.tsx +69 -41
  40. package/src/core/primitives/tooltip/tooltipDelayGroup/TooltipDelayGroupContext.tsx +1 -1
  41. package/src/core/utils/boundaryElement/BoundaryElementContext.ts +1 -1
  42. package/src/core/utils/elementQuery/__workshop__/CustomMedia.tsx +3 -25
  43. package/src/core/utils/elementQuery/__workshop__/styles.css.ts +31 -0
  44. package/src/core/utils/portal/Portal.test.tsx +6 -6
  45. package/src/core/utils/portal/PortalContext.ts +1 -1
  46. package/src/core/utils/portal/PortalProvider.tsx +5 -37
  47. package/src/core/primitives/button/__workshop__/Styled2.tsx +0 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ui",
3
- "version": "4.0.0-static.31",
3
+ "version": "4.0.0-static.32",
4
4
  "keywords": [
5
5
  "react",
6
6
  "ui",
@@ -130,9 +130,9 @@
130
130
  "@babel/preset-typescript": "^7.27.1",
131
131
  "@commitlint/cli": "^19.8.1",
132
132
  "@commitlint/config-conventional": "^19.8.1",
133
- "@eslint/js": "^9.35.0",
133
+ "@eslint/js": "^9.36.0",
134
134
  "@sanity/browserslist-config": "^1.0.5",
135
- "@sanity/pkg-utils": "^8.1.14",
135
+ "@sanity/pkg-utils": "^8.1.16",
136
136
  "@sanity/prettier-config": "^2.0.1",
137
137
  "@sanity/semantic-release-preset": "^5.0.0",
138
138
  "@sanity/ui-workshop": "4.0.0-static.11",
@@ -156,16 +156,16 @@
156
156
  "cypress-real-events": "^1.14.0",
157
157
  "cz-conventional-changelog": "^3.3.0",
158
158
  "dotenv-flow": "^4.1.0",
159
- "eslint": "^9.35.0",
159
+ "eslint": "^9.36.0",
160
160
  "eslint-config-prettier": "^10.1.8",
161
- "eslint-formatter-compact": "^8.40.0",
161
+ "eslint-formatter-gha": "^1.6.0",
162
162
  "eslint-plugin-boundaries": "^5.0.1",
163
163
  "eslint-plugin-import": "^2.32.0",
164
164
  "eslint-plugin-jsx-a11y": "^6.10.2",
165
165
  "eslint-plugin-react": "^7.37.5",
166
- "eslint-plugin-react-compiler": "19.1.0-rc.2",
167
- "eslint-plugin-react-hooks": "0.0.0-experimental-66f09bd0-20250806",
168
- "eslint-plugin-react-refresh": "^0.4.20",
166
+ "eslint-plugin-react-hooks": "6.0.0-rc.2",
167
+ "eslint-plugin-react-hooks-with-use-effect-event": "npm:eslint-plugin-react-hooks@0.0.0-experimental-83c88ad4-20250923",
168
+ "eslint-plugin-react-refresh": "^0.4.21",
169
169
  "eslint-plugin-simple-import-sort": "^12.1.1",
170
170
  "eslint-plugin-storybook": "^9.1.7",
171
171
  "globals": "^16.3.0",
@@ -183,10 +183,9 @@
183
183
  "rimraf": "^5.0.10",
184
184
  "semantic-release": "24.2.7",
185
185
  "start-server-and-test": "^2.1.1",
186
- "styled-components": "^6.1.19",
187
186
  "tsconfig-paths": "^4.2.0",
188
187
  "typescript": "5.9.2",
189
- "typescript-eslint": "^8.44.0",
188
+ "typescript-eslint": "^8.44.1",
190
189
  "vite": "^7.1.6",
191
190
  "vite-tsconfig-paths": "^5.1.4",
192
191
  "vitest": "^3.2.4",
@@ -16,6 +16,7 @@ import {
16
16
  useMemo,
17
17
  useReducer,
18
18
  useRef,
19
+ useState,
19
20
  } from 'react'
20
21
 
21
22
  import {EMPTY_ARRAY, EMPTY_RECORD} from '../../constants'
@@ -177,6 +178,8 @@ export function Autocomplete<
177
178
  const resultsPopoverElementRef = useRef<HTMLDivElement | null>(null)
178
179
  const inputElementRef = useRef<HTMLInputElement | null>(null)
179
180
  const listBoxElementRef = useRef<HTMLUListElement | null>(null)
181
+ // Element refs that need to be accessed during render
182
+ const [inputElement, setInputElement] = useState<HTMLInputElement | null>(null)
180
183
 
181
184
  // Value refs
182
185
  const listFocusedRef = useRef(false)
@@ -184,10 +187,17 @@ export function Autocomplete<
184
187
  const valuePropRef = useRef(valueProp)
185
188
  const popoverMouseWithinRef = useRef(false)
186
189
 
187
- // Forward ref to parent
190
+ // Forward inputElement state to inputElementRef
191
+ useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(
192
+ inputElementRef,
193
+ () => inputElement,
194
+ [inputElement],
195
+ )
196
+ // Forward inputElement to parent
188
197
  useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(
189
198
  forwardedRef,
190
- () => inputElementRef.current,
199
+ () => inputElement,
200
+ [inputElement],
191
201
  )
192
202
 
193
203
  const listBoxId = `${id}-listbox`
@@ -525,41 +535,6 @@ export function Autocomplete<
525
535
  return query
526
536
  }, [currentOption, query, renderValue, value])
527
537
 
528
- const input = (
529
- <TextInput
530
- {...rest}
531
- aria-activedescendant={activeItemId}
532
- aria-autocomplete="list"
533
- aria-expanded={expanded}
534
- aria-owns={listBoxId}
535
- autoCapitalize="off"
536
- autoComplete="off"
537
- autoCorrect="off"
538
- as={as}
539
- border={border}
540
- clearButton={clearButton}
541
- customValidity={customValidity}
542
- disabled={disabled}
543
- fontSize={fontSize}
544
- icon={icon}
545
- iconRight={loading && AnimatedSpinnerIcon}
546
- id={id}
547
- inputMode="search"
548
- onChange={handleInputChange}
549
- onClear={handleClearButtonClick}
550
- onFocus={handleInputFocus}
551
- padding={padding}
552
- prefix={prefix}
553
- radius={radius}
554
- readOnly={readOnly}
555
- ref={inputElementRef}
556
- role="combobox"
557
- spellCheck={false}
558
- suffix={suffix || openButtonNode}
559
- value={inputValue}
560
- />
561
- )
562
-
563
538
  const handleListBoxKeyDown = useCallback(
564
539
  (event: KeyboardEvent<HTMLDivElement>) => {
565
540
  // If the focus is currently in the list, move focus to the input element
@@ -629,15 +604,16 @@ export function Autocomplete<
629
604
 
630
605
  const results = useMemo(() => {
631
606
  if (renderPopover) {
632
- return renderPopover(
633
- {
634
- content,
635
- hidden: !expanded,
636
- inputElement: inputElementRef.current,
637
- onMouseEnter: handlePopoverMouseEnter,
638
- onMouseLeave: handlePopoverMouseLeave,
639
- },
640
- resultsPopoverElementRef,
607
+ return (
608
+ <RenderPopover
609
+ content={content}
610
+ hidden={!expanded}
611
+ inputElement={inputElement}
612
+ onMouseEnter={handlePopoverMouseEnter}
613
+ onMouseLeave={handlePopoverMouseLeave}
614
+ resultsPopoverElementRef={resultsPopoverElementRef}
615
+ renderPopover={renderPopover}
616
+ />
641
617
  )
642
618
  }
643
619
 
@@ -660,7 +636,7 @@ export function Autocomplete<
660
636
  portal
661
637
  radius={3}
662
638
  ref={resultsPopoverElementRef}
663
- referenceElement={inputElementRef.current}
639
+ referenceElement={inputElement}
664
640
  {...popover}
665
641
  />
666
642
  )
@@ -670,6 +646,7 @@ export function Autocomplete<
670
646
  filteredOptionsLen,
671
647
  handlePopoverMouseEnter,
672
648
  handlePopoverMouseLeave,
649
+ inputElement,
673
650
  popover,
674
651
  renderPopover,
675
652
  ])
@@ -683,8 +660,63 @@ export function Autocomplete<
683
660
  onKeyDown={handleRootKeyDown}
684
661
  ref={rootElementRef}
685
662
  >
686
- {input}
663
+ <TextInput
664
+ {...rest}
665
+ aria-activedescendant={activeItemId}
666
+ aria-autocomplete="list"
667
+ aria-expanded={expanded}
668
+ aria-owns={listBoxId}
669
+ autoCapitalize="off"
670
+ autoComplete="off"
671
+ autoCorrect="off"
672
+ as={as}
673
+ border={border}
674
+ clearButton={clearButton}
675
+ customValidity={customValidity}
676
+ disabled={disabled}
677
+ fontSize={fontSize}
678
+ icon={icon}
679
+ iconRight={loading && AnimatedSpinnerIcon}
680
+ id={id}
681
+ inputMode="search"
682
+ onChange={handleInputChange}
683
+ onClear={handleClearButtonClick}
684
+ onFocus={handleInputFocus}
685
+ padding={padding}
686
+ prefix={prefix}
687
+ radius={radius}
688
+ readOnly={readOnly}
689
+ ref={setInputElement}
690
+ role="combobox"
691
+ spellCheck={false}
692
+ suffix={suffix || openButtonNode}
693
+ value={inputValue}
694
+ />
687
695
  {results}
688
696
  </div>
689
697
  )
690
698
  }
699
+
700
+ function RenderPopover({
701
+ renderPopover,
702
+ content,
703
+ hidden,
704
+ inputElement,
705
+ onMouseEnter,
706
+ onMouseLeave,
707
+ resultsPopoverElementRef,
708
+ }: {
709
+ renderPopover: Exclude<AutocompleteProps['renderPopover'], undefined>
710
+ resultsPopoverElementRef: Parameters<Exclude<AutocompleteProps['renderPopover'], undefined>>[1]
711
+ } & Parameters<Exclude<AutocompleteProps['renderPopover'], undefined>>[0]) {
712
+ return renderPopover(
713
+ {
714
+ content,
715
+ hidden,
716
+ inputElement,
717
+ onMouseEnter,
718
+ onMouseLeave,
719
+ },
720
+ resultsPopoverElementRef,
721
+ )
722
+ }
@@ -48,8 +48,8 @@ export function Breadcrumbs<E extends BreadcrumbsElementType = typeof DEFAULT_BR
48
48
  className,
49
49
  expandButton: expandButtonProps,
50
50
  gap = 2,
51
- gapX,
52
- gapY,
51
+ gapX = gap,
52
+ gapY = gap,
53
53
  maxLength,
54
54
  separator,
55
55
  ...rest
@@ -66,45 +66,17 @@ export function Breadcrumbs<E extends BreadcrumbsElementType = typeof DEFAULT_BR
66
66
 
67
67
  const rawItems = useMemo(() => Children.toArray(children).filter(isValidElement), [children])
68
68
 
69
- const items = useMemo(() => {
70
- const len = rawItems.length
71
-
72
- if (maxLength && len > maxLength) {
73
- const beforeLength = Math.ceil(maxLength / 2)
74
- const afterLength = Math.floor(maxLength / 2)
75
-
76
- return [
77
- ...rawItems.slice(0, beforeLength - 1),
78
- <Popover
79
- constrainSize
80
- content={
81
- <Stack as="ol" gap={gapY} overflow="auto">
82
- {rawItems.slice(beforeLength - 1, len - afterLength)}
83
- </Stack>
84
- }
85
- key="button"
86
- open={open}
87
- padding={2}
88
- placement="top"
89
- portal
90
- ref={popoverElementRef}
91
- >
92
- <Button
93
- mode="bleed"
94
- padding={0}
95
- text="…"
96
- {...expandButtonProps}
97
- onClick={open ? collapse : expand}
98
- ref={expandElementRef}
99
- selected={open}
100
- />
101
- </Popover>,
102
- ...rawItems.slice(len - afterLength),
103
- ]
104
- }
105
-
106
- return rawItems
107
- }, [collapse, expand, expandButtonProps, gapY, maxLength, open, rawItems])
69
+ const items = useItems({
70
+ collapse,
71
+ expand,
72
+ expandElementRef,
73
+ maxLength,
74
+ open,
75
+ popoverElementRef,
76
+ rawItems,
77
+ gapY,
78
+ expandButtonProps,
79
+ })
108
80
 
109
81
  return (
110
82
  <Flex
@@ -129,3 +101,64 @@ export function Breadcrumbs<E extends BreadcrumbsElementType = typeof DEFAULT_BR
129
101
  </Flex>
130
102
  )
131
103
  }
104
+
105
+ function useItems({
106
+ collapse,
107
+ expand,
108
+ expandElementRef,
109
+ maxLength,
110
+ open,
111
+ popoverElementRef,
112
+ rawItems,
113
+ gapY,
114
+ expandButtonProps,
115
+ }: {
116
+ collapse: () => void
117
+ expand: () => void
118
+ expandElementRef: React.RefObject<HTMLButtonElement | null>
119
+ maxLength: number | undefined
120
+ open: boolean
121
+ popoverElementRef: React.RefObject<HTMLDivElement | null>
122
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
123
+ rawItems: React.ReactElement<unknown, string | React.JSXElementConstructor<any>>[]
124
+ gapY: NonNullable<GapStyleProps['gapY']>
125
+ expandButtonProps: BreadcrumbsOwnProps['expandButton']
126
+ }): React.JSX.Element[] {
127
+ const len = rawItems.length
128
+
129
+ if (maxLength && len > maxLength) {
130
+ const beforeLength = Math.ceil(maxLength / 2)
131
+ const afterLength = Math.floor(maxLength / 2)
132
+
133
+ return [
134
+ ...rawItems.slice(0, beforeLength - 1),
135
+ <Popover
136
+ constrainSize
137
+ content={
138
+ <Stack as="ol" gap={gapY} overflow="auto">
139
+ {rawItems.slice(beforeLength - 1, len - afterLength)}
140
+ </Stack>
141
+ }
142
+ key="button"
143
+ open={open}
144
+ padding={2}
145
+ placement="top"
146
+ portal
147
+ ref={popoverElementRef}
148
+ >
149
+ <Button
150
+ mode="bleed"
151
+ padding={0}
152
+ text="…"
153
+ {...expandButtonProps}
154
+ onClick={open ? collapse : expand}
155
+ ref={expandElementRef}
156
+ selected={open}
157
+ />
158
+ </Popover>,
159
+ ...rawItems.slice(len - afterLength),
160
+ ]
161
+ }
162
+
163
+ return rawItems
164
+ }
@@ -18,4 +18,4 @@ export interface DialogContextValue {
18
18
  * @internal
19
19
  */
20
20
  export const DialogContext: Context<DialogContextValue> =
21
- createGlobalScopedContext<DialogContextValue>('@sanity/ui/v3/dialog', {version: 0.0})
21
+ createGlobalScopedContext<DialogContextValue>('@sanity/ui/v4/dialog', {version: 0.0})
@@ -11,7 +11,8 @@ import {
11
11
  PortalProvider,
12
12
  } from '@sanity/ui'
13
13
  import {useCallback, useState} from 'react'
14
- import {styled} from 'styled-components'
14
+
15
+ import {panePortal, paneRoot} from './styles.css'
15
16
 
16
17
  export default function PanesStory(): React.JSX.Element {
17
18
  return (
@@ -22,23 +23,6 @@ export default function PanesStory(): React.JSX.Element {
22
23
  )
23
24
  }
24
25
 
25
- const PaneRoot = styled(Card)`
26
- position: relative;
27
- `
28
-
29
- const PanePortal = styled.div`
30
- position: absolute;
31
- left: 0;
32
- right: 0;
33
- top: 0;
34
- bottom: 0;
35
- pointer-events: none;
36
-
37
- & > * {
38
- pointer-events: auto;
39
- }
40
- `
41
-
42
26
  function Pane(props: {borderLeft?: boolean; id: string}) {
43
27
  const {borderLeft, id} = props
44
28
  const [element, setElement] = useState<HTMLDivElement | null>(null)
@@ -52,13 +36,13 @@ function Pane(props: {borderLeft?: boolean; id: string}) {
52
36
  return (
53
37
  <BoundaryElementProvider element={element}>
54
38
  <PortalProvider element={portalElement}>
55
- <PaneRoot borderLeft={borderLeft} flex={1} ref={setElement}>
39
+ <Card className={paneRoot} borderLeft={borderLeft} flex={1} ref={setElement}>
56
40
  <Box padding={4}>
57
41
  <Button onClick={() => setDialogOpen(true)} selected={dialogOpen} text="Open dialog" />
58
42
  </Box>
59
43
 
60
- <PanePortal ref={setPortalElement} />
61
- </PaneRoot>
44
+ <div className={panePortal} ref={setPortalElement} />
45
+ </Card>
62
46
 
63
47
  {dialogOpen && (
64
48
  <Dialog
@@ -0,0 +1,18 @@
1
+ import {globalStyle, style} from '@vanilla-extract/css'
2
+
3
+ export const paneRoot: string = style({
4
+ position: 'relative',
5
+ })
6
+
7
+ export const panePortal: string = style({
8
+ position: 'absolute',
9
+ left: 0,
10
+ right: 0,
11
+ top: 0,
12
+ bottom: 0,
13
+ pointerEvents: 'none',
14
+ })
15
+
16
+ globalStyle(`${panePortal} > *`, {
17
+ pointerEvents: 'auto',
18
+ })
@@ -34,15 +34,12 @@ describe('components/menu', () => {
34
34
 
35
35
  const value: MenuContextValue = useMemo(
36
36
  () => ({
37
- version: 0.0,
37
+ version: 2,
38
38
  activeElement: null,
39
- activeIndex: 0,
40
39
  mount: (element: HTMLElement | null) => () => console.log(element),
41
40
  onItemClick: () => undefined,
42
41
  onItemMouseEnter: handleItemMouseEnter,
43
42
  onItemMouseLeave: handleItemMouseLeave,
44
- onMouseEnter: handleItemMouseEnter,
45
- onMouseLeave: handleItemMouseLeave,
46
43
  }),
47
44
  [handleItemMouseEnter, handleItemMouseLeave],
48
45
  )
@@ -58,8 +55,7 @@ describe('components/menu', () => {
58
55
 
59
56
  const contextValue = log.mock.calls[0][0]
60
57
 
61
- expect(contextValue.version).toBe(0.0)
62
- expect(contextValue.activeIndex).toBe(0)
58
+ expect(contextValue.version).toBe(2)
63
59
  expect(typeof contextValue.mount).toBe('function')
64
60
  expect(typeof contextValue.onItemClick).toBe('function')
65
61
  expect(typeof contextValue.onItemMouseEnter).toBe('function')
@@ -127,22 +127,21 @@ export function Menu<E extends MenuElementType = typeof DEFAULT_MENU_ELEMENT>(
127
127
  ),
128
128
  )
129
129
 
130
- const value: MenuContextValue = useMemo(
131
- () => ({
132
- version: 0.0,
133
- activeElement,
134
- activeIndex,
135
- mount,
136
- onClickOutside,
137
- onEscape,
138
- onItemClick,
139
- onItemMouseEnter: handleItemMouseEnter,
140
- onItemMouseLeave: handleItemMouseLeave,
141
- registerElement,
142
- }),
130
+ const value = useMemo(
131
+ () =>
132
+ ({
133
+ version: 2,
134
+ activeElement,
135
+ mount,
136
+ onClickOutside,
137
+ onEscape,
138
+ onItemClick,
139
+ onItemMouseEnter: handleItemMouseEnter,
140
+ onItemMouseLeave: handleItemMouseLeave,
141
+ registerElement,
142
+ }) satisfies MenuContextValue,
143
143
  [
144
144
  activeElement,
145
- activeIndex,
146
145
  mount,
147
146
  handleItemMouseEnter,
148
147
  handleItemMouseLeave,
@@ -3,9 +3,8 @@ import {type Context, type MouseEvent as ReactMouseEvent} from 'react'
3
3
  import {createGlobalScopedContext} from '../../lib/createGlobalScopedContext'
4
4
 
5
5
  export interface MenuContextValue {
6
- version: 0.0
6
+ version: 2
7
7
  activeElement: HTMLElement | null
8
- activeIndex: number
9
8
  mount: (element: HTMLElement | null, selected?: boolean) => () => void
10
9
  onClickOutside?: (event: MouseEvent) => void
11
10
  onEscape?: () => void
@@ -16,4 +15,4 @@ export interface MenuContextValue {
16
15
  }
17
16
 
18
17
  export const MenuContext: Context<MenuContextValue | null> =
19
- createGlobalScopedContext<MenuContextValue | null>('@sanity/ui/v3/menu', null)
18
+ createGlobalScopedContext<MenuContextValue | null>('@sanity/ui/v4/menu', null)
@@ -142,11 +142,13 @@ export function MenuGroup<E extends MenuGroupElementType = typeof DEFAULT_MENU_G
142
142
 
143
143
  // Close child menu when a sibling item becomes active
144
144
  useEffect(() => {
145
+ // eslint-disable-next-line react-hooks/set-state-in-effect
145
146
  if (!active) setOpen(false)
146
147
  }, [active])
147
148
 
148
149
  // Update state when child menu is no longer open
149
150
  useEffect(() => {
151
+ // eslint-disable-next-line react-hooks/set-state-in-effect
150
152
  if (!open) setWithinMenu(false)
151
153
  }, [open])
152
154
 
@@ -12,8 +12,8 @@ export function useMenu(): MenuContextValue {
12
12
 
13
13
  // NOTE: This check is for future-compatiblity
14
14
  // - If the value is not an object, it’s not compatible with the current version
15
- // - If the value is an object, but doesn’t have `version: 0.0`, it’s not compatible with the current version
16
- if (!isRecord(value) || value.version !== 0.0) {
15
+ // - If the value is an object, but doesn’t have `version: 2`, it’s not compatible with the current version
16
+ if (!isRecord(value) || value.version !== 2) {
17
17
  throw new Error('useMenu(): the context value is not compatible')
18
18
  }
19
19
 
@@ -5,7 +5,6 @@ import {
5
5
  type MutableRefObject,
6
6
  useCallback,
7
7
  useEffect,
8
- useMemo,
9
8
  useRef,
10
9
  useState,
11
10
  } from 'react'
@@ -39,12 +38,13 @@ export function useMenuController(props: {
39
38
  const elementsRef = useRef<HTMLElement[]>([])
40
39
  const [activeIndex, _setActiveIndex] = useState(-1)
41
40
  const activeIndexRef = useRef(activeIndex)
42
- const activeElement = useMemo(() => elementsRef.current[activeIndex] || null, [activeIndex])
43
- const mounted = Boolean(rootElementRef.current)
41
+ const [activeElement, setActiveElement] = useState<HTMLElement | null>(null)
44
42
 
45
43
  const setActiveIndex = useCallback((nextActiveIndex: number) => {
46
44
  _setActiveIndex(nextActiveIndex)
47
45
  activeIndexRef.current = nextActiveIndex
46
+
47
+ setActiveElement(elementsRef.current[nextActiveIndex] || null)
48
48
  }, [])
49
49
 
50
50
  const mount = useCallback(
@@ -193,7 +193,7 @@ export function useMenuController(props: {
193
193
 
194
194
  // Set focus on the currently active element
195
195
  useEffect(() => {
196
- if (!mounted) return
196
+ if (!rootElementRef.current) return
197
197
 
198
198
  const rafId = requestAnimationFrame(() => {
199
199
  if (activeIndex === -1) {
@@ -228,7 +228,7 @@ export function useMenuController(props: {
228
228
  })
229
229
 
230
230
  return () => cancelAnimationFrame(rafId)
231
- }, [activeIndex, mounted, setActiveIndex, shouldFocus])
231
+ }, [activeIndex, rootElementRef, setActiveIndex, shouldFocus])
232
232
 
233
233
  return {
234
234
  activeElement,
@@ -4,4 +4,4 @@ import {createGlobalScopedContext} from '../../lib/createGlobalScopedContext'
4
4
  import type {ToastContextValue} from './types'
5
5
 
6
6
  export const ToastContext: Context<ToastContextValue | null> =
7
- createGlobalScopedContext<ToastContextValue | null>('@sanity/ui/v3/toast', null)
7
+ createGlobalScopedContext<ToastContextValue | null>('@sanity/ui/v4/toast', null)
@@ -226,6 +226,7 @@ export function Tree<E extends TreeElementType = typeof DEFAULT_TREE_ELEMENT>(
226
226
  ref.current.querySelectorAll('[data-ui="TreeItem"]'),
227
227
  ) as HTMLElement[]
228
228
 
229
+ // eslint-disable-next-line react-hooks/set-state-in-effect
229
230
  setItemElements(_itemElements)
230
231
  }, [children])
231
232
 
@@ -7,4 +7,4 @@ import type {TreeContextValue} from './types'
7
7
  * @internal
8
8
  */
9
9
  export const TreeContext: Context<TreeContextValue | null> =
10
- createGlobalScopedContext<TreeContextValue | null>('@sanity/ui/v3/tree', null)
10
+ createGlobalScopedContext<TreeContextValue | null>('@sanity/ui/v4/tree', null)