@sanity/ui 2.1.15-canary.5 → 2.2.0

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 (39) hide show
  1. package/dist/index.d.mts +2 -7
  2. package/dist/index.d.ts +2 -7
  3. package/dist/index.esm.js +2964 -2988
  4. package/dist/index.esm.js.map +1 -1
  5. package/dist/index.js +2964 -2987
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +2964 -2988
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/theme.esm.js +478 -1283
  10. package/dist/theme.esm.js.map +1 -1
  11. package/dist/theme.js +478 -1283
  12. package/dist/theme.js.map +1 -1
  13. package/dist/theme.mjs +478 -1283
  14. package/dist/theme.mjs.map +1 -1
  15. package/package.json +40 -38
  16. package/src/core/components/autocomplete/autocomplete.tsx +12 -9
  17. package/src/core/components/dialog/dialog.tsx +32 -20
  18. package/src/core/components/menu/menu.tsx +6 -8
  19. package/src/core/components/menu/menuItem.tsx +10 -10
  20. package/src/core/components/tab/tab.tsx +12 -9
  21. package/src/core/components/tree/tree.tsx +8 -21
  22. package/src/core/hooks/useArrayProp.ts +10 -1
  23. package/src/core/hooks/useForwardedRef.ts +11 -9
  24. package/src/core/hooks/useIsomorphicEffect.ts +0 -1
  25. package/src/core/primitives/card/__workshop__/props.tsx +2 -0
  26. package/src/core/primitives/card/card.tsx +3 -0
  27. package/src/core/primitives/card/styles.ts +2 -2
  28. package/src/core/primitives/card/types.ts +1 -0
  29. package/src/core/primitives/checkbox/checkbox.tsx +5 -10
  30. package/src/core/primitives/popover/popover.tsx +13 -13
  31. package/src/core/primitives/radio/radio.tsx +3 -8
  32. package/src/core/primitives/select/select.tsx +3 -8
  33. package/src/core/primitives/switch/switch.tsx +4 -8
  34. package/src/core/primitives/textArea/textArea.tsx +3 -8
  35. package/src/core/primitives/textInput/textInput.tsx +4 -16
  36. package/src/core/primitives/tooltip/tooltip.tsx +5 -8
  37. package/src/core/utils/elementQuery/elementQuery.tsx +11 -10
  38. package/src/core/utils/layer/layer.tsx +13 -8
  39. package/src/core/utils/virtualList/virtualList.tsx +8 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ui",
3
- "version": "2.1.15-canary.5",
3
+ "version": "2.2.0",
4
4
  "keywords": [
5
5
  "sanity",
6
6
  "ui",
@@ -51,6 +51,32 @@
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
+ "prepublishOnly": "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
+ },
54
80
  "commitlint": {
55
81
  "extends": [
56
82
  "@commitlint/config-conventional"
@@ -82,7 +108,7 @@
82
108
  "@sanity/color": "^3.0.6",
83
109
  "@sanity/icons": "^3.0.0",
84
110
  "csstype": "^3.1.3",
85
- "framer-motion": "11.2.10",
111
+ "framer-motion": "11.0.8",
86
112
  "react-refractor": "^2.2.0"
87
113
  },
88
114
  "devDependencies": {
@@ -93,7 +119,7 @@
93
119
  "@commitlint/cli": "^19.3.0",
94
120
  "@commitlint/config-conventional": "^19.2.2",
95
121
  "@juggle/resize-observer": "^3.4.0",
96
- "@sanity/pkg-utils": "6.8.20-canary.0",
122
+ "@sanity/pkg-utils": "^6.8.18",
97
123
  "@sanity/semantic-release-preset": "^4.1.7",
98
124
  "@sanity/ui-workshop": "^2.0.13",
99
125
  "@storybook/addon-a11y": "^8.0.8",
@@ -123,7 +149,6 @@
123
149
  "@types/testing-library__jest-dom": "^5.14.9",
124
150
  "@typescript-eslint/eslint-plugin": "^7.11.0",
125
151
  "@typescript-eslint/parser": "^7.11.0",
126
- "babel-plugin-react-compiler": "0.0.0-experimental-938cd9a-20240601",
127
152
  "commitizen": "^4.3.0",
128
153
  "cypress": "^13.10.0",
129
154
  "cypress-real-events": "^1.12.0",
@@ -135,7 +160,7 @@
135
160
  "eslint-plugin-jsx-a11y": "^6.8.0",
136
161
  "eslint-plugin-prettier": "^5.1.3",
137
162
  "eslint-plugin-react": "^7.34.2",
138
- "eslint-plugin-react-compiler": "0.0.0-experimental-a97cca1-20240529",
163
+ "eslint-plugin-react-compiler": "0.0.0-experimental-c8b3f72-20240517",
139
164
  "eslint-plugin-react-hooks": "^4.6.2",
140
165
  "eslint-plugin-storybook": "^0.8.0",
141
166
  "http-server": "^14.1.1",
@@ -146,11 +171,11 @@
146
171
  "lint-staged": "^14.0.1",
147
172
  "module-alias": "^2.2.3",
148
173
  "npm-run-all2": "^5.0.2",
149
- "prettier": "^3.2.5",
174
+ "prettier": "^3.3.0",
150
175
  "prettier-plugin-packagejson": "^2.5.0",
151
- "react": "19.0.0-rc-6d3110b4d9-20240531",
152
- "react-dom": "19.0.0-rc-6d3110b4d9-20240531",
153
- "react-is": "19.0.0-rc-6d3110b4d9-20240531",
176
+ "react": "^18.3.1",
177
+ "react-dom": "^18.3.1",
178
+ "react-is": "^18.3.1",
154
179
  "refractor": "^4.8.1",
155
180
  "rimraf": "^5.0.5",
156
181
  "semantic-release": "^23.1.1",
@@ -163,11 +188,12 @@
163
188
  "vite-tsconfig-paths": "^4.3.2"
164
189
  },
165
190
  "peerDependencies": {
166
- "react": ">=19.0.0-rc",
167
- "react-dom": ">=19.0.0-rc",
168
- "react-is": ">=19.0.0-rc",
169
- "styled-components": "^6.1"
191
+ "react": "^18",
192
+ "react-dom": "^18",
193
+ "react-is": "^18",
194
+ "styled-components": "^5.2 || ^6"
170
195
  },
196
+ "packageManager": "pnpm@9.1.3",
171
197
  "engines": {
172
198
  "node": ">=14.0.0"
173
199
  },
@@ -176,29 +202,5 @@
176
202
  },
177
203
  "esm.sh": {
178
204
  "bundle": false
179
- },
180
- "scripts": {
181
- "build": "run-s clean pkg:build pkg:check figma:pkg:build",
182
- "clean": "rimraf .workshop dist",
183
- "commit": "cz",
184
- "cypress:dev": "run-p dev cypress:open",
185
- "cypress:open": "cypress open",
186
- "cypress:run": "cypress run",
187
- "dev": "run-p storybook:dev workshop:dev",
188
- "figma:pkg:build": "cd figma && pnpm build",
189
- "format": "prettier --write --cache --ignore-unknown .",
190
- "lint": "eslint . --ext .cjs,.js,.jsx,.mjs,.ts,.tsx",
191
- "pkg:build": "pkg build --strict",
192
- "pkg:check": "pkg --strict",
193
- "release": "semantic-release",
194
- "storybook:build": "storybook build",
195
- "storybook:dev": "storybook dev -p 6006",
196
- "test": "jest",
197
- "test:browser": "start-server-and-test 'run-s workshop:build workshop:start' http://localhost:1337 'run-s cypress:run'",
198
- "ts:check": "tsc",
199
- "watch": "pkg watch --strict",
200
- "workshop:build": "workshop build",
201
- "workshop:dev": "workshop dev",
202
- "workshop:start": "http-server -a localhost -c-0 -p 1337 -s -P http://localhost:1337/index.html? dist"
203
205
  }
204
- }
206
+ }
@@ -13,14 +13,13 @@ import {
13
13
  forwardRef,
14
14
  useCallback,
15
15
  useEffect,
16
- useImperativeHandle,
17
16
  useMemo,
18
17
  useReducer,
19
18
  useRef,
20
19
  } from 'react'
21
20
  import {EMPTY_ARRAY, EMPTY_RECORD} from '../../constants'
22
21
  import {_hasFocus, _raf, focusFirstDescendant} from '../../helpers'
23
- import {useArrayProp} from '../../hooks'
22
+ import {useArrayProp, useForwardedRef} from '../../hooks'
24
23
  import {
25
24
  Box,
26
25
  BoxProps,
@@ -120,7 +119,7 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
120
119
  | 'type'
121
120
  | 'value'
122
121
  >,
123
- forwardedRef: React.ForwardedRef<HTMLInputElement>,
122
+ ref: Ref<HTMLInputElement>,
124
123
  ) {
125
124
  const {
126
125
  border = true,
@@ -192,11 +191,7 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
192
191
  const valuePropRef = useRef(valueProp)
193
192
  const popoverMouseWithinRef = useRef(false)
194
193
 
195
- // Forward ref to parent
196
- useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(
197
- forwardedRef,
198
- () => inputElementRef.current,
199
- )
194
+ const forwardedRef = useForwardedRef(ref)
200
195
 
201
196
  const listBoxId = `${id}-listbox`
202
197
  const options = Array.isArray(optionsProp) ? optionsProp : EMPTY_ARRAY
@@ -437,6 +432,14 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
437
432
  }
438
433
  }, [activeValue, filteredOptions])
439
434
 
435
+ const setRef = useCallback(
436
+ (el: HTMLInputElement | null) => {
437
+ inputElementRef.current = el
438
+ forwardedRef.current = el
439
+ },
440
+ [forwardedRef],
441
+ )
442
+
440
443
  const clearButton = useMemo(() => {
441
444
  if (!loading && !disabled && value) {
442
445
  return {
@@ -546,7 +549,7 @@ const InnerAutocomplete = forwardRef(function InnerAutocomplete<
546
549
  prefix={prefix}
547
550
  radius={radius}
548
551
  readOnly={readOnly}
549
- ref={inputElementRef}
552
+ ref={setRef}
550
553
  role="combobox"
551
554
  spellCheck={false}
552
555
  suffix={suffix || openButtonNode}
@@ -1,6 +1,6 @@
1
1
  import {CloseIcon} from '@sanity/icons'
2
2
  import {ThemeColorSchemeKey} from '@sanity/ui/theme'
3
- import {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'
3
+ import {forwardRef, useCallback, useEffect, useRef, useState} from 'react'
4
4
  import {styled} from 'styled-components'
5
5
  import {
6
6
  containsOrEqualsElement,
@@ -8,7 +8,13 @@ import {
8
8
  focusLastDescendant,
9
9
  isHTMLElement,
10
10
  } from '../../helpers'
11
- import {useArrayProp, useClickOutside, useGlobalKeyDown, usePrefersReducedMotion} from '../../hooks'
11
+ import {
12
+ useArrayProp,
13
+ useClickOutside,
14
+ useForwardedRef,
15
+ useGlobalKeyDown,
16
+ usePrefersReducedMotion,
17
+ } from '../../hooks'
12
18
  import {Box, Button, Card, Container, Flex, Text} from '../../primitives'
13
19
  import {ResponsivePaddingProps, ResponsiveWidthProps} from '../../primitives/types'
14
20
  import {responsivePaddingStyle, ResponsivePaddingStyleProps} from '../../styles/internal'
@@ -145,13 +151,13 @@ const DialogFooter = styled(Box)`
145
151
 
146
152
  const DialogCard = forwardRef(function DialogCard(
147
153
  props: DialogCardProps,
148
- forwardedRef: React.ForwardedRef<HTMLDivElement>,
154
+ ref: React.ForwardedRef<HTMLDivElement>,
149
155
  ) {
150
156
  const {
151
157
  __unstable_autoFocus: autoFocus,
152
158
  __unstable_hideCloseButton: hideCloseButton,
153
159
  children,
154
- contentRef: forwardedContentRef,
160
+ contentRef,
155
161
  footer,
156
162
  header,
157
163
  id,
@@ -169,29 +175,23 @@ const DialogCard = forwardRef(function DialogCard(
169
175
  const radius = useArrayProp(radiusProp)
170
176
  const shadow = useArrayProp(shadowProp)
171
177
  const width = useArrayProp(widthProp)
172
- const ref = useRef<HTMLDivElement | null>(null)
178
+ const forwardedRef = useForwardedRef(ref)
173
179
  const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
174
- const contentRef = useRef<HTMLDivElement | null>(null)
180
+ const localContentRef = useRef<HTMLDivElement | null>(null)
175
181
  const layer = useLayer()
176
182
  const {isTopLayer} = layer
177
183
  const labelId = `${id}_label`
178
184
  const showCloseButton = Boolean(onClose) && hideCloseButton === false
179
185
  const showHeader = Boolean(header) || showCloseButton
180
186
 
181
- useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(forwardedRef, () => ref.current)
182
- useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(
183
- forwardedContentRef,
184
- () => contentRef.current,
185
- )
186
-
187
187
  useEffect(() => {
188
188
  if (!autoFocus) return
189
189
 
190
190
  // On mount: focus the first focusable element
191
- if (ref.current) {
192
- focusFirstDescendant(ref.current)
191
+ if (forwardedRef.current) {
192
+ focusFirstDescendant(forwardedRef.current)
193
193
  }
194
- }, [autoFocus, ref])
194
+ }, [autoFocus, forwardedRef])
195
195
 
196
196
  useGlobalKeyDown(
197
197
  useCallback(
@@ -234,10 +234,22 @@ const DialogCard = forwardRef(function DialogCard(
234
234
  [rootElement],
235
235
  )
236
236
 
237
- const setRef = useCallback((el: HTMLDivElement | null) => {
238
- setRootElement(el)
239
- ref.current = el
240
- }, [])
237
+ const setRef = useCallback(
238
+ (el: HTMLDivElement | null) => {
239
+ setRootElement(el)
240
+ forwardedRef.current = el
241
+ },
242
+ [forwardedRef],
243
+ )
244
+
245
+ const setContentRef = useCallback(
246
+ (el: HTMLDivElement | null) => {
247
+ localContentRef.current = el
248
+ if (typeof contentRef === 'function') contentRef(el)
249
+ else if (contentRef) contentRef.current = el
250
+ },
251
+ [contentRef],
252
+ )
241
253
 
242
254
  return (
243
255
  <DialogContainer data-ui="DialogCard" width={width}>
@@ -269,7 +281,7 @@ const DialogCard = forwardRef(function DialogCard(
269
281
  </DialogHeader>
270
282
  )}
271
283
 
272
- <DialogContent flex={1} ref={contentRef} tabIndex={-1}>
284
+ <DialogContent flex={1} ref={setContentRef} tabIndex={-1}>
273
285
  {children}
274
286
  </DialogContent>
275
287
 
@@ -1,6 +1,6 @@
1
- import {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef} from 'react'
1
+ import {forwardRef, useCallback, useEffect, useMemo} from 'react'
2
2
  import {styled} from 'styled-components'
3
- import {useClickOutside, useGlobalKeyDown} from '../../hooks'
3
+ import {useClickOutside, useForwardedRef, useGlobalKeyDown} from '../../hooks'
4
4
  import {Box, Stack} from '../../primitives'
5
5
  import {ResponsivePaddingProps} from '../../primitives/types'
6
6
  import {useLayer} from '../../utils'
@@ -41,7 +41,7 @@ const Root = styled(Box)`
41
41
  */
42
42
  export const Menu = forwardRef(function Menu(
43
43
  props: MenuProps & Omit<React.HTMLProps<HTMLDivElement>, 'as' | 'height' | 'role' | 'tabIndex'>,
44
- forwardedRef: React.ForwardedRef<HTMLDivElement>,
44
+ ref: React.ForwardedRef<HTMLDivElement>,
45
45
  ) {
46
46
  const {
47
47
  children,
@@ -62,9 +62,7 @@ export const Menu = forwardRef(function Menu(
62
62
  ...restProps
63
63
  } = props
64
64
 
65
- const ref = useRef<HTMLDivElement | null>(null)
66
-
67
- useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(forwardedRef, () => ref.current)
65
+ const forwardedRef = useForwardedRef(ref)
68
66
 
69
67
  const {isTopLayer} = useLayer()
70
68
 
@@ -82,9 +80,9 @@ export const Menu = forwardRef(function Menu(
82
80
  const handleRefChange = useCallback(
83
81
  (el: HTMLDivElement | null) => {
84
82
  setRootElement(el)
85
- ref.current = el
83
+ forwardedRef.current = el
86
84
  },
87
- [setRootElement],
85
+ [forwardedRef, setRootElement],
88
86
  )
89
87
 
90
88
  // Trigger `onItemSelect` when active index changes
@@ -4,13 +4,11 @@ import {
4
4
  isValidElement,
5
5
  useCallback,
6
6
  useEffect,
7
- useImperativeHandle,
8
7
  useMemo,
9
- useRef,
10
8
  useState,
11
9
  } from 'react'
12
10
  import {isValidElementType} from 'react-is'
13
- import {useArrayProp} from '../../hooks'
11
+ import {useArrayProp, useForwardedRef} from '../../hooks'
14
12
  import {Box, Flex, Text} from '../../primitives'
15
13
  import {Selectable} from '../../primitives/_selectable'
16
14
  import {ResponsivePaddingProps, ResponsiveRadiusProps} from '../../primitives/types'
@@ -78,12 +76,11 @@ export const MenuItem = forwardRef(function MenuItem(
78
76
  } = menu
79
77
  const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
80
78
  const active = Boolean(activeElement) && activeElement === rootElement
81
- const ref = useRef<HTMLDivElement | null>(null)
82
-
83
- useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(forwardedRef, () => ref.current)
84
79
 
85
80
  useEffect(() => mount(rootElement, selectedProp), [mount, rootElement, selectedProp])
86
81
 
82
+ const ref = useForwardedRef(forwardedRef)
83
+
87
84
  const handleClick = useCallback(
88
85
  (event: React.MouseEvent<HTMLDivElement>) => {
89
86
  if (disabled) return
@@ -108,10 +105,13 @@ export const MenuItem = forwardRef(function MenuItem(
108
105
 
109
106
  const hotkeysFontSize = useArrayProp(fontSize).map((s) => s - 1)
110
107
 
111
- const setRef = useCallback((el: HTMLDivElement | null) => {
112
- ref.current = el
113
- setRootElement(el)
114
- }, [])
108
+ const setRef = useCallback(
109
+ (el: HTMLDivElement | null) => {
110
+ ref.current = el
111
+ setRootElement(el)
112
+ },
113
+ [ref],
114
+ )
115
115
 
116
116
  return (
117
117
  <Selectable
@@ -1,5 +1,6 @@
1
- import {forwardRef, useCallback, useEffect, useImperativeHandle, useRef} from 'react'
1
+ import {forwardRef, useCallback, useEffect, useRef} from 'react'
2
2
  import {styled} from 'styled-components'
3
+ import {useForwardedRef} from '../../hooks'
3
4
  import {Button} from '../../primitives'
4
5
  import {ButtonTone} from '../../types'
5
6
 
@@ -48,14 +49,9 @@ export const Tab = forwardRef(function Tab(
48
49
  selected,
49
50
  ...restProps
50
51
  } = props
51
- const ref = useRef<HTMLButtonElement | null>(null)
52
+ const elementRef = useRef<HTMLButtonElement | null>(null)
52
53
  const focusedRef = useRef(false)
53
54
 
54
- useImperativeHandle<HTMLButtonElement | null, HTMLButtonElement | null>(
55
- forwardedRef,
56
- () => ref.current,
57
- )
58
-
59
55
  const handleBlur = useCallback(() => {
60
56
  focusedRef.current = false
61
57
  }, [])
@@ -68,13 +64,20 @@ export const Tab = forwardRef(function Tab(
68
64
  [onFocus],
69
65
  )
70
66
 
67
+ const ref = useForwardedRef(forwardedRef)
68
+
71
69
  useEffect(() => {
72
70
  if (focused && !focusedRef.current) {
73
- if (ref.current) ref.current.focus()
71
+ if (elementRef.current) elementRef.current.focus()
74
72
  focusedRef.current = true
75
73
  }
76
74
  }, [focused])
77
75
 
76
+ const setRef = (el: HTMLButtonElement | null) => {
77
+ elementRef.current = el
78
+ ref.current = el
79
+ }
80
+
78
81
  return (
79
82
  <CustomButton
80
83
  data-ui="Tab"
@@ -88,7 +91,7 @@ export const Tab = forwardRef(function Tab(
88
91
  onBlur={handleBlur}
89
92
  onFocus={handleFocus}
90
93
  padding={padding}
91
- ref={ref}
94
+ ref={setRef}
92
95
  role="tab"
93
96
  selected={selected}
94
97
  tabIndex={selected ? 0 : -1}
@@ -1,13 +1,5 @@
1
- import {
2
- forwardRef,
3
- memo,
4
- useCallback,
5
- useEffect,
6
- useImperativeHandle,
7
- useMemo,
8
- useRef,
9
- useState,
10
- } from 'react'
1
+ import {forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
2
+ import {useForwardedRef} from '../../hooks'
11
3
  import {Stack} from '../../primitives'
12
4
  import {_findNextItemElement, _findPrevItemElement, _focusItemElement} from './helpers'
13
5
  import {TreeContext} from './treeContext'
@@ -29,10 +21,10 @@ export const Tree = memo(
29
21
  forwardRef(function Tree(
30
22
  props: TreeProps &
31
23
  Omit<React.HTMLProps<HTMLDivElement>, 'align' | 'as' | 'height' | 'ref' | 'role' | 'wrap'>,
32
- forwardedRef: React.ForwardedRef<HTMLDivElement>,
24
+ ref: React.ForwardedRef<HTMLDivElement>,
33
25
  ): React.ReactElement {
34
26
  const {children, space = 1, onFocus, ...restProps} = props
35
- const ref = useRef<HTMLDivElement | null>(null)
27
+ const forwardedRef = useForwardedRef(ref)
36
28
  const [focusedElement, setFocusedElement] = useState<HTMLElement | null>(null)
37
29
  const focusedElementRef = useRef(focusedElement)
38
30
  const path: string[] = useMemo(() => [], [])
@@ -40,11 +32,6 @@ export const Tree = memo(
40
32
  const [state, setState] = useState<TreeState>({})
41
33
  const stateRef = useRef(state)
42
34
 
43
- useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(
44
- forwardedRef,
45
- () => ref.current,
46
- )
47
-
48
35
  useEffect(() => {
49
36
  focusedElementRef.current = focusedElement
50
37
  }, [focusedElement])
@@ -207,13 +194,13 @@ export const Tree = memo(
207
194
  )
208
195
 
209
196
  useEffect(() => {
210
- if (!ref.current) return
197
+ if (!forwardedRef.current) return
211
198
  const _itemElements = Array.from(
212
- ref.current.querySelectorAll('[data-ui="TreeItem"]'),
199
+ forwardedRef.current.querySelectorAll('[data-ui="TreeItem"]'),
213
200
  ) as HTMLElement[]
214
201
 
215
202
  setItemElements(_itemElements)
216
- }, [children, ref])
203
+ }, [children, forwardedRef])
217
204
 
218
205
  return (
219
206
  <TreeContext.Provider value={contextValue}>
@@ -223,7 +210,7 @@ export const Tree = memo(
223
210
  {...restProps}
224
211
  onFocus={handleFocus}
225
212
  onKeyDown={handleKeyDown}
226
- ref={ref}
213
+ ref={forwardedRef}
227
214
  role="tree"
228
215
  space={space}
229
216
  >
@@ -12,5 +12,14 @@ export function useArrayProp<T extends ArrayPropPrimitive = ArrayPropPrimitive>(
12
12
  val: T | T[] | undefined,
13
13
  defaultVal?: T[],
14
14
  ): T[] {
15
- return useMemo(() => _getArrayProp(val, defaultVal), [defaultVal, val])
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])
17
+
18
+ return useMemo(
19
+ () => _getArrayProp(val, defaultVal),
20
+
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
+ )
16
25
  }
@@ -1,19 +1,21 @@
1
- import {useImperativeHandle, useRef} from 'react'
1
+ import {useRef} from 'react'
2
+ import {useIsomorphicEffect} from './useIsomorphicEffect'
2
3
 
3
4
  /**
4
5
  * @beta
5
- * @deprecated use `useImperativeHandle` instead
6
- * @example
7
- * ```diff
8
- * -const ref = useForwardedRef(forwardedRef)
9
- * +const ref = useRef(null)
10
- * +useImperativeHandle(forwardedRef, () => ref.current)
11
- * ```
12
6
  */
13
7
  export function useForwardedRef<T>(ref: React.ForwardedRef<T>): React.MutableRefObject<T | null> {
14
8
  const innerRef = useRef<T | null>(null)
15
9
 
16
- useImperativeHandle(ref, () => innerRef.current!)
10
+ useIsomorphicEffect(() => {
11
+ if (!ref) return
12
+
13
+ if (typeof ref === 'function') {
14
+ ref(innerRef.current)
15
+ } else {
16
+ ref.current = innerRef.current
17
+ }
18
+ })
17
19
 
18
20
  return innerRef
19
21
  }
@@ -2,6 +2,5 @@ import {useEffect, useLayoutEffect} from 'react'
2
2
 
3
3
  /**
4
4
  * @beta
5
- * @deprecated use `useLayoutEffect` instead, or `useEffect` if possible
6
5
  */
7
6
  export const useIsomorphicEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
@@ -12,6 +12,7 @@ export default function PropsStory() {
12
12
  const as = useSelect('As', WORKSHOP_CARD_AS_OPTIONS, 'div', 'Props')
13
13
  const border = useBoolean('Border', false, 'Props')
14
14
  const checkered = useBoolean('Checkered', false, 'Props')
15
+ const muted = useBoolean('Muted', false, 'Props')
15
16
  const padding = useSelect('Padding', WORKSHOP_SPACE_OPTIONS, 0, 'Props')
16
17
  const radius = useSelect('Radius', WORKSHOP_RADIUS_OPTIONS, 0, 'Props')
17
18
  const selected = useBoolean('Selected', false, 'Props')
@@ -24,6 +25,7 @@ export default function PropsStory() {
24
25
  __unstable_checkered={checkered}
25
26
  as={as}
26
27
  border={border}
28
+ muted={muted}
27
29
  onClick={useAction('onClick')}
28
30
  padding={padding}
29
31
  radius={radius}
@@ -36,6 +36,7 @@ export interface CardProps
36
36
  * @beta
37
37
  */
38
38
  __unstable_focusRing?: boolean
39
+ muted?: boolean
39
40
  pressed?: boolean
40
41
  scheme?: ThemeColorSchemeKey
41
42
  tone?: CardTone
@@ -67,6 +68,7 @@ export const Card = forwardRef(function Card(
67
68
  borderRight,
68
69
  borderBottom,
69
70
  borderLeft,
71
+ muted,
70
72
  pressed,
71
73
  radius = 0,
72
74
  scheme,
@@ -96,6 +98,7 @@ export const Card = forwardRef(function Card(
96
98
  $borderLeft={useArrayProp(borderLeft)}
97
99
  $checkered={checkered}
98
100
  $focusRing={focusRing}
101
+ $muted={muted}
99
102
  $radius={useArrayProp(radius)}
100
103
  $shadow={useArrayProp(shadow)}
101
104
  $tone={tone}
@@ -49,7 +49,7 @@ export function cardBaseStyle(props: CardStyleProps & ThemeProps): ReturnType<ty
49
49
  }
50
50
 
51
51
  export function cardColorStyle(props: CardStyleProps & ThemeProps): ReturnType<typeof css> {
52
- const {$checkered, $focusRing} = props
52
+ const {$checkered, $focusRing, $muted} = props
53
53
  const {card, color, style} = getTheme_v2(props.theme)
54
54
  const border = {width: card.border.width, color: 'var(--card-border-color)'}
55
55
 
@@ -58,7 +58,7 @@ export function cardColorStyle(props: CardStyleProps & ThemeProps): ReturnType<t
58
58
 
59
59
  ${_cardColorStyle(color, color, $checkered)}
60
60
 
61
- background-color: var(--card-bg-color);
61
+ background-color: ${$muted ? 'var(--card-muted-bg-color)' : 'var(--card-bg-color)'};
62
62
  color: var(--card-fg-color);
63
63
 
64
64
  /* &:is(button) */
@@ -6,5 +6,6 @@ import {ThemeColorToneKey} from '@sanity/ui/theme'
6
6
  export interface CardStyleProps {
7
7
  $checkered: boolean
8
8
  $focusRing: boolean
9
+ $muted: boolean
9
10
  $tone: ThemeColorToneKey
10
11
  }
@@ -1,7 +1,7 @@
1
1
  import {CheckmarkIcon, RemoveIcon} from '@sanity/icons'
2
- import {forwardRef, useEffect, useImperativeHandle, useRef} from 'react'
2
+ import {forwardRef, useEffect} from 'react'
3
3
  import {styled} from 'styled-components'
4
- import {useCustomValidity} from '../../hooks'
4
+ import {useForwardedRef, useCustomValidity} from '../../hooks'
5
5
  import {checkboxBaseStyles, inputElementStyles} from './styles'
6
6
 
7
7
  /**
@@ -34,21 +34,16 @@ export const Checkbox = forwardRef(function Checkbox(
34
34
  style,
35
35
  ...restProps
36
36
  } = props
37
- const ref = useRef<HTMLInputElement | null>(null)
37
+ const ref = useForwardedRef(forwardedRef)
38
38
 
39
- useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(
40
- forwardedRef,
41
- () => ref.current,
42
- )
39
+ useCustomValidity(ref, customValidity)
43
40
 
44
41
  useEffect(() => {
45
42
  if (ref.current) {
46
43
  // Set the indeterminate state
47
44
  ref.current.indeterminate = indeterminate || false
48
45
  }
49
- }, [indeterminate])
50
-
51
- useCustomValidity(ref, customValidity)
46
+ }, [indeterminate, ref])
52
47
 
53
48
  return (
54
49
  <Root className={className} data-ui="Checkbox" style={style}>