@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.
- package/dist/index.d.mts +51 -15
- package/dist/index.d.ts +51 -15
- package/dist/index.esm.js +103 -167
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +102 -167
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +103 -167
- package/dist/index.mjs.map +1 -1
- package/dist/theme.d.mts +14 -14
- package/dist/theme.d.ts +14 -14
- package/dist/theme.esm.js.map +1 -1
- package/dist/theme.js.map +1 -1
- package/dist/theme.mjs.map +1 -1
- package/package.json +28 -27
- package/src/core/__workshop__/constants.ts +36 -36
- package/src/core/components/autocomplete/autocomplete.tsx +1 -1
- package/src/core/components/breadcrumbs/breadcrumbs.tsx +15 -6
- package/src/core/components/dialog/dialog.tsx +12 -21
- package/src/core/components/dialog/styles.ts +8 -8
- package/src/core/components/menu/menu.tsx +11 -18
- package/src/core/components/menu/menuButton.tsx +11 -11
- package/src/core/components/menu/menuContext.ts +1 -1
- package/src/core/components/menu/useMenuController.ts +11 -18
- package/src/core/components/tab/tab.tsx +8 -8
- package/src/core/components/tab/tabPanel.tsx +1 -1
- package/src/core/components/toast/useToast.ts +1 -0
- package/src/core/components/tree/tree.tsx +1 -0
- package/src/core/components/tree/treeItem.tsx +1 -0
- package/src/core/hooks/index.ts +3 -2
- package/src/core/hooks/useClickOutside.ts +37 -74
- package/src/core/hooks/useMatchMedia.ts +46 -0
- package/src/core/hooks/useMediaIndex/useMediaIndex.ts +2 -11
- package/src/core/hooks/usePrefersDark.ts +10 -58
- package/src/core/hooks/usePrefersReducedMotion.ts +10 -58
- package/src/core/primitives/avatar/styles.ts +42 -42
- package/src/core/primitives/badge/styles.ts +2 -2
- package/src/core/primitives/button/styles.ts +4 -4
- package/src/core/primitives/inline/styles.ts +2 -2
- package/src/core/primitives/popover/__workshop__/AlignedStory.tsx +9 -7
- package/src/core/primitives/popover/constants.ts +4 -4
- package/src/core/primitives/stack/styles.ts +2 -2
- package/src/core/primitives/tooltip/__workshop__/customPortal.tsx +9 -7
- package/src/core/primitives/tooltip/constants.ts +4 -4
- package/src/core/primitives/tooltip/tooltip.tsx +45 -27
- package/src/core/primitives/tooltip/tooltipDelayGroup/tooltipDelayGroupProvider.tsx +1 -0
- package/src/core/styles/font/responsiveFont.ts +8 -8
- package/src/core/theme/__workshop__/debug/story.tsx +11 -11
- package/src/core/utils/layer/layerProvider.tsx +1 -0
- package/src/core/utils/portal/__workshop__/named.tsx +10 -8
- package/src/core/utils/portal/portalProvider.tsx +7 -14
- package/src/theme/build/buildColorTheme.test.ts +13 -13
- package/src/theme/config/tokens/color/types.ts +14 -14
- package/src/theme/defaults/colorTokens.ts +36 -36
- package/src/core/hooks/_internal/index.ts +0 -1
- 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],
|
package/src/core/hooks/index.ts
CHANGED
|
@@ -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
|
|
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:
|
|
22
|
+
elementsArg:
|
|
23
|
+
| Array<HTMLElement | HTMLElement[] | null>
|
|
24
|
+
| (() => Array<HTMLElement | HTMLElement[] | null>) = EMPTY_ARRAY,
|
|
32
25
|
boundaryElement?: HTMLElement | null,
|
|
33
|
-
):
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
69
|
-
|
|
42
|
+
const resolvedElements = Array.isArray(elementsArg) ? elementsArg : elementsArg()
|
|
43
|
+
const elements = resolvedElements.flat()
|
|
70
44
|
|
|
71
|
-
const
|
|
72
|
-
|
|
45
|
+
for (const el of elements) {
|
|
46
|
+
if (!el) continue
|
|
73
47
|
|
|
74
|
-
if (
|
|
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
|
-
|
|
92
|
-
|
|
53
|
+
listener(evt)
|
|
54
|
+
})
|
|
93
55
|
|
|
94
|
-
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
document.addEventListener('mousedown', eventHandler)
|
|
58
|
+
document.addEventListener('touchstart', eventHandler)
|
|
95
59
|
|
|
96
60
|
return () => {
|
|
97
|
-
|
|
61
|
+
document.removeEventListener('mousedown', eventHandler)
|
|
62
|
+
document.removeEventListener('touchstart', eventHandler)
|
|
98
63
|
}
|
|
99
|
-
}, [
|
|
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 {
|
|
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
|
-
*
|
|
50
|
-
*
|
|
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
|
|
14
|
+
export function usePrefersDark(getServerSnapshot = () => false): boolean {
|
|
15
|
+
return useMatchMedia('(prefers-color-scheme: dark)', getServerSnapshot)
|
|
64
16
|
}
|
|
@@ -1,64 +1,16 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
24
|
-
const
|
|
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(
|
|
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={
|
|
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={
|
|
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 {
|