@sanity/ui 2.9.0-canary.6 → 2.10.0-corel.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 (75) hide show
  1. package/dist/_chunks-cjs/getTheme_v2.js +54 -84
  2. package/dist/_chunks-cjs/getTheme_v2.js.map +1 -1
  3. package/dist/_chunks-es/getTheme_v2.mjs +54 -84
  4. package/dist/_chunks-es/getTheme_v2.mjs.map +1 -1
  5. package/dist/_legacy/getTheme_v2.esm.js +54 -84
  6. package/dist/_legacy/getTheme_v2.esm.js.map +1 -1
  7. package/dist/index.d.mts +21 -14
  8. package/dist/index.d.ts +21 -14
  9. package/dist/index.esm.js +3150 -3805
  10. package/dist/index.esm.js.map +1 -1
  11. package/dist/index.js +3148 -3802
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +3150 -3805
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/theme.d.mts +22 -28
  16. package/dist/theme.d.ts +22 -28
  17. package/dist/theme.esm.js +630 -1318
  18. package/dist/theme.esm.js.map +1 -1
  19. package/dist/theme.js +630 -1318
  20. package/dist/theme.js.map +1 -1
  21. package/dist/theme.mjs +630 -1318
  22. package/dist/theme.mjs.map +1 -1
  23. package/package.json +1 -2
  24. package/src/core/__workshop__/constants.ts +13 -13
  25. package/src/core/components/autocomplete/autocomplete.tsx +31 -42
  26. package/src/core/components/breadcrumbs/__workshop__/buttons.tsx +58 -0
  27. package/src/core/components/breadcrumbs/__workshop__/example.tsx +3 -1
  28. package/src/core/components/breadcrumbs/__workshop__/index.ts +5 -0
  29. package/src/core/components/breadcrumbs/breadcrumbs.styles.ts +1 -13
  30. package/src/core/components/breadcrumbs/breadcrumbs.tsx +70 -50
  31. package/src/core/components/dialog/dialog.tsx +3 -5
  32. package/src/core/components/menu/menu.tsx +11 -15
  33. package/src/core/components/menu/menuButton.tsx +36 -14
  34. package/src/core/components/menu/menuGroup.tsx +1 -2
  35. package/src/core/components/menu/menuItem.tsx +2 -4
  36. package/src/core/components/menu/useMenuController.ts +21 -18
  37. package/src/core/components/tab/tabList.tsx +11 -3
  38. package/src/core/components/toast/toastProvider.tsx +6 -5
  39. package/src/core/components/tree/treeItem.tsx +15 -18
  40. package/src/core/constants.ts +1 -1
  41. package/src/core/hooks/useArrayProp.ts +9 -14
  42. package/src/core/hooks/useMatchMedia.ts +22 -12
  43. package/src/core/primitives/avatar/avatarStack.tsx +5 -2
  44. package/src/core/primitives/button/__workshop__/props.tsx +1 -1
  45. package/src/core/primitives/button/button.tsx +11 -6
  46. package/src/core/primitives/button/styles.ts +1 -1
  47. package/src/core/primitives/flex/flex.tsx +5 -1
  48. package/src/core/primitives/heading/heading.tsx +1 -1
  49. package/src/core/primitives/popover/popover.tsx +139 -74
  50. package/src/core/primitives/popover/popoverCard.tsx +4 -1
  51. package/src/core/primitives/text/styles.ts +1 -0
  52. package/src/core/primitives/text/text.tsx +4 -2
  53. package/src/core/primitives/tooltip/tooltip.tsx +17 -47
  54. package/src/core/styles/card/_cardColorStyle.ts +8 -0
  55. package/src/core/styles/flex/flexStyle.ts +18 -0
  56. package/src/core/styles/flex/types.ts +2 -0
  57. package/src/core/theme/themeProvider.tsx +6 -4
  58. package/src/core/utils/elementQuery/elementQuery.tsx +17 -16
  59. package/src/core/utils/layer/layerProvider.tsx +1 -1
  60. package/src/core/utils/portal/portalProvider.tsx +5 -5
  61. package/src/theme/build/_deprecated/color/factory.ts +1 -1
  62. package/src/theme/build/_deprecated/color/muted/createMuted.ts +2 -2
  63. package/src/theme/build/buildColorTheme.ts +45 -39
  64. package/src/theme/build/renderColorTheme.ts +239 -65
  65. package/src/theme/build/resolveColorTokens.ts +51 -115
  66. package/src/theme/config/tokens/color/types.ts +4 -18
  67. package/src/theme/defaults/colorTokens.ts +19 -11
  68. package/src/theme/getScopedTheme.ts +9 -2
  69. package/src/theme/system/color/_constants.ts +8 -0
  70. package/src/theme/system/color/badge.ts +3 -7
  71. package/src/theme/system/v0/color/_system.ts +8 -4
  72. package/src/theme/system/v0/color/color.ts +3 -2
  73. package/src/theme/versioning/themeColor_v0_v2.ts +26 -0
  74. package/src/theme/versioning/v0_v2.ts +4 -0
  75. package/src/core/components/toast/toastState.ts +0 -6
@@ -169,7 +169,7 @@ export const MenuButton = forwardRef(function MenuButton(
169
169
  }, [buttonElement, disableRestoreFocusOnClose])
170
170
 
171
171
  const handleBlur = useCallback(
172
- (event: FocusEvent) => {
172
+ (event: React.FocusEvent<HTMLButtonElement>) => {
173
173
  const target = event.relatedTarget
174
174
 
175
175
  if (!(target instanceof Node)) {
@@ -199,19 +199,32 @@ export const MenuButton = forwardRef(function MenuButton(
199
199
  return () => setChildMenuElements((els) => els.filter((_el) => _el !== el))
200
200
  }, [])
201
201
 
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
- }
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
+ )
212
224
 
213
225
  const menu = menuProp && cloneElement(menuProp, menuProps)
214
226
 
227
+ const ref = useRef<HTMLButtonElement | null>(null)
215
228
  const button = useMemo(
216
229
  () =>
217
230
  buttonProp &&
@@ -223,7 +236,7 @@ export const MenuButton = forwardRef(function MenuButton(
223
236
  'onMouseDown': handleMouseDown,
224
237
  'aria-haspopup': true,
225
238
  'aria-expanded': open,
226
- 'ref': setButtonElement,
239
+ 'ref': ref,
227
240
  'selected': buttonProp.props.selected ?? open,
228
241
  }),
229
242
  [buttonProp, handleButtonClick, handleButtonKeyDown, handleMouseDown, id, open],
@@ -232,10 +245,19 @@ export const MenuButton = forwardRef(function MenuButton(
232
245
  // Forward button ref to parent
233
246
  useImperativeHandle<HTMLButtonElement | null, HTMLButtonElement | null>(
234
247
  forwardedRef,
235
- () => buttonElement,
236
- [buttonElement],
248
+ () => ref.current,
237
249
  )
238
250
 
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
+
239
261
  const popoverProps: MenuButtonProps['popover'] = useMemo(
240
262
  () => ({
241
263
  boundaryElement: deprecated_boundaryElement,
@@ -53,10 +53,9 @@ export function MenuGroup(
53
53
  onClickOutside,
54
54
  onEscape,
55
55
  onItemClick,
56
- onItemMouseEnter: _onItemMouseEnter,
56
+ onItemMouseEnter = menu.onMouseEnter,
57
57
  registerElement,
58
58
  } = menu
59
- const onItemMouseEnter = _onItemMouseEnter ?? menu.onMouseEnter
60
59
  const [rootElement, setRootElement] = useState<HTMLButtonElement | HTMLDivElement | null>(null)
61
60
  const [open, setOpen] = useState(false)
62
61
  const [shouldFocus, setShouldFocus] = useState<'first' | 'last' | null>(null)
@@ -72,11 +72,9 @@ export const MenuItem = forwardRef(function MenuItem(
72
72
  activeElement,
73
73
  mount,
74
74
  onItemClick,
75
- onItemMouseEnter: _onItemMouseEnter,
76
- onItemMouseLeave: _onItemMouseLeave,
75
+ onItemMouseEnter = menu.onMouseEnter,
76
+ onItemMouseLeave = menu.onMouseLeave,
77
77
  } = menu
78
- const onItemMouseEnter = _onItemMouseEnter ?? menu.onMouseEnter
79
- const onItemMouseLeave = _onItemMouseLeave ?? menu.onMouseLeave
80
78
  const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
81
79
  const active = Boolean(activeElement) && activeElement === rootElement
82
80
  const ref = useRef<HTMLDivElement | null>(null)
@@ -36,28 +36,31 @@ export function useMenuController(props: {
36
36
  activeIndexRef.current = nextActiveIndex
37
37
  }, [])
38
38
 
39
- const mount = (element: HTMLElement | null, selected?: boolean): (() => void) => {
40
- if (!element) return () => undefined
39
+ const mount = useCallback(
40
+ (element: HTMLElement | null, selected?: boolean): (() => void) => {
41
+ if (!element) return () => undefined
41
42
 
42
- if (elementsRef.current.indexOf(element) === -1) {
43
- elementsRef.current.push(element)
44
- _sortElements(rootElementRef.current, elementsRef.current)
45
- }
43
+ if (elementsRef.current.indexOf(element) === -1) {
44
+ elementsRef.current.push(element)
45
+ _sortElements(rootElementRef.current, elementsRef.current)
46
+ }
46
47
 
47
- if (selected) {
48
- const selectedIndex = elementsRef.current.indexOf(element)
48
+ if (selected) {
49
+ const selectedIndex = elementsRef.current.indexOf(element)
49
50
 
50
- setActiveIndex(selectedIndex)
51
- }
51
+ setActiveIndex(selectedIndex)
52
+ }
52
53
 
53
- return () => {
54
- const idx = elementsRef.current.indexOf(element)
54
+ return () => {
55
+ const idx = elementsRef.current.indexOf(element)
55
56
 
56
- if (idx > -1) {
57
- elementsRef.current.splice(idx, 1)
57
+ if (idx > -1) {
58
+ elementsRef.current.splice(idx, 1)
59
+ }
58
60
  }
59
- }
60
- }
61
+ },
62
+ [rootElementRef, setActiveIndex],
63
+ )
61
64
 
62
65
  const handleKeyDown = useCallback(
63
66
  (event: React.KeyboardEvent<HTMLDivElement>) => {
@@ -167,7 +170,7 @@ export function useMenuController(props: {
167
170
  [setActiveIndex],
168
171
  )
169
172
 
170
- const handleItemMouseLeave = () => {
173
+ const handleItemMouseLeave = useCallback(() => {
171
174
  // Set the active index to -2 to deactivate all menu items
172
175
  // when the user moves the mouse away from the menu item.
173
176
  // We avoid using -1 because it would focus the first menu item,
@@ -175,7 +178,7 @@ export function useMenuController(props: {
175
178
  // between two menu items or a menu divider.
176
179
  setActiveIndex(-2)
177
180
  rootElementRef.current?.focus()
178
- }
181
+ }, [rootElementRef, setActiveIndex])
179
182
 
180
183
  // Set focus on the currently active element
181
184
  useEffect(() => {
@@ -1,4 +1,4 @@
1
- import {cloneElement, forwardRef, useCallback, useState, Children, isValidElement} from 'react'
1
+ import {cloneElement, forwardRef, useCallback, useMemo, useState} from 'react'
2
2
  import {styled} from 'styled-components'
3
3
  import {Inline, InlineProps} from '../../primitives'
4
4
 
@@ -9,6 +9,10 @@ export interface TabListProps extends Omit<InlineProps, 'as' | 'height'> {
9
9
  children: Array<React.ReactElement | null | undefined | false>
10
10
  }
11
11
 
12
+ function _isReactElement(node: unknown): node is React.ReactElement {
13
+ return Boolean(node)
14
+ }
15
+
12
16
  //Limits the width of tabs in tablist
13
17
  const CustomInline = styled(Inline)`
14
18
  & > div {
@@ -29,18 +33,22 @@ export const TabList = forwardRef(function TabList(
29
33
  const {children: childrenProp, ...restProps} = props
30
34
  const [focusedIndex, setFocusedIndex] = useState(-1)
31
35
 
32
- const children: React.ReactElement[] = Children.toArray(childrenProp).filter(isValidElement)
36
+ const children = useMemo(() => childrenProp.filter(_isReactElement), [childrenProp])
33
37
 
34
38
  const tabs = children.map((child, childIndex) =>
35
39
  cloneElement(child, {
36
40
  focused: focusedIndex === childIndex,
37
41
  key: childIndex,
38
- onFocus: () => setFocusedIndex(childIndex),
42
+ onFocus: () => handleTabFocus(childIndex),
39
43
  }),
40
44
  )
41
45
 
42
46
  const numTabs = tabs.length
43
47
 
48
+ const handleTabFocus = useCallback((tabIdx: number) => {
49
+ setFocusedIndex(tabIdx)
50
+ }, [])
51
+
44
52
  const handleKeyDown = useCallback(
45
53
  (event: React.KeyboardEvent<HTMLDivElement>) => {
46
54
  if (event.key === 'ArrowLeft') {
@@ -8,7 +8,6 @@ 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'
12
11
  import {ToastContextValue, ToastParams} from './types'
13
12
 
14
13
  type ToastState = {
@@ -46,6 +45,8 @@ const ToastContainer = styled.div`
46
45
  width: 100%;
47
46
  `
48
47
 
48
+ let toastId = 0
49
+
49
50
  /**
50
51
  * @public
51
52
  */
@@ -59,20 +60,20 @@ export function ToastProvider(props: ToastProviderProps): React.ReactElement {
59
60
  () => ({
60
61
  initial: {
61
62
  opacity: 0,
62
- [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY]: 0,
63
+ [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY as string]: 0,
63
64
  y: 32,
64
65
  scale: 0.25,
65
66
  willChange: 'transform',
66
67
  },
67
68
  animate: {
68
69
  opacity: [0, 1, 1],
69
- [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY]: [0, 0, 1],
70
+ [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY as string]: [0, 0, 1],
70
71
  y: 0,
71
72
  scale: 1,
72
73
  },
73
74
  exit: {
74
75
  opacity: [1, 1, 0],
75
- [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY]: [1, 0, 0],
76
+ [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY as string]: [1, 0, 0],
76
77
  scale: 0.5,
77
78
  transition: prefersReducedMotion ? {duration: 0} : {duration: 0.2},
78
79
  },
@@ -85,7 +86,7 @@ export function ToastProvider(props: ToastProviderProps): React.ReactElement {
85
86
  // Wrap setState in startTransition to allow React to give input state updates higher priority
86
87
  const setState: typeof _setState = (state) => startTransition(() => _setState(state))
87
88
 
88
- const id = params.id || generateToastId()
89
+ const id = params.id || String(toastId++)
89
90
  const duration = params.duration || 5000
90
91
 
91
92
  const dismiss = () => {
@@ -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, useState} from 'react'
3
+ import {memo, useCallback, useEffect, useId, useMemo, useRef} from 'react'
4
4
  import {styled} from 'styled-components'
5
5
  import {Box, BoxProps, Flex, Text} from '../../primitives'
6
6
  import {
@@ -64,21 +64,18 @@ export const TreeItem = memo(function TreeItem(
64
64
  weight,
65
65
  ...restProps
66
66
  } = props
67
- const [rootElement, setRootElement] = useState<HTMLLIElement | null>(null)
67
+ const rootRef = useRef<HTMLLIElement | null>(null)
68
68
  const treeitemRef = useRef<HTMLDivElement | null>(null)
69
69
  const tree = useTree()
70
70
  const {path, registerItem, setExpanded, setFocusedElement} = tree
71
71
  const _id = useId()
72
72
  const id = idProp || _id
73
- const [itemPath, itemKey] = useMemo(() => {
74
- const result = path.concat([id || ''])
75
-
76
- return [result, result.join('/')]
77
- }, [id, path])
73
+ const itemPath = useMemo(() => path.concat([id || '']), [id, path])
74
+ const itemKey = itemPath.join('/')
78
75
  const itemState = tree.state[itemKey]
79
- const focused = tree.focusedElement === rootElement
76
+ const focused = tree.focusedElement === rootRef.current
80
77
  const expanded = itemState?.expanded === undefined ? expandedProp : itemState?.expanded || false
81
- const tabIndex = tree.focusedElement && tree.focusedElement === rootElement ? 0 : -1
78
+ const tabIndex = tree.focusedElement && tree.focusedElement === rootRef.current ? 0 : -1
82
79
  const contextValue = useMemo(
83
80
  () => ({...tree, level: tree.level + 1, path: itemPath}),
84
81
  [itemPath, tree],
@@ -97,28 +94,28 @@ export const TreeItem = memo(function TreeItem(
97
94
  ) {
98
95
  event.stopPropagation()
99
96
  setExpanded(itemKey, !expanded)
100
- setFocusedElement(rootElement)
97
+ setFocusedElement(rootRef.current)
101
98
  }
102
99
  },
103
- [expanded, itemKey, onClick, rootElement, setExpanded, setFocusedElement],
100
+ [expanded, itemKey, onClick, setExpanded, setFocusedElement],
104
101
  )
105
102
 
106
103
  const handleKeyDown = useCallback(
107
104
  (event: React.KeyboardEvent<HTMLElement>) => {
108
105
  if (focused && event.key === 'Enter') {
109
- const el = treeitemRef.current || rootElement
106
+ const el = treeitemRef.current || rootRef.current
110
107
 
111
108
  el?.click()
112
109
  }
113
110
  },
114
- [focused, rootElement],
111
+ [focused],
115
112
  )
116
113
 
117
114
  useEffect(() => {
118
- if (!rootElement) return
115
+ if (!rootRef.current) return
119
116
 
120
- return registerItem(rootElement, itemPath.join('/'), expanded, selected)
121
- }, [expanded, itemPath, registerItem, rootElement, selected])
117
+ return registerItem(rootRef.current, itemPath.join('/'), expanded, selected)
118
+ }, [expanded, itemPath, registerItem, selected])
122
119
 
123
120
  const content = (
124
121
  <Flex padding={padding}>
@@ -157,7 +154,7 @@ export const TreeItem = memo(function TreeItem(
157
154
  data-ui="TreeItem"
158
155
  {...restProps}
159
156
  onClick={handleClick}
160
- ref={setRootElement}
157
+ ref={rootRef}
161
158
  role="none"
162
159
  >
163
160
  <TreeItemBox
@@ -190,7 +187,7 @@ export const TreeItem = memo(function TreeItem(
190
187
  aria-expanded={expanded}
191
188
  onClick={handleClick}
192
189
  onKeyDown={handleKeyDown}
193
- ref={setRootElement}
190
+ ref={rootRef}
194
191
  role="treeitem"
195
192
  tabIndex={tabIndex}
196
193
  >
@@ -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' as string
16
+ export const POPOVER_MOTION_CONTENT_OPACITY_PROPERTY = '--motion-content-opacity'
17
17
 
18
18
  /**
19
19
  * Shared `framer-motion` variants used by `Popover` and `Tooltip` components.
@@ -1,4 +1,4 @@
1
- import {useState} from 'react'
1
+ import {useMemo} from 'react'
2
2
  import {_getArrayProp} from '../styles'
3
3
 
4
4
  /** @beta */
@@ -12,19 +12,14 @@ export function useArrayProp<T extends ArrayPropPrimitive = ArrayPropPrimitive>(
12
12
  val: T | T[] | undefined,
13
13
  defaultVal?: T[],
14
14
  ): T[] {
15
- const [[cachedVal, cachedHash], setCache] = useState<[T[], string]>(() => [
16
- _getArrayProp(val, defaultVal),
17
- JSON.stringify(val ?? defaultVal),
18
- ])
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])
19
17
 
20
- const hash = JSON.stringify(val ?? defaultVal)
18
+ return useMemo(
19
+ () => _getArrayProp(val, defaultVal),
21
20
 
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
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
+ )
30
25
  }
@@ -11,24 +11,34 @@ 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
- )
22
14
  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
+
23
31
  return {
24
32
  subscribe: (onStoreChange: () => void): (() => void) => {
25
- cachedMatchMedia!.addEventListener('change', onStoreChange)
33
+ const matchMedia = getMatchMedia()
34
+
35
+ matchMedia.addEventListener('change', onStoreChange)
26
36
 
27
- return () => cachedMatchMedia!.removeEventListener('change', onStoreChange)
37
+ return () => matchMedia.removeEventListener('change', onStoreChange)
28
38
  },
29
- getSnapshot: () => cachedMatchMedia!.matches,
39
+ getSnapshot: () => getMatchMedia().matches,
30
40
  }
31
- }, [cachedMatchMedia])
41
+ }, [mediaQueryString])
32
42
 
33
43
  useDebugValue(mediaQueryString)
34
44
 
@@ -1,5 +1,5 @@
1
1
  import {getTheme_v2} from '@sanity/ui/theme'
2
- import {Children, cloneElement, forwardRef, isValidElement} from 'react'
2
+ import {Children, cloneElement, forwardRef, isValidElement, useMemo} from 'react'
3
3
  import {styled, css} from 'styled-components'
4
4
  import {EMPTY_RECORD} from '../../constants'
5
5
  import {useArrayProp} from '../../hooks'
@@ -65,7 +65,10 @@ export const AvatarStack = forwardRef(function AvatarStack(
65
65
  size: sizeProp = 1,
66
66
  ...restProps
67
67
  } = props
68
- const children: React.ReactElement[] = Children.toArray(childrenProp).filter(isValidElement)
68
+ const children = useMemo<React.ReactElement[]>(
69
+ () => Children.toArray(childrenProp).filter(isValidElement),
70
+ [childrenProp],
71
+ )
69
72
  const maxLength = Math.max(maxLengthProp, 0)
70
73
  const size = useArrayProp(sizeProp)
71
74
 
@@ -13,7 +13,7 @@ import {
13
13
 
14
14
  export default function ButtonStory() {
15
15
  const disabled = useBoolean('Disabled', false, 'Props')
16
- const fontSize = useSelect('Font size', WORKSHOP_TEXT_SIZE_OPTIONS, 2, 'Props')
16
+ const fontSize = useSelect('Font size', WORKSHOP_TEXT_SIZE_OPTIONS, 1, 'Props')
17
17
  const icon = useSelect('Icon', WORKSHOP_ICON_SYMBOL_OPTIONS, 'add-circle', 'Props')
18
18
  const iconRight = useSelect('Icon (right)', WORKSHOP_ICON_SYMBOL_OPTIONS, '', 'Props')
19
19
  const justify = useSelect('Justify', WORKSHOP_FLEX_JUSTIFY_OPTIONS, 'center', 'Props')
@@ -9,7 +9,7 @@ import {ButtonMode, ButtonTextAlign, ButtonTone, ButtonWidth, FlexJustify} from
9
9
  import {Box} from '../box'
10
10
  import {Flex} from '../flex'
11
11
  import {Spinner} from '../spinner'
12
- import {Text} from '../text'
12
+ import {Text, TextProps} from '../text'
13
13
  import {ResponsivePaddingProps, ResponsiveRadiusProps} from '../types'
14
14
  import {buttonBaseStyles, buttonColorStyles} from './styles'
15
15
 
@@ -32,6 +32,8 @@ export interface ButtonProps extends ResponsivePaddingProps, ResponsiveRadiusPro
32
32
  textAlign?: ButtonTextAlign
33
33
  muted?: boolean
34
34
  text?: React.ReactNode
35
+ textOverflow?: TextProps['textOverflow']
36
+ textWeight?: TextProps['weight']
35
37
  tone?: ButtonTone
36
38
  type?: 'button' | 'reset' | 'submit'
37
39
  width?: ButtonWidth
@@ -85,6 +87,8 @@ export const Button = forwardRef(function Button(
85
87
  space: spaceProp = 3,
86
88
  text,
87
89
  textAlign,
90
+ textOverflow = 'ellipsis',
91
+ textWeight,
88
92
  tone = 'default',
89
93
  type = 'button',
90
94
  muted = false,
@@ -142,20 +146,21 @@ export const Button = forwardRef(function Button(
142
146
  <Box as="span" {...boxProps}>
143
147
  <Flex as="span" justify={justify} gap={space}>
144
148
  {IconComponent && (
145
- <Text size={fontSize}>
149
+ <Text as="span" size={fontSize} style={{flex: 'none'}}>
146
150
  {isValidElement(IconComponent) && IconComponent}
147
151
  {isValidElementType(IconComponent) && <IconComponent />}
148
152
  </Text>
149
153
  )}
150
154
 
151
155
  {text && (
152
- <Box>
156
+ <Box as="span">
153
157
  <Text
158
+ as="span"
154
159
  muted={muted}
155
160
  align={textAlign}
156
161
  size={fontSize}
157
- textOverflow="ellipsis"
158
- weight={button.textWeight}
162
+ textOverflow={textOverflow}
163
+ weight={textWeight ?? button.textWeight}
159
164
  >
160
165
  {text}
161
166
  </Text>
@@ -163,7 +168,7 @@ export const Button = forwardRef(function Button(
163
168
  )}
164
169
 
165
170
  {IconRightComponent && (
166
- <Text size={fontSize}>
171
+ <Text as="span" size={fontSize} style={{flex: 'none'}}>
167
172
  {isValidElement(IconRightComponent) && IconRightComponent}
168
173
  {isValidElementType(IconRightComponent) && <IconRightComponent />}
169
174
  </Text>
@@ -29,8 +29,8 @@ export function buttonBaseStyles(
29
29
  border: 0;
30
30
  box-sizing: border-box;
31
31
  padding: 0;
32
+ overflow: hidden;
32
33
  margin: 0;
33
- white-space: nowrap;
34
34
  text-align: left;
35
35
  position: relative;
36
36
  vertical-align: top;
@@ -18,6 +18,8 @@ export interface FlexProps
18
18
  ResponsiveFlexProps,
19
19
  ResponsiveFlexItemProps {
20
20
  gap?: number | number[]
21
+ gapX?: number | number[]
22
+ gapY?: number | number[]
21
23
  }
22
24
 
23
25
  const Root = styled(Box)<FlexItemStyleProps & ResponsiveFlexStyleProps>(
@@ -34,7 +36,7 @@ export const Flex = forwardRef(function Flex(
34
36
  props: FlexProps & Omit<React.HTMLProps<HTMLDivElement>, 'as' | 'wrap'>,
35
37
  ref: React.ForwardedRef<HTMLDivElement>,
36
38
  ) {
37
- const {align, as, direction = 'row', gap, justify, wrap, ...restProps} = props
39
+ const {align, as, direction = 'row', gap, gapX, gapY, justify, wrap, ...restProps} = props
38
40
 
39
41
  return (
40
42
  <Root
@@ -43,6 +45,8 @@ export const Flex = forwardRef(function Flex(
43
45
  $align={useArrayProp(align)}
44
46
  $direction={useArrayProp(direction)}
45
47
  $gap={useArrayProp(gap)}
48
+ $gapX={useArrayProp(gapX)}
49
+ $gapY={useArrayProp(gapY)}
46
50
  $justify={useArrayProp(justify)}
47
51
  $wrap={useArrayProp(wrap)}
48
52
  forwardedAs={as}
@@ -26,7 +26,7 @@ export interface HeadingProps {
26
26
  * Use `textOverflow="ellipsis"` to render text as a single line which is concatenated with a `…` symbol.
27
27
  * @beta
28
28
  */
29
- textOverflow?: 'ellipsis'
29
+ textOverflow?: 'ellipsis' | 'none'
30
30
  weight?: ThemeFontWeightKey
31
31
  }
32
32