@sanity/ui 2.8.20 → 2.8.22

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.20",
3
+ "version": "2.8.22",
4
4
  "keywords": [
5
5
  "sanity",
6
6
  "ui",
@@ -122,24 +122,24 @@
122
122
  "@commitlint/cli": "^19.4.0",
123
123
  "@commitlint/config-conventional": "^19.2.2",
124
124
  "@juggle/resize-observer": "^3.4.0",
125
- "@sanity/pkg-utils": "^6.11.8",
125
+ "@sanity/pkg-utils": "^6.11.9",
126
126
  "@sanity/prettier-config": "^1.0.2",
127
127
  "@sanity/semantic-release-preset": "^5.0.0",
128
128
  "@sanity/ui-workshop": "^2.0.16",
129
- "@storybook/addon-a11y": "^8.4.1",
130
- "@storybook/addon-docs": "^8.4.1",
131
- "@storybook/addon-essentials": "^8.4.1",
132
- "@storybook/addon-interactions": "^8.4.1",
133
- "@storybook/addon-links": "^8.4.1",
134
- "@storybook/addon-mdx-gfm": "^8.4.1",
135
- "@storybook/addon-storysource": "^8.4.1",
136
- "@storybook/addon-themes": "^8.4.1",
137
- "@storybook/blocks": "^8.4.1",
138
- "@storybook/manager-api": "^8.4.1",
139
- "@storybook/react": "^8.4.1",
140
- "@storybook/react-vite": "^8.4.1",
141
- "@storybook/test": "^8.4.1",
142
- "@storybook/theming": "^8.4.1",
129
+ "@storybook/addon-a11y": "^8.4.2",
130
+ "@storybook/addon-docs": "^8.4.2",
131
+ "@storybook/addon-essentials": "^8.4.2",
132
+ "@storybook/addon-interactions": "^8.4.2",
133
+ "@storybook/addon-links": "^8.4.2",
134
+ "@storybook/addon-mdx-gfm": "^8.4.2",
135
+ "@storybook/addon-storysource": "^8.4.2",
136
+ "@storybook/addon-themes": "^8.4.2",
137
+ "@storybook/blocks": "^8.4.2",
138
+ "@storybook/manager-api": "^8.4.2",
139
+ "@storybook/react": "^8.4.2",
140
+ "@storybook/react-vite": "^8.4.2",
141
+ "@storybook/test": "^8.4.2",
142
+ "@storybook/theming": "^8.4.2",
143
143
  "@testing-library/dom": "^10.4.0",
144
144
  "@testing-library/jest-dom": "^6.6.2",
145
145
  "@testing-library/react": "^16.0.1",
@@ -168,7 +168,7 @@
168
168
  "eslint-plugin-react": "^7.37.2",
169
169
  "eslint-plugin-react-compiler": "19.0.0-beta-63b359f-20241101",
170
170
  "eslint-plugin-react-hooks": "^5.0.0",
171
- "eslint-plugin-storybook": "^0.10.2",
171
+ "eslint-plugin-storybook": "^0.11.0",
172
172
  "http-server": "^14.1.1",
173
173
  "husky": "^8.0.3",
174
174
  "jest": "^29.7.0",
@@ -185,7 +185,7 @@
185
185
  "rimraf": "^5.0.5",
186
186
  "semantic-release": "^24.0.0",
187
187
  "start-server-and-test": "^2.0.5",
188
- "storybook": "^8.4.1",
188
+ "storybook": "^8.4.2",
189
189
  "styled-components": "^6.1.13",
190
190
  "tsconfig-paths": "^4.2.0",
191
191
  "typescript": "5.6.3",
@@ -14,19 +14,21 @@ export interface MenuProps extends ResponsivePaddingProps {
14
14
  /**
15
15
  * @deprecated Use `shouldFocus="first"` instead.
16
16
  */
17
- focusFirst?: boolean
17
+ 'focusFirst'?: boolean
18
18
  /**
19
19
  * @deprecated Use `shouldFocus="last"` instead.
20
20
  */
21
- focusLast?: boolean
22
- onClickOutside?: (event: MouseEvent) => void
23
- onEscape?: () => void
24
- onItemClick?: () => void
25
- onItemSelect?: (index: number) => void
26
- originElement?: HTMLElement | null
27
- registerElement?: (el: HTMLElement) => () => void
28
- shouldFocus?: 'first' | 'last' | null
29
- space?: number | number[]
21
+ 'focusLast'?: boolean
22
+ 'onClickOutside'?: (event: MouseEvent) => void
23
+ 'onEscape'?: () => void
24
+ 'onItemClick'?: () => void
25
+ 'onItemSelect'?: (index: number) => void
26
+ 'originElement'?: HTMLElement | null
27
+ 'registerElement'?: (el: HTMLElement) => () => void
28
+ 'shouldFocus'?: 'first' | 'last' | null
29
+ 'space'?: number | number[]
30
+ 'aria-labelledby'?: string
31
+ 'onBlurCapture'?: (event: FocusEvent) => void
30
32
  }
31
33
 
32
34
  const Root = styled(Box)`
@@ -169,7 +169,7 @@ export const MenuButton = forwardRef(function MenuButton(
169
169
  }, [buttonElement, disableRestoreFocusOnClose])
170
170
 
171
171
  const handleBlur = useCallback(
172
- (event: React.FocusEvent<HTMLButtonElement>) => {
172
+ (event: FocusEvent) => {
173
173
  const target = event.relatedTarget
174
174
 
175
175
  if (!(target instanceof Node)) {
@@ -199,32 +199,19 @@ export const MenuButton = forwardRef(function MenuButton(
199
199
  return () => setChildMenuElements((els) => els.filter((_el) => _el !== el))
200
200
  }, [])
201
201
 
202
- const menuProps: MenuProps = useMemo(
203
- () => ({
204
- 'aria-labelledby': id,
205
- 'onBlurCapture': handleBlur,
206
- 'onClickOutside': handleMenuClickOutside,
207
- 'onEscape': handleMenuEscape,
208
- 'onItemClick': handleItemClick,
209
- 'originElement': buttonElement,
210
- registerElement,
211
- shouldFocus,
212
- }),
213
- [
214
- buttonElement,
215
- handleMenuClickOutside,
216
- handleMenuEscape,
217
- handleItemClick,
218
- id,
219
- handleBlur,
220
- registerElement,
221
- shouldFocus,
222
- ],
223
- )
202
+ const menuProps: MenuProps = {
203
+ 'aria-labelledby': id,
204
+ 'onBlurCapture': handleBlur,
205
+ 'onClickOutside': handleMenuClickOutside,
206
+ 'onEscape': handleMenuEscape,
207
+ 'onItemClick': handleItemClick,
208
+ 'originElement': buttonElement,
209
+ registerElement,
210
+ shouldFocus,
211
+ }
224
212
 
225
213
  const menu = menuProp && cloneElement(menuProp, menuProps)
226
214
 
227
- const ref = useRef<HTMLButtonElement | null>(null)
228
215
  const button = useMemo(
229
216
  () =>
230
217
  buttonProp &&
@@ -236,7 +223,7 @@ export const MenuButton = forwardRef(function MenuButton(
236
223
  'onMouseDown': handleMouseDown,
237
224
  'aria-haspopup': true,
238
225
  'aria-expanded': open,
239
- 'ref': ref,
226
+ 'ref': setButtonElement,
240
227
  'selected': buttonProp.props.selected ?? open,
241
228
  }),
242
229
  [buttonProp, handleButtonClick, handleButtonKeyDown, handleMouseDown, id, open],
@@ -245,19 +232,10 @@ export const MenuButton = forwardRef(function MenuButton(
245
232
  // Forward button ref to parent
246
233
  useImperativeHandle<HTMLButtonElement | null, HTMLButtonElement | null>(
247
234
  forwardedRef,
248
- () => ref.current,
235
+ () => buttonElement,
236
+ [buttonElement],
249
237
  )
250
238
 
251
- // If there's a button then we need to set the reference element to the cloned button ref
252
- // and if button changes we make sure to update or remove the reference element.
253
- useEffect(() => {
254
- if (!button) return undefined
255
-
256
- setButtonElement(ref.current)
257
-
258
- return () => setButtonElement(null)
259
- }, [button])
260
-
261
239
  const popoverProps: MenuButtonProps['popover'] = useMemo(
262
240
  () => ({
263
241
  boundaryElement: deprecated_boundaryElement,
@@ -8,6 +8,7 @@ import {Box} from '../../primitives'
8
8
  import {Layer} from '../../utils'
9
9
  import {Toast} from './toast'
10
10
  import {ToastContext} from './toastContext'
11
+ import {generateToastId} from './toastState'
11
12
  import {ToastContextValue, ToastParams} from './types'
12
13
 
13
14
  type ToastState = {
@@ -45,8 +46,6 @@ const ToastContainer = styled.div`
45
46
  width: 100%;
46
47
  `
47
48
 
48
- let toastId = 0
49
-
50
49
  /**
51
50
  * @public
52
51
  */
@@ -60,20 +59,20 @@ export function ToastProvider(props: ToastProviderProps): React.ReactElement {
60
59
  () => ({
61
60
  initial: {
62
61
  opacity: 0,
63
- [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY as string]: 0,
62
+ [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY]: 0,
64
63
  y: 32,
65
64
  scale: 0.25,
66
65
  willChange: 'transform',
67
66
  },
68
67
  animate: {
69
68
  opacity: [0, 1, 1],
70
- [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY as string]: [0, 0, 1],
69
+ [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY]: [0, 0, 1],
71
70
  y: 0,
72
71
  scale: 1,
73
72
  },
74
73
  exit: {
75
74
  opacity: [1, 1, 0],
76
- [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY as string]: [1, 0, 0],
75
+ [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY]: [1, 0, 0],
77
76
  scale: 0.5,
78
77
  transition: prefersReducedMotion ? {duration: 0} : {duration: 0.2},
79
78
  },
@@ -86,7 +85,7 @@ export function ToastProvider(props: ToastProviderProps): React.ReactElement {
86
85
  // Wrap setState in startTransition to allow React to give input state updates higher priority
87
86
  const setState: typeof _setState = (state) => startTransition(() => _setState(state))
88
87
 
89
- const id = params.id || String(toastId++)
88
+ const id = params.id || generateToastId()
90
89
  const duration = params.duration || 5000
91
90
 
92
91
  const dismiss = () => {
@@ -0,0 +1,6 @@
1
+ let toastId = 0
2
+
3
+ /** @internal */
4
+ export function generateToastId(): string {
5
+ return String(toastId++)
6
+ }
@@ -13,7 +13,7 @@ export const EMPTY_RECORD: Record<string, never> = {}
13
13
  /**
14
14
  * @internal
15
15
  */
16
- export const POPOVER_MOTION_CONTENT_OPACITY_PROPERTY = '--motion-content-opacity'
16
+ export const POPOVER_MOTION_CONTENT_OPACITY_PROPERTY = '--motion-content-opacity' as string
17
17
 
18
18
  /**
19
19
  * Shared `framer-motion` variants used by `Popover` and `Tooltip` components.
@@ -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
  }
@@ -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
 
@@ -1,4 +1,4 @@
1
- import {forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'
1
+ import {forwardRef, useImperativeHandle, useMemo, useState} from 'react'
2
2
  import {useElementSize} from '../../hooks'
3
3
  import {useTheme_v2} from '../../theme'
4
4
  import {findMaxBreakpoints, findMinBreakpoints} from './helpers'
@@ -21,31 +21,30 @@ export const ElementQuery = forwardRef(function ElementQuery(
21
21
  forwardedRef: React.ForwardedRef<HTMLDivElement>,
22
22
  ) {
23
23
  const theme = useTheme_v2()
24
- const {children, media = theme.media, ...restProps} = props
24
+ const {children, media: _media, ...restProps} = props
25
+ const media = _media ?? theme.media
25
26
 
26
- const ref = useRef<HTMLDivElement | null>(null)
27
27
  const [element, setElement] = useState<HTMLDivElement | null>(null)
28
28
  const elementSize = useElementSize(element)
29
29
  const width = useMemo(() => elementSize?.border.width ?? window.innerWidth, [elementSize])
30
30
 
31
- const max = useMemo(() => findMaxBreakpoints(media, width), [media, width])
32
- const min = useMemo(() => findMinBreakpoints(media, width), [media, width])
31
+ const max = useMemo(() => {
32
+ const eq = findMaxBreakpoints(media, width)
33
33
 
34
- useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(forwardedRef, () => ref.current)
34
+ return eq.length ? eq.join(' ') : undefined
35
+ }, [media, width])
36
+ const min = useMemo(() => {
37
+ const eq = findMinBreakpoints(media, width)
35
38
 
36
- const setRef = useCallback((el: HTMLDivElement | null) => {
37
- ref.current = el
38
- setElement(el)
39
- }, [])
39
+ return eq.length ? eq.join(' ') : undefined
40
+ }, [media, width])
41
+
42
+ useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(forwardedRef, () => element, [
43
+ element,
44
+ ])
40
45
 
41
46
  return (
42
- <div
43
- data-ui="ElementQuery"
44
- {...restProps}
45
- data-eq-max={max.length ? max.join(' ') : undefined}
46
- data-eq-min={min.length ? min.join(' ') : undefined}
47
- ref={setRef}
48
- >
47
+ <div data-ui="ElementQuery" {...restProps} data-eq-max={max} data-eq-min={min} ref={setElement}>
49
48
  {children}
50
49
  </div>
51
50
  )
@@ -85,7 +85,7 @@ export function LayerProvider(props: LayerProviderProps): React.ReactElement {
85
85
  parentDispose?.()
86
86
  }
87
87
  },
88
- [parentRegisterChild],
88
+ [parentRegisterChild, setSize, setChildLayers],
89
89
  )
90
90
 
91
91
  // Register this layer on mount