@sanity/ui 2.6.4-canary.0 → 2.6.4-canary.2

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 (55) hide show
  1. package/dist/index.d.mts +51 -15
  2. package/dist/index.d.ts +51 -15
  3. package/dist/index.esm.js +103 -167
  4. package/dist/index.esm.js.map +1 -1
  5. package/dist/index.js +102 -167
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +103 -167
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/theme.d.mts +14 -14
  10. package/dist/theme.d.ts +14 -14
  11. package/dist/theme.esm.js.map +1 -1
  12. package/dist/theme.js.map +1 -1
  13. package/dist/theme.mjs.map +1 -1
  14. package/package.json +28 -27
  15. package/src/core/__workshop__/constants.ts +36 -36
  16. package/src/core/components/autocomplete/autocomplete.tsx +1 -1
  17. package/src/core/components/breadcrumbs/breadcrumbs.tsx +15 -6
  18. package/src/core/components/dialog/dialog.tsx +12 -21
  19. package/src/core/components/dialog/styles.ts +8 -8
  20. package/src/core/components/menu/menu.tsx +11 -18
  21. package/src/core/components/menu/menuButton.tsx +11 -11
  22. package/src/core/components/menu/menuContext.ts +1 -1
  23. package/src/core/components/menu/useMenuController.ts +11 -18
  24. package/src/core/components/tab/tab.tsx +8 -8
  25. package/src/core/components/tab/tabPanel.tsx +1 -1
  26. package/src/core/components/toast/useToast.ts +1 -0
  27. package/src/core/components/tree/tree.tsx +1 -0
  28. package/src/core/components/tree/treeItem.tsx +1 -0
  29. package/src/core/hooks/index.ts +3 -2
  30. package/src/core/hooks/useClickOutside.ts +37 -74
  31. package/src/core/hooks/useMatchMedia.ts +46 -0
  32. package/src/core/hooks/useMediaIndex/useMediaIndex.ts +2 -11
  33. package/src/core/hooks/usePrefersDark.ts +10 -58
  34. package/src/core/hooks/usePrefersReducedMotion.ts +10 -58
  35. package/src/core/primitives/avatar/styles.ts +42 -42
  36. package/src/core/primitives/badge/styles.ts +2 -2
  37. package/src/core/primitives/button/styles.ts +4 -4
  38. package/src/core/primitives/inline/styles.ts +2 -2
  39. package/src/core/primitives/popover/__workshop__/AlignedStory.tsx +9 -7
  40. package/src/core/primitives/popover/constants.ts +4 -4
  41. package/src/core/primitives/stack/styles.ts +2 -2
  42. package/src/core/primitives/tooltip/__workshop__/customPortal.tsx +9 -7
  43. package/src/core/primitives/tooltip/constants.ts +4 -4
  44. package/src/core/primitives/tooltip/tooltip.tsx +45 -27
  45. package/src/core/primitives/tooltip/tooltipDelayGroup/tooltipDelayGroupProvider.tsx +1 -0
  46. package/src/core/styles/font/responsiveFont.ts +8 -8
  47. package/src/core/theme/__workshop__/debug/story.tsx +11 -11
  48. package/src/core/utils/layer/layerProvider.tsx +1 -0
  49. package/src/core/utils/portal/__workshop__/named.tsx +10 -8
  50. package/src/core/utils/portal/portalProvider.tsx +7 -14
  51. package/src/theme/build/buildColorTheme.test.ts +13 -13
  52. package/src/theme/config/tokens/color/types.ts +14 -14
  53. package/src/theme/defaults/colorTokens.ts +36 -36
  54. package/src/core/hooks/_internal/index.ts +0 -1
  55. package/src/core/hooks/_internal/useUnique.ts +0 -34
@@ -76,6 +76,7 @@ export const TreeItem = memo(function TreeItem(
76
76
  const focused = tree.focusedElement === rootRef.current
77
77
  const expanded = itemState?.expanded === undefined ? expandedProp : itemState?.expanded || false
78
78
  const tabIndex = tree.focusedElement && tree.focusedElement === rootRef.current ? 0 : -1
79
+ // @TODO split out into separate contexts
79
80
  const contextValue = useMemo(
80
81
  () => ({...tree, level: tree.level + 1, path: itemPath}),
81
82
  [itemPath, tree],
@@ -1,10 +1,11 @@
1
1
  export * from './useArrayProp'
2
2
  export * from './useClickOutside'
3
+ export * from './useCustomValidity'
3
4
  export * from './useElementRect'
4
5
  export * from './useElementSize'
6
+ export * from './useForwardedRef'
5
7
  export * from './useGlobalKeyDown'
8
+ export * from './useMatchMedia'
6
9
  export * from './useMediaIndex'
7
10
  export * from './usePrefersDark'
8
11
  export * from './usePrefersReducedMotion'
9
- export * from './useForwardedRef'
10
- export * from './useCustomValidity'
@@ -1,102 +1,65 @@
1
- import {useEffect, useRef, useState} from 'react'
1
+ import {useEffect} from 'react'
2
+ import {useEffectEvent} from 'use-effect-event'
2
3
  import {EMPTY_ARRAY} from '../constants'
3
4
 
4
5
  /**
5
6
  * @public
6
7
  */
7
- export type ClickOutsideListener = (event: MouseEvent) => void
8
-
9
- function _getElements(
10
- element: HTMLElement | null,
11
- elementsArg: Array<HTMLElement | HTMLElement[] | null>,
12
- ): HTMLElement[] {
13
- const ret = [element]
14
-
15
- for (const el of elementsArg) {
16
- if (Array.isArray(el)) {
17
- ret.push(...el)
18
- } else {
19
- ret.push(el)
20
- }
21
- }
22
-
23
- return ret.filter(Boolean) as HTMLElement[]
24
- }
8
+ export type ClickOutsideListener = (event: MouseEvent | TouchEvent) => void
25
9
 
26
10
  /**
11
+ * Use the callback version of `elementsArg` if you're using `useRef` to handle elements:
12
+ * ```tsx
13
+ * useClickOutside(
14
+ * () => {},
15
+ * () => [ref.current],
16
+ * )
17
+ *
27
18
  * @public
28
19
  */
29
20
  export function useClickOutside(
30
21
  listener: ClickOutsideListener,
31
- elementsArg: Array<HTMLElement | HTMLElement[] | null> = EMPTY_ARRAY,
22
+ elementsArg:
23
+ | Array<HTMLElement | HTMLElement[] | null>
24
+ | (() => Array<HTMLElement | HTMLElement[] | null>) = EMPTY_ARRAY,
32
25
  boundaryElement?: HTMLElement | null,
33
- ): (el: HTMLElement | null) => void {
34
- const [element, setElement] = useState<HTMLElement | null>(null)
35
- const [elements, setElements] = useState(() => _getElements(element, elementsArg))
36
- const elementsRef = useRef(elements)
37
-
38
- useEffect(() => {
39
- const prevElements = elementsRef.current
40
- const nextElements = _getElements(element, elementsArg)
41
-
42
- if (prevElements.length !== nextElements.length) {
43
- setElements(nextElements)
44
- elementsRef.current = nextElements
45
-
26
+ ): void {
27
+ /**
28
+ * The `useEffectEvent` hook allow us to always see the latest value of `listener`, `elementsArg` and `boundaryElement` without needing to
29
+ * juggle `useState`, `useRef` and `useState` to make sure the `mousedown` event listener isn't constantly being added and removed.
30
+ */
31
+ const eventHandler = useEffectEvent((evt: MouseEvent | TouchEvent) => {
32
+ const target = evt.target
33
+
34
+ if (!(target instanceof Node)) {
46
35
  return
47
36
  }
48
37
 
49
- for (const el of prevElements) {
50
- if (!nextElements.includes(el)) {
51
- setElements(nextElements)
52
- elementsRef.current = nextElements
53
-
54
- return
55
- }
56
- }
57
-
58
- for (const el of nextElements) {
59
- if (!prevElements.includes(el)) {
60
- setElements(nextElements)
61
- elementsRef.current = nextElements
62
-
63
- return
64
- }
38
+ if (boundaryElement && !boundaryElement.contains(target)) {
39
+ return
65
40
  }
66
- }, [element, elementsArg])
67
41
 
68
- useEffect(() => {
69
- if (!listener) return undefined
42
+ const resolvedElements = Array.isArray(elementsArg) ? elementsArg : elementsArg()
43
+ const elements = resolvedElements.flat()
70
44
 
71
- const handleWindowMouseDown = (evt: MouseEvent) => {
72
- const target = evt.target
45
+ for (const el of elements) {
46
+ if (!el) continue
73
47
 
74
- if (!(target instanceof Node)) {
48
+ if (target === el || el.contains(target)) {
75
49
  return
76
50
  }
77
-
78
- if (boundaryElement && !boundaryElement.contains(target)) {
79
- return
80
- }
81
-
82
- for (const el of elements) {
83
- if (target === el || el.contains(target)) {
84
- return
85
- }
86
- }
87
-
88
- listener(evt)
89
51
  }
90
52
 
91
- // @TODO seems expensive, improve with useEffectEvent? The identity of boundaryElement, elements and listener probably shouldn't trigger resub
92
- // or maybe it's enough to put listener and elements etc in refs?
53
+ listener(evt)
54
+ })
93
55
 
94
- window.addEventListener('mousedown', handleWindowMouseDown)
56
+ useEffect(() => {
57
+ document.addEventListener('mousedown', eventHandler)
58
+ document.addEventListener('touchstart', eventHandler)
95
59
 
96
60
  return () => {
97
- window.removeEventListener('mousedown', handleWindowMouseDown)
61
+ document.removeEventListener('mousedown', eventHandler)
62
+ document.removeEventListener('touchstart', eventHandler)
98
63
  }
99
- }, [boundaryElement, listener, elements])
100
-
101
- return setElement
64
+ }, [eventHandler])
102
65
  }
@@ -0,0 +1,46 @@
1
+ import {useDebugValue, useMemo, useSyncExternalStore} from 'react'
2
+
3
+ /**
4
+ * Efficiently subscribes to `window.matchMedia` queries
5
+ *
6
+ * @param getServerSnapshot - Only called during server-side rendering, and hydration if using hydrateRoot. Required if the hook is called during SSR (https://react.dev/reference/react/useSyncExternalStore#adding-support-for-server-rendering)
7
+ *
8
+ * @public
9
+ */
10
+ export function useMatchMedia(
11
+ mediaQueryString: `(${string})`,
12
+ getServerSnapshot?: () => boolean,
13
+ ): boolean {
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
+
31
+ return {
32
+ subscribe: (onStoreChange: () => void): (() => void) => {
33
+ const matchMedia = getMatchMedia()
34
+
35
+ matchMedia.addEventListener('change', onStoreChange)
36
+
37
+ return () => matchMedia.removeEventListener('change', onStoreChange)
38
+ },
39
+ getSnapshot: () => getMatchMedia().matches,
40
+ }
41
+ }, [mediaQueryString])
42
+
43
+ useDebugValue(mediaQueryString)
44
+
45
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
46
+ }
@@ -1,4 +1,4 @@
1
- import {useSyncExternalStore} from 'react'
1
+ import {useMemo, useSyncExternalStore} from 'react'
2
2
  import {useTheme_v2} from '../../theme'
3
3
 
4
4
  /**
@@ -9,9 +9,6 @@ export interface _MediaStore {
9
9
  getSnapshot: () => number
10
10
  }
11
11
 
12
- // @TODO probably overkill, why not let React handle the memo?
13
- const MEDIA_STORE_CACHE = new WeakMap<number[], _MediaStore>()
14
-
15
12
  type MediaQueryMinWidth = `(min-width: ${number}px)`
16
13
  type MediaQueryMaxWidth = `(max-width: ${number}px)`
17
14
  type MediaQueryMinMaxWidth = `${MediaQueryMinWidth} and ${MediaQueryMaxWidth}`
@@ -98,13 +95,7 @@ function getServerSnapshot() {
98
95
  */
99
96
  export function useMediaIndex(): number {
100
97
  const {media} = useTheme_v2()
101
-
102
- let store = MEDIA_STORE_CACHE.get(media)
103
-
104
- if (!store) {
105
- store = _createMediaStore(media)
106
- MEDIA_STORE_CACHE.set(media, store)
107
- }
98
+ const store = useMemo(() => _createMediaStore(media), [media])
108
99
 
109
100
  return useSyncExternalStore(store.subscribe, store.getSnapshot, getServerSnapshot)
110
101
  }
@@ -1,64 +1,16 @@
1
- import {useSyncExternalStore} from 'react'
2
-
3
- // @TODO is it really necessary to cache this way?
4
- // also, what about providing a server snapshot pattern?
5
- // that implements the trick vision-on-demand uses?
6
- let MEDIA_QUERY_CACHE: MediaQueryList | undefined
7
-
8
- /**
9
- * Lazy init the matchMedia instance
10
- */
11
- function getMatchMedia(): MediaQueryList {
12
- if (!MEDIA_QUERY_CACHE) {
13
- // As this function is only called during `subscribe` and `getSnapshot`, we can assume that the
14
- // the `window` global is available and we're in a browser environment
15
- MEDIA_QUERY_CACHE = window.matchMedia('(prefers-color-scheme: dark)')
16
- }
17
-
18
- return MEDIA_QUERY_CACHE
19
- }
20
-
21
- /**
22
- * As the query is the same for all instances of this hook, we can cache the matchMedia instance
23
- * and have cheap `change` event listeners, while getSnapshot always reads from the same
24
- * matchMedia instance and we don't get any tearing.
25
- * Tearing in this context means the bad edge case in React concurrent render mdoe
26
- * where you sometimes would end up with some components doing render while seeing `usePrefersDark() === true` while others would see `usePrefersDark() === false`
27
- * during the same render.
28
- * By using `useSyncExternalStore` every component only sees the same value during the same render, and always re-render when it changes no matter
29
- * what React.memo boundaries there might be between the layers..
30
- */
31
- function subscribe(onStoreChange: () => void): () => void {
32
- const matchMedia = getMatchMedia()
33
-
34
- matchMedia.addEventListener('change', onStoreChange)
35
-
36
- return () => matchMedia.removeEventListener('change', onStoreChange)
37
- }
38
-
39
- /**
40
- * Only called client-side, when using createRoot, or after hydration is complete when using hydrateRoot.
41
- * It's important that this function does not create new objects or arrays when called:
42
- * https://beta.reactjs.org/apis/react/useSyncExternalStore#im-getting-an-error-the-result-of-getsnapshot-should-be-cached
43
- */
44
- function getSnapshot() {
45
- return getMatchMedia().matches
46
- }
1
+ import {useMatchMedia} from './useMatchMedia'
47
2
 
48
3
  /**
49
- * Only called during server-side rendering, and hydration if using hydrateRoot
50
- * Since the server environment doesn't have access to the DOM, we can't determine the current value of the media query
51
- * and we assume `(prefers-color-scheme: light)` since it's the most common scheme
4
+ * Returns true if a dark color scheme is preferred, false if a light color scheme is preferred or the preference is not known.
5
+ *
6
+ * @param getServerSnapshot - Only called during server-side rendering, and hydration if using hydrateRoot. Since the server environment doesn't have access to the DOM, we can't determine the current value of the media query and we assume `(prefers-color-scheme: light)` since it's the most common scheme (https://react.dev/reference/react/useSyncExternalStore#adding-support-for-server-rendering)
7
+ *
8
+ * If you persist the detected preference in a cookie or a header then you may implement your own server snapshot to read it.
9
+ * Chrome supports reading the `prefers-color-scheme` media query from a header if the server response: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Color-Scheme
10
+ * @example https://gist.github.com/stipsan/13c0cccf8dfc34f4b44bb1b984baf7df
52
11
  *
53
- * @link https://beta.reactjs.org/apis/react/useSyncExternalStore#adding-support-for-server-rendering
54
- */
55
- function getServerSnapshot() {
56
- return false
57
- }
58
-
59
- /**
60
12
  * @public
61
13
  */
62
- export function usePrefersDark(): boolean {
63
- return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
14
+ export function usePrefersDark(getServerSnapshot = () => false): boolean {
15
+ return useMatchMedia('(prefers-color-scheme: dark)', getServerSnapshot)
64
16
  }
@@ -1,64 +1,16 @@
1
- import {useSyncExternalStore} from 'react'
2
-
3
- // @TODO is the cache here really beneficial? Maybe it's better to use useMemo?
4
- // And how about supplying a server snapshot pattern so userland can implement headers for SSR like vision-on-demand?
5
- let MEDIA_QUERY_CACHE: MediaQueryList | undefined
6
-
7
- /**
8
- * Lazy init the matchMedia instance
9
- */
10
- function getMatchMedia(): MediaQueryList {
11
- if (!MEDIA_QUERY_CACHE) {
12
- // As this function is only called during `subscribe` and `getSnapshot`, we can assume that the
13
- // the `window` global is available and we're in a browser environment
14
- MEDIA_QUERY_CACHE = window.matchMedia('(prefers-reduced-motion: reduce)')
15
- }
16
-
17
- return MEDIA_QUERY_CACHE
18
- }
19
-
20
- /**
21
- * As the query is the same for all instances of this hook, we can cache the matchMedia instance
22
- * and have cheap `change` event listeners, while getSnapshot always reads from the same
23
- * matchMedia instance and we don't get any tearing.
24
- * Tearing in this context means the bad edge case in React concurrent render mdoe
25
- * where you sometimes would end up with some components doing render while seeing `usePrefersDark() === true` while others would see `usePrefersDark() === false`
26
- * during the same render.
27
- * By using `useSyncExternalStore` every component only sees the same value during the same render, and always re-render when it changes no matter
28
- * what React.memo boundaries there might be between the layers..
29
- */
30
- function subscribe(onStoreChange: () => void): () => void {
31
- const matchMedia = getMatchMedia()
32
-
33
- matchMedia.addEventListener('change', onStoreChange)
34
-
35
- return () => matchMedia.removeEventListener('change', onStoreChange)
36
- }
37
-
38
- /**
39
- * Only called client-side, when using createRoot, or after hydration is complete when using hydrateRoot.
40
- * It's important that this function does not create new objects or arrays when called:
41
- * https://beta.reactjs.org/apis/react/useSyncExternalStore#im-getting-an-error-the-result-of-getsnapshot-should-be-cached
42
- */
43
- function getSnapshot() {
44
- return getMatchMedia().matches
45
- }
46
-
47
- /**
48
- * Only called during server-side rendering, and hydration if using hydrateRoot
49
- * Since the server environment doesn't have access to the DOM, we can't determine the current value of the media query
50
- * and we assume `(prefers-reduced-motion: no-preference)` since it's the most common scheme
51
- *
52
- * @link https://beta.reactjs.org/apis/react/useSyncExternalStore#adding-support-for-server-rendering
53
- */
54
- function getServerSnapshot() {
55
- return false
56
- }
1
+ import {useMatchMedia} from './useMatchMedia'
57
2
 
58
3
  /**
59
4
  * Returns true if motion should be reduced
5
+ *
6
+ * @param getServerSnapshot - Only called during server-side rendering, and hydration if using hydrateRoot. Since the server environment doesn't have access to the DOM, we can't determine the current value of the media query and we assume `(prefers-reduced-motion: no-preference)` since it's the most common scheme (https://react.dev/reference/react/useSyncExternalStore#adding-support-for-server-rendering)
7
+ *
8
+ * If you persist the detected preference in a cookie or a header then you may implement your own server snapshot to read it.
9
+ * Chrome supports reading the `prefers-reduced-motion` media query from a header if the server response: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-Prefers-Reduced-Motion
10
+ * @example https://gist.github.com/stipsan/0c0f839a27842249cada893e9fb7767b
11
+ *
60
12
  * @public
61
13
  */
62
- export function usePrefersReducedMotion(): boolean {
63
- return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
14
+ export function usePrefersReducedMotion(getServerSnapshot = () => false): boolean {
15
+ return useMatchMedia('(prefers-reduced-motion: reduce)', getServerSnapshot)
64
16
  }
@@ -14,24 +14,24 @@ export const avatarStyle = {
14
14
 
15
15
  function avatarArrowStyle(): CSSObject {
16
16
  return {
17
- position: 'absolute',
18
- boxSizing: 'border-box',
19
- zIndex: 0,
20
- opacity: 0,
21
- transition: 'all 0.2s linear',
22
- transform: 'rotate(-90deg) translate3d(0, 6px, 0)',
23
- left: 0,
24
- right: 0,
25
- top: 0,
26
- bottom: 0,
17
+ 'position': 'absolute',
18
+ 'boxSizing': 'border-box',
19
+ 'zIndex': 0,
20
+ 'opacity': 0,
21
+ 'transition': 'all 0.2s linear',
22
+ 'transform': 'rotate(-90deg) translate3d(0, 6px, 0)',
23
+ 'left': 0,
24
+ 'right': 0,
25
+ 'top': 0,
26
+ 'bottom': 0,
27
27
 
28
28
  '& > svg': {
29
- width: '11px',
30
- height: '7px',
31
- position: 'absolute',
32
- top: '-5px',
33
- left: '50%',
34
- transform: 'translateX(-6px)',
29
+ 'width': '11px',
30
+ 'height': '7px',
31
+ 'position': 'absolute',
32
+ 'top': '-5px',
33
+ 'left': '50%',
34
+ 'transform': 'translateX(-6px)',
35
35
 
36
36
  '&:not([hidden])': {
37
37
  display: 'block',
@@ -63,11 +63,11 @@ export function avatarRootStyle(props: AvatarRootStyleProps & ThemeProps): CSSOb
63
63
  '--avatar-bg-color': `var(--card-avatar-${$color}-bg-color)`,
64
64
  '--avatar-fg-color': `var(--card-avatar-${$color}-fg-color)`,
65
65
 
66
- backgroundColor: 'var(--avatar-bg-color)',
67
- position: 'relative',
68
- boxSizing: 'border-box',
69
- userSelect: 'none',
70
- boxShadow: '0 0 0 1px var(--card-bg-color)',
66
+ 'backgroundColor': 'var(--avatar-bg-color)',
67
+ 'position': 'relative',
68
+ 'boxSizing': 'border-box',
69
+ 'userSelect': 'none',
70
+ 'boxShadow': '0 0 0 1px var(--card-bg-color)',
71
71
 
72
72
  '&[data-status="inactive"]': {
73
73
  opacity: 0.5,
@@ -81,14 +81,14 @@ export function avatarRootStyle(props: AvatarRootStyleProps & ThemeProps): CSSOb
81
81
 
82
82
  /* &:is(button) */
83
83
  '&[data-as="button"]': {
84
- WebkitFontSmoothing: 'inherit',
85
- appearance: 'none',
86
- margin: 0,
87
- padding: 0,
88
- border: 0,
89
- font: 'inherit',
90
- color: 'inherit',
91
- outline: 'none',
84
+ 'WebkitFontSmoothing': 'inherit',
85
+ 'appearance': 'none',
86
+ 'margin': 0,
87
+ 'padding': 0,
88
+ 'border': 0,
89
+ 'font': 'inherit',
90
+ 'color': 'inherit',
91
+ 'outline': 'none',
92
92
 
93
93
  '&:focus': {
94
94
  boxShadow: focusRingStyle({focusRing: avatar.focusRing}),
@@ -110,9 +110,9 @@ export function responsiveAvatarSizeStyle(
110
110
  const avatarSize = avatar.sizes[size] || avatar.sizes[0]
111
111
 
112
112
  return {
113
- width: rem(avatarSize.size),
114
- height: rem(avatarSize.size),
115
- borderRadius: rem(avatarSize.size / 2),
113
+ 'width': rem(avatarSize.size),
114
+ 'height': rem(avatarSize.size),
115
+ 'borderRadius': rem(avatarSize.size / 2),
116
116
 
117
117
  '&>svg': {
118
118
  width: rem(avatarSize.size),
@@ -131,14 +131,14 @@ export function avatarImageStyle(): CSSObject {
131
131
 
132
132
  export function avatarInitialsStyle(): CSSObject {
133
133
  return {
134
- width: '100%',
135
- height: '100%',
136
- color: 'var(--avatar-fg-color)',
137
- alignItems: 'center',
138
- justifyContent: 'center',
139
- textTransform: 'uppercase',
140
- textAlign: 'center',
141
- borderRadius: '50%',
134
+ 'width': '100%',
135
+ 'height': '100%',
136
+ 'color': 'var(--avatar-fg-color)',
137
+ 'alignItems': 'center',
138
+ 'justifyContent': 'center',
139
+ 'textTransform': 'uppercase',
140
+ 'textAlign': 'center',
141
+ 'borderRadius': '50%',
142
142
 
143
143
  '&:not([hidden])': {
144
144
  display: 'flex',
@@ -155,8 +155,8 @@ function avatarBgStrokeStyle(): CSSObject {
155
155
 
156
156
  function avatarStrokeStyle(): CSSObject {
157
157
  return {
158
- strokeWidth: '2px',
159
- stroke: 'var(--avatar-bg-color)',
158
+ 'strokeWidth': '2px',
159
+ 'stroke': 'var(--avatar-bg-color)',
160
160
 
161
161
  '[data-status="editing"] &': {
162
162
  strokeDasharray: '2 4',
@@ -8,8 +8,8 @@ export function badgeStyle(props: BadgeStyleProps): CSSObject {
8
8
  '--card-bg-color': `var(--card-badge-${$tone}-bg-color)`,
9
9
  '--card-fg-color': `var(--card-badge-${$tone}-fg-color)`,
10
10
 
11
- backgroundColor: 'var(--card-bg-color)',
12
- cursor: 'default',
11
+ 'backgroundColor': 'var(--card-bg-color)',
12
+ 'cursor': 'default',
13
13
 
14
14
  '&:not([hidden])': {
15
15
  display: 'inline-block',
@@ -81,12 +81,12 @@ export function buttonColorStyles(
81
81
  return [
82
82
  _cardColorStyle(baseColor, color.enabled),
83
83
  {
84
- backgroundColor: 'var(--card-bg-color)',
85
- color: 'var(--card-fg-color)',
86
- boxShadow: focusRingBorderStyle(border),
84
+ 'backgroundColor': 'var(--card-bg-color)',
85
+ 'color': 'var(--card-fg-color)',
86
+ 'boxShadow': focusRingBorderStyle(border),
87
87
  '&:disabled, &[data-disabled="true"]': _cardColorStyle(baseColor, color.disabled),
88
88
  "&:not([data-disabled='true'])": {
89
- boxShadow: combineBoxShadow(
89
+ 'boxShadow': combineBoxShadow(
90
90
  focusRingBorderStyle(border),
91
91
  shadow ? defaultBoxShadow : undefined,
92
92
  ),
@@ -4,7 +4,7 @@ import {ResponsiveInlineSpaceStyleProps} from './types'
4
4
 
5
5
  export function inlineBaseStyle(): CSSObject {
6
6
  return {
7
- lineHeight: 0,
7
+ 'lineHeight': 0,
8
8
 
9
9
  '&&:not([hidden])': {
10
10
  display: 'block',
@@ -24,7 +24,7 @@ export function inlineSpaceStyle(props: ResponsiveInlineSpaceStyleProps & ThemeP
24
24
  const _space = rem(spaceIndex === 0.5 ? space[1] / 2 : space[spaceIndex])
25
25
 
26
26
  return {
27
- margin: `-${_space} 0 0 -${_space}`,
27
+ 'margin': `-${_space} 0 0 -${_space}`,
28
28
  '& > div': {padding: `${_space} 0 0 ${_space}`},
29
29
  }
30
30
  })
@@ -1,7 +1,7 @@
1
1
  import {EllipsisVerticalIcon} from '@sanity/icons'
2
2
  import {Button, Card, Flex, Popover, Text, useClickOutside} from '@sanity/ui'
3
3
  import {useBoolean, useSelect} from '@sanity/ui-workshop'
4
- import {useCallback, useState} from 'react'
4
+ import {useCallback, useRef, useState} from 'react'
5
5
  import {
6
6
  WORKSHOP_FLEX_ALIGN_OPTIONS,
7
7
  WORKSHOP_FLEX_JUSTIFY_OPTIONS,
@@ -20,8 +20,8 @@ export default function AlignedStory() {
20
20
 
21
21
  const [open, setOpen] = useState(false)
22
22
  const [boundaryElement, setBoundaryElement] = useState<HTMLDivElement | null>(null)
23
- const [buttonElement, setButtonElement] = useState<HTMLButtonElement | null>(null)
24
- const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
23
+ const buttonElementRef = useRef<HTMLButtonElement | null>(null)
24
+ const popoverElementRef = useRef<HTMLDivElement | null>(null)
25
25
 
26
26
  const content = (
27
27
  <Text>
@@ -39,9 +39,11 @@ export default function AlignedStory() {
39
39
  )
40
40
 
41
41
  const handleToggleOpen = useCallback(() => setOpen((v) => !v), [])
42
- const handleClose = useCallback(() => setOpen(false), [])
43
42
 
44
- useClickOutside(handleClose, [buttonElement, popoverElement])
43
+ useClickOutside(
44
+ () => setOpen(false),
45
+ () => [buttonElementRef.current, popoverElementRef.current],
46
+ )
45
47
 
46
48
  return (
47
49
  <Card height="fill" padding={[4, 5, 6]} sizing="border" tone="transparent">
@@ -56,14 +58,14 @@ export default function AlignedStory() {
56
58
  padding={3}
57
59
  portal={portal}
58
60
  placement={placement}
59
- ref={setPopoverElement}
61
+ ref={popoverElementRef}
60
62
  width={width}
61
63
  >
62
64
  <Button
63
65
  icon={EllipsisVerticalIcon}
64
66
  mode="bleed"
65
67
  onClick={handleToggleOpen}
66
- ref={setButtonElement}
68
+ ref={buttonElementRef}
67
69
  selected={open}
68
70
  />
69
71
  </Popover>
@@ -7,16 +7,16 @@ export const DEFAULT_POPOVER_ARROW_HEIGHT = 8
7
7
  export const DEFAULT_POPOVER_ARROW_RADIUS = 2
8
8
  export const DEFAULT_POPOVER_MARGINS: PopoverMargins = [0, 0, 0, 0]
9
9
  export const DEFAULT_FALLBACK_PLACEMENTS: Record<Placement, Placement[]> = {
10
- top: ['bottom', 'left', 'right'],
10
+ 'top': ['bottom', 'left', 'right'],
11
11
  'top-start': ['bottom-start', 'left-start', 'right-start'],
12
12
  'top-end': ['bottom-end', 'left-end', 'right-end'],
13
- bottom: ['top', 'left', 'right'],
13
+ 'bottom': ['top', 'left', 'right'],
14
14
  'bottom-start': ['top-start', 'left-start', 'right-start'],
15
15
  'bottom-end': ['top-end', 'left-end', 'right-end'],
16
- left: ['right', 'top', 'bottom'],
16
+ 'left': ['right', 'top', 'bottom'],
17
17
  'left-start': ['right-start', 'top-start', 'bottom-start'],
18
18
  'left-end': ['right-end', 'top-end', 'bottom-end'],
19
- right: ['left', 'top', 'bottom'],
19
+ 'right': ['left', 'top', 'bottom'],
20
20
  'right-start': ['left-start', 'top-start', 'bottom-start'],
21
21
  'right-end': ['left-end', 'top-end', 'bottom-end'],
22
22
  }
@@ -12,8 +12,8 @@ const BASE_STYLE: CSSObject = {
12
12
  '&[data-as="ul"],&[data-as="ol"]': {
13
13
  listStyle: 'none',
14
14
  },
15
- gridTemplateColumns: 'minmax(0, 1fr)',
16
- gridAutoRows: 'min-content',
15
+ 'gridTemplateColumns': 'minmax(0, 1fr)',
16
+ 'gridAutoRows': 'min-content',
17
17
  }
18
18
 
19
19
  export function stackBaseStyle(): CSSObject {