@sanity/ui 2.6.8 → 2.7.1-canary.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.
- package/dist/index.d.mts +29 -2
- package/dist/index.d.ts +29 -2
- package/dist/index.esm.js +87 -73
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +86 -72
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +87 -73
- package/dist/index.mjs.map +1 -1
- package/package.json +50 -58
- package/src/core/components/dialog/dialogContext.ts +4 -5
- package/src/core/components/menu/menuContext.ts +4 -3
- package/src/core/components/toast/toastContext.ts +4 -3
- package/src/core/components/tree/treeContext.ts +4 -3
- package/src/core/hooks/index.ts +3 -2
- package/src/core/hooks/useMatchMedia.ts +52 -0
- package/src/core/hooks/useMediaIndex/useMediaIndex.ts +2 -10
- package/src/core/hooks/usePrefersDark.ts +10 -55
- package/src/core/hooks/usePrefersReducedMotion.ts +10 -56
- package/src/core/lib/createGlobalScopedContext.ts +15 -6
- package/src/core/primitives/tooltip/__workshop__/customPortal.tsx +8 -6
- package/src/core/primitives/tooltip/tooltipDelayGroup/tooltipDelayGroupContext.tsx +4 -3
- package/src/core/theme/themeContext.ts +4 -3
- package/src/core/utils/boundaryElement/boundaryElementContext.ts +1 -3
- package/src/core/utils/layer/layerContext.ts +4 -3
- package/src/core/utils/portal/__workshop__/named.tsx +10 -8
- package/src/core/utils/portal/portalContext.ts +2 -2
- package/src/core/utils/portal/portalProvider.tsx +32 -2
- package/src/core/hooks/_internal/index.ts +0 -1
- package/src/core/hooks/_internal/useUnique.ts +0 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/ui",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.1-canary.0",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"sanity",
|
|
6
6
|
"ui",
|
|
@@ -51,32 +51,6 @@
|
|
|
51
51
|
"src",
|
|
52
52
|
"theme.js"
|
|
53
53
|
],
|
|
54
|
-
"scripts": {
|
|
55
|
-
"build": "run-s clean pkg:build pkg:check figma:pkg:build",
|
|
56
|
-
"clean": "rimraf .workshop dist",
|
|
57
|
-
"commit": "cz",
|
|
58
|
-
"cypress:dev": "run-p dev cypress:open",
|
|
59
|
-
"cypress:open": "cypress open",
|
|
60
|
-
"cypress:run": "cypress run",
|
|
61
|
-
"dev": "run-p storybook:dev workshop:dev",
|
|
62
|
-
"figma:pkg:build": "cd figma && pnpm build",
|
|
63
|
-
"format": "prettier --write --cache --ignore-unknown .",
|
|
64
|
-
"lint": "eslint . --ext .cjs,.js,.jsx,.mjs,.ts,.tsx",
|
|
65
|
-
"pkg:build": "pkg build --strict",
|
|
66
|
-
"pkg:check": "pkg --strict",
|
|
67
|
-
"prepare": "husky install",
|
|
68
|
-
"prepublishOnly": "pnpm build",
|
|
69
|
-
"release": "semantic-release",
|
|
70
|
-
"storybook:build": "storybook build",
|
|
71
|
-
"storybook:dev": "storybook dev -p 6006",
|
|
72
|
-
"test": "jest",
|
|
73
|
-
"test:browser": "start-server-and-test 'run-s workshop:build workshop:start' http://localhost:1337 'run-s cypress:run'",
|
|
74
|
-
"ts:check": "tsc",
|
|
75
|
-
"watch": "pkg watch --strict",
|
|
76
|
-
"workshop:build": "workshop build",
|
|
77
|
-
"workshop:dev": "workshop dev",
|
|
78
|
-
"workshop:start": "http-server -a localhost -c-0 -p 1337 -s -P http://localhost:1337/index.html? dist"
|
|
79
|
-
},
|
|
80
54
|
"commitlint": {
|
|
81
55
|
"extends": [
|
|
82
56
|
"@commitlint/config-conventional"
|
|
@@ -114,32 +88,32 @@
|
|
|
114
88
|
"use-effect-event": "^1.0.2"
|
|
115
89
|
},
|
|
116
90
|
"devDependencies": {
|
|
117
|
-
"@babel/core": "^7.24.
|
|
118
|
-
"@babel/preset-env": "^7.24.
|
|
91
|
+
"@babel/core": "^7.24.9",
|
|
92
|
+
"@babel/preset-env": "^7.24.8",
|
|
119
93
|
"@babel/preset-react": "^7.24.7",
|
|
120
94
|
"@babel/preset-typescript": "^7.24.7",
|
|
121
95
|
"@commitlint/cli": "^19.3.0",
|
|
122
96
|
"@commitlint/config-conventional": "^19.2.2",
|
|
123
97
|
"@juggle/resize-observer": "^3.4.0",
|
|
124
|
-
"@sanity/pkg-utils": "^6.10.
|
|
98
|
+
"@sanity/pkg-utils": "^6.10.4",
|
|
125
99
|
"@sanity/prettier-config": "^1.0.2",
|
|
126
100
|
"@sanity/semantic-release-preset": "^5.0.0",
|
|
127
101
|
"@sanity/ui-workshop": "^2.0.15",
|
|
128
|
-
"@storybook/addon-a11y": "^8.2.
|
|
129
|
-
"@storybook/addon-docs": "^8.2.
|
|
130
|
-
"@storybook/addon-essentials": "^8.2.
|
|
131
|
-
"@storybook/addon-interactions": "^8.2.
|
|
132
|
-
"@storybook/addon-links": "^8.2.
|
|
133
|
-
"@storybook/addon-mdx-gfm": "^8.2.
|
|
134
|
-
"@storybook/addon-storysource": "^8.2.
|
|
135
|
-
"@storybook/addon-themes": "^8.2.
|
|
136
|
-
"@storybook/blocks": "^8.2.
|
|
137
|
-
"@storybook/manager-api": "^8.2.
|
|
138
|
-
"@storybook/react": "^8.2.
|
|
139
|
-
"@storybook/react-vite": "^8.2.
|
|
140
|
-
"@storybook/test": "^8.2.
|
|
141
|
-
"@storybook/theming": "^8.2.
|
|
142
|
-
"@testing-library/dom": "^10.3.
|
|
102
|
+
"@storybook/addon-a11y": "^8.2.4",
|
|
103
|
+
"@storybook/addon-docs": "^8.2.4",
|
|
104
|
+
"@storybook/addon-essentials": "^8.2.4",
|
|
105
|
+
"@storybook/addon-interactions": "^8.2.4",
|
|
106
|
+
"@storybook/addon-links": "^8.2.4",
|
|
107
|
+
"@storybook/addon-mdx-gfm": "^8.2.4",
|
|
108
|
+
"@storybook/addon-storysource": "^8.2.4",
|
|
109
|
+
"@storybook/addon-themes": "^8.2.4",
|
|
110
|
+
"@storybook/blocks": "^8.2.4",
|
|
111
|
+
"@storybook/manager-api": "^8.2.4",
|
|
112
|
+
"@storybook/react": "^8.2.4",
|
|
113
|
+
"@storybook/react-vite": "^8.2.4",
|
|
114
|
+
"@storybook/test": "^8.2.4",
|
|
115
|
+
"@storybook/theming": "^8.2.4",
|
|
116
|
+
"@testing-library/dom": "^10.3.2",
|
|
143
117
|
"@testing-library/jest-dom": "^6.4.6",
|
|
144
118
|
"@testing-library/react": "^16.0.0",
|
|
145
119
|
"@types/jest": "^29.5.12",
|
|
@@ -149,8 +123,8 @@
|
|
|
149
123
|
"@types/react-dom": "^18.3.0",
|
|
150
124
|
"@types/react-is": "^18.3.0",
|
|
151
125
|
"@types/refractor": "^3.4.1",
|
|
152
|
-
"@typescript-eslint/eslint-plugin": "^7.16.
|
|
153
|
-
"@typescript-eslint/parser": "^7.16.
|
|
126
|
+
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
|
127
|
+
"@typescript-eslint/parser": "^7.16.1",
|
|
154
128
|
"commitizen": "^4.3.0",
|
|
155
129
|
"cypress": "^13.13.0",
|
|
156
130
|
"cypress-real-events": "^1.13.0",
|
|
@@ -161,7 +135,7 @@
|
|
|
161
135
|
"eslint-plugin-import": "^2.29.1",
|
|
162
136
|
"eslint-plugin-jsx-a11y": "^6.9.0",
|
|
163
137
|
"eslint-plugin-prettier": "^5.1.3",
|
|
164
|
-
"eslint-plugin-react": "^7.34.
|
|
138
|
+
"eslint-plugin-react": "^7.34.4",
|
|
165
139
|
"eslint-plugin-react-compiler": "0.0.0-experimental-a97cca1-20240529",
|
|
166
140
|
"eslint-plugin-react-hooks": "^4.6.2",
|
|
167
141
|
"eslint-plugin-storybook": "^0.8.0",
|
|
@@ -173,7 +147,7 @@
|
|
|
173
147
|
"lint-staged": "^14.0.1",
|
|
174
148
|
"module-alias": "^2.2.3",
|
|
175
149
|
"npm-run-all2": "^5.0.2",
|
|
176
|
-
"prettier": "^3.3.
|
|
150
|
+
"prettier": "^3.3.3",
|
|
177
151
|
"react": "^18.3.1",
|
|
178
152
|
"react-dom": "^18.3.1",
|
|
179
153
|
"react-is": "^18.3.1",
|
|
@@ -181,11 +155,11 @@
|
|
|
181
155
|
"rimraf": "^5.0.5",
|
|
182
156
|
"semantic-release": "^24.0.0",
|
|
183
157
|
"start-server-and-test": "^2.0.4",
|
|
184
|
-
"storybook": "^8.2.
|
|
158
|
+
"storybook": "^8.2.4",
|
|
185
159
|
"styled-components": "^6.1.11",
|
|
186
160
|
"tsconfig-paths": "^4.2.0",
|
|
187
161
|
"typescript": "5.5.3",
|
|
188
|
-
"vite": "^5.3.
|
|
162
|
+
"vite": "^5.3.4",
|
|
189
163
|
"vite-tsconfig-paths": "^4.3.2"
|
|
190
164
|
},
|
|
191
165
|
"peerDependencies": {
|
|
@@ -194,19 +168,37 @@
|
|
|
194
168
|
"react-is": "^18",
|
|
195
169
|
"styled-components": "^5.2 || ^6"
|
|
196
170
|
},
|
|
197
|
-
"packageManager": "pnpm@9.5.0",
|
|
198
171
|
"engines": {
|
|
199
172
|
"node": ">=14.0.0"
|
|
200
173
|
},
|
|
201
174
|
"publishConfig": {
|
|
202
175
|
"access": "public"
|
|
203
176
|
},
|
|
204
|
-
"pnpm": {
|
|
205
|
-
"overrides": {
|
|
206
|
-
"conventional-changelog-conventionalcommits": ">= 8.0.0"
|
|
207
|
-
}
|
|
208
|
-
},
|
|
209
177
|
"esm.sh": {
|
|
210
178
|
"bundle": false
|
|
179
|
+
},
|
|
180
|
+
"scripts": {
|
|
181
|
+
"build": "run-s clean pkg:build pkg:check figma:pkg:build",
|
|
182
|
+
"clean": "rimraf .workshop dist",
|
|
183
|
+
"commit": "cz",
|
|
184
|
+
"cypress:dev": "run-p dev cypress:open",
|
|
185
|
+
"cypress:open": "cypress open",
|
|
186
|
+
"cypress:run": "cypress run",
|
|
187
|
+
"dev": "run-p storybook:dev workshop:dev",
|
|
188
|
+
"figma:pkg:build": "cd figma && pnpm build",
|
|
189
|
+
"format": "prettier --write --cache --ignore-unknown .",
|
|
190
|
+
"lint": "eslint . --ext .cjs,.js,.jsx,.mjs,.ts,.tsx",
|
|
191
|
+
"pkg:build": "pkg build --strict",
|
|
192
|
+
"pkg:check": "pkg --strict",
|
|
193
|
+
"release": "semantic-release",
|
|
194
|
+
"storybook:build": "storybook build",
|
|
195
|
+
"storybook:dev": "storybook dev -p 6006",
|
|
196
|
+
"test": "jest",
|
|
197
|
+
"test:browser": "start-server-and-test 'run-s workshop:build workshop:start' http://localhost:1337 'run-s cypress:run'",
|
|
198
|
+
"ts:check": "tsc",
|
|
199
|
+
"watch": "pkg watch --strict",
|
|
200
|
+
"workshop:build": "workshop build",
|
|
201
|
+
"workshop:dev": "workshop dev",
|
|
202
|
+
"workshop:start": "http-server -a localhost -c-0 -p 1337 -s -P http://localhost:1337/index.html? dist"
|
|
211
203
|
}
|
|
212
|
-
}
|
|
204
|
+
}
|
|
@@ -11,11 +11,10 @@ export interface DialogContextValue {
|
|
|
11
11
|
zOffset?: number | number[]
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const key = Symbol.for('@sanity/ui/context/dialog')
|
|
15
|
-
|
|
16
14
|
/**
|
|
17
15
|
* @internal
|
|
18
16
|
*/
|
|
19
|
-
export const DialogContext = createGlobalScopedContext<DialogContextValue>(
|
|
20
|
-
|
|
21
|
-
}
|
|
17
|
+
export const DialogContext = createGlobalScopedContext<DialogContextValue>(
|
|
18
|
+
'@sanity/ui/context/dialog',
|
|
19
|
+
{version: 0.0},
|
|
20
|
+
)
|
|
@@ -23,6 +23,7 @@ export interface MenuContextValue {
|
|
|
23
23
|
onMouseLeave: (event: React.MouseEvent<HTMLElement>) => void
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
export const MenuContext = createGlobalScopedContext<MenuContextValue | null>(
|
|
27
|
+
'@sanity/ui/context/menu',
|
|
28
|
+
null,
|
|
29
|
+
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {createGlobalScopedContext} from '../../lib/createGlobalScopedContext'
|
|
2
2
|
import {ToastContextValue} from './types'
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
export const ToastContext = createGlobalScopedContext<ToastContextValue | null>(
|
|
5
|
+
'@sanity/ui/context/toast',
|
|
6
|
+
null,
|
|
7
|
+
)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {createGlobalScopedContext} from '../../lib/createGlobalScopedContext'
|
|
2
2
|
import {TreeContextValue} from './types'
|
|
3
3
|
|
|
4
|
-
const key = Symbol.for('@sanity/ui/context/tree')
|
|
5
|
-
|
|
6
4
|
/**
|
|
7
5
|
* @internal
|
|
8
6
|
*/
|
|
9
|
-
export const TreeContext = createGlobalScopedContext<TreeContextValue | null>(
|
|
7
|
+
export const TreeContext = createGlobalScopedContext<TreeContextValue | null>(
|
|
8
|
+
'@sanity/ui/context/tree',
|
|
9
|
+
null,
|
|
10
|
+
)
|
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'
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
function getMatchMedia(): MediaQueryList {
|
|
22
|
+
if (MEDIA_QUERY_CACHE === undefined) {
|
|
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: () => {
|
|
40
|
+
try {
|
|
41
|
+
return getMatchMedia().matches
|
|
42
|
+
} catch (err) {
|
|
43
|
+
throw new Error(`Failed to match media query: ${mediaQueryString}`, {cause: err})
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
}, [mediaQueryString])
|
|
48
|
+
|
|
49
|
+
useDebugValue(mediaQueryString)
|
|
50
|
+
|
|
51
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
|
52
|
+
}
|
|
@@ -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,8 +9,6 @@ export interface _MediaStore {
|
|
|
9
9
|
getSnapshot: () => number
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const MEDIA_STORE_CACHE = new WeakMap<number[], _MediaStore>()
|
|
13
|
-
|
|
14
12
|
type MediaQueryMinWidth = `(min-width: ${number}px)`
|
|
15
13
|
type MediaQueryMaxWidth = `(max-width: ${number}px)`
|
|
16
14
|
type MediaQueryMinMaxWidth = `${MediaQueryMinWidth} and ${MediaQueryMaxWidth}`
|
|
@@ -97,13 +95,7 @@ function getServerSnapshot() {
|
|
|
97
95
|
*/
|
|
98
96
|
export function useMediaIndex(): number {
|
|
99
97
|
const {media} = useTheme_v2()
|
|
100
|
-
|
|
101
|
-
let store = MEDIA_STORE_CACHE.get(media)
|
|
102
|
-
|
|
103
|
-
if (!store) {
|
|
104
|
-
store = _createMediaStore(media)
|
|
105
|
-
MEDIA_STORE_CACHE.set(media, store)
|
|
106
|
-
}
|
|
98
|
+
const store = useMemo(() => _createMediaStore(media), [media])
|
|
107
99
|
|
|
108
100
|
return useSyncExternalStore(store.subscribe, store.getSnapshot, getServerSnapshot)
|
|
109
101
|
}
|
|
@@ -1,61 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
let MEDIA_QUERY_CACHE: MediaQueryList | undefined
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Lazy init the matchMedia instance
|
|
7
|
-
*/
|
|
8
|
-
function getMatchMedia(): MediaQueryList {
|
|
9
|
-
if (!MEDIA_QUERY_CACHE) {
|
|
10
|
-
// As this function is only called during `subscribe` and `getSnapshot`, we can assume that the
|
|
11
|
-
// the `window` global is available and we're in a browser environment
|
|
12
|
-
MEDIA_QUERY_CACHE = window.matchMedia('(prefers-color-scheme: dark)')
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return MEDIA_QUERY_CACHE
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* As the query is the same for all instances of this hook, we can cache the matchMedia instance
|
|
20
|
-
* and have cheap `change` event listeners, while getSnapshot always reads from the same
|
|
21
|
-
* matchMedia instance and we don't get any tearing.
|
|
22
|
-
* Tearing in this context means the bad edge case in React concurrent render mdoe
|
|
23
|
-
* where you sometimes would end up with some components doing render while seeing `usePrefersDark() === true` while others would see `usePrefersDark() === false`
|
|
24
|
-
* during the same render.
|
|
25
|
-
* By using `useSyncExternalStore` every component only sees the same value during the same render, and always re-render when it changes no matter
|
|
26
|
-
* what React.memo boundaries there might be between the layers..
|
|
27
|
-
*/
|
|
28
|
-
function subscribe(onStoreChange: () => void): () => void {
|
|
29
|
-
const matchMedia = getMatchMedia()
|
|
30
|
-
|
|
31
|
-
matchMedia.addEventListener('change', onStoreChange)
|
|
32
|
-
|
|
33
|
-
return () => matchMedia.removeEventListener('change', onStoreChange)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Only called client-side, when using createRoot, or after hydration is complete when using hydrateRoot.
|
|
38
|
-
* It's important that this function does not create new objects or arrays when called:
|
|
39
|
-
* https://beta.reactjs.org/apis/react/useSyncExternalStore#im-getting-an-error-the-result-of-getsnapshot-should-be-cached
|
|
40
|
-
*/
|
|
41
|
-
function getSnapshot() {
|
|
42
|
-
return getMatchMedia().matches
|
|
43
|
-
}
|
|
1
|
+
import {useMatchMedia} from './useMatchMedia'
|
|
44
2
|
|
|
45
3
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* 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
|
|
49
11
|
*
|
|
50
|
-
* @link https://beta.reactjs.org/apis/react/useSyncExternalStore#adding-support-for-server-rendering
|
|
51
|
-
*/
|
|
52
|
-
function getServerSnapshot() {
|
|
53
|
-
return false
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
12
|
* @public
|
|
58
13
|
*/
|
|
59
|
-
export function usePrefersDark(): boolean {
|
|
60
|
-
return
|
|
14
|
+
export function usePrefersDark(getServerSnapshot = () => false): boolean {
|
|
15
|
+
return useMatchMedia('(prefers-color-scheme: dark)', getServerSnapshot)
|
|
61
16
|
}
|
|
@@ -1,62 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
let MEDIA_QUERY_CACHE: MediaQueryList | undefined
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Lazy init the matchMedia instance
|
|
7
|
-
*/
|
|
8
|
-
function getMatchMedia(): MediaQueryList {
|
|
9
|
-
if (!MEDIA_QUERY_CACHE) {
|
|
10
|
-
// As this function is only called during `subscribe` and `getSnapshot`, we can assume that the
|
|
11
|
-
// the `window` global is available and we're in a browser environment
|
|
12
|
-
MEDIA_QUERY_CACHE = window.matchMedia('(prefers-reduced-motion: reduce)')
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return MEDIA_QUERY_CACHE
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* As the query is the same for all instances of this hook, we can cache the matchMedia instance
|
|
20
|
-
* and have cheap `change` event listeners, while getSnapshot always reads from the same
|
|
21
|
-
* matchMedia instance and we don't get any tearing.
|
|
22
|
-
* Tearing in this context means the bad edge case in React concurrent render mdoe
|
|
23
|
-
* where you sometimes would end up with some components doing render while seeing `usePrefersDark() === true` while others would see `usePrefersDark() === false`
|
|
24
|
-
* during the same render.
|
|
25
|
-
* By using `useSyncExternalStore` every component only sees the same value during the same render, and always re-render when it changes no matter
|
|
26
|
-
* what React.memo boundaries there might be between the layers..
|
|
27
|
-
*/
|
|
28
|
-
function subscribe(onStoreChange: () => void): () => void {
|
|
29
|
-
const matchMedia = getMatchMedia()
|
|
30
|
-
|
|
31
|
-
matchMedia.addEventListener('change', onStoreChange)
|
|
32
|
-
|
|
33
|
-
return () => matchMedia.removeEventListener('change', onStoreChange)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Only called client-side, when using createRoot, or after hydration is complete when using hydrateRoot.
|
|
38
|
-
* It's important that this function does not create new objects or arrays when called:
|
|
39
|
-
* https://beta.reactjs.org/apis/react/useSyncExternalStore#im-getting-an-error-the-result-of-getsnapshot-should-be-cached
|
|
40
|
-
*/
|
|
41
|
-
function getSnapshot() {
|
|
42
|
-
return getMatchMedia().matches
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Only called during server-side rendering, and hydration if using hydrateRoot
|
|
47
|
-
* Since the server environment doesn't have access to the DOM, we can't determine the current value of the media query
|
|
48
|
-
* and we assume `(prefers-reduced-motion: no-preference)` since it's the most common scheme
|
|
49
|
-
*
|
|
50
|
-
* @link https://beta.reactjs.org/apis/react/useSyncExternalStore#adding-support-for-server-rendering
|
|
51
|
-
*/
|
|
52
|
-
function getServerSnapshot() {
|
|
53
|
-
return false
|
|
54
|
-
}
|
|
1
|
+
import {useMatchMedia} from './useMatchMedia'
|
|
55
2
|
|
|
56
3
|
/**
|
|
57
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
|
+
*
|
|
58
12
|
* @public
|
|
59
13
|
*/
|
|
60
|
-
export function usePrefersReducedMotion(): boolean {
|
|
61
|
-
return
|
|
14
|
+
export function usePrefersReducedMotion(getServerSnapshot = () => false): boolean {
|
|
15
|
+
return useMatchMedia('(prefers-reduced-motion: reduce)', getServerSnapshot)
|
|
62
16
|
}
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* As `@sanity/ui` is declared as a dependency, and may be duplicated, sometimes across major versions
|
|
3
|
+
* it's critical that vital react contexts are shared even when there is a duplicate.
|
|
4
|
+
* If we used a model similar to `sanity` itself, or `styled-components`, this would be unnecessary as
|
|
5
|
+
* those libraries enforce single instances.
|
|
6
|
+
* Since we don't enforce it we have to support a sanity plugin being able to call hooks like `useToast`, and then
|
|
7
|
+
* read the context setup by `sanity`, which calls `ToastProvider`, even if the provider and hook are different instances in memory.
|
|
8
|
+
* It's also why it's vital that all changes to globally scoped providers remain fully backwards compatible to v1.
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
11
|
import {createContext, type Context} from 'react'
|
|
8
12
|
import {globalScope} from './globalScope'
|
|
9
13
|
|
|
10
14
|
export function createGlobalScopedContext<ContextType, const T extends ContextType = ContextType>(
|
|
11
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Enforce that all Symbol.for keys used for globally scoped contexts have a predictable prefix
|
|
17
|
+
*/
|
|
18
|
+
key: `@sanity/ui/context/${string}`,
|
|
12
19
|
defaultValue: T,
|
|
13
20
|
): Context<ContextType> {
|
|
21
|
+
const symbol = Symbol.for(key)
|
|
22
|
+
|
|
14
23
|
/**
|
|
15
24
|
* Prevent errors about re-renders on React SSR on Next.js App Router
|
|
16
25
|
*/
|
|
@@ -18,7 +27,7 @@ export function createGlobalScopedContext<ContextType, const T extends ContextTy
|
|
|
18
27
|
return createContext<ContextType>(defaultValue)
|
|
19
28
|
}
|
|
20
29
|
|
|
21
|
-
globalScope[
|
|
30
|
+
globalScope[symbol] = globalScope[symbol] || createContext<T>(defaultValue)
|
|
22
31
|
|
|
23
|
-
return globalScope[
|
|
32
|
+
return globalScope[symbol]
|
|
24
33
|
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
Tooltip,
|
|
11
11
|
} from '@sanity/ui'
|
|
12
12
|
import {useBoolean, useSelect, useText} from '@sanity/ui-workshop'
|
|
13
|
-
import {useState} from 'react'
|
|
13
|
+
import {useMemo, useState} from 'react'
|
|
14
14
|
import {WORKSHOP_PLACEMENT_OPTIONS} from '../../../__workshop__/constants'
|
|
15
15
|
|
|
16
16
|
const PORTAL_OPTIONS = {
|
|
@@ -30,13 +30,15 @@ export default function CustomPortalStory() {
|
|
|
30
30
|
|
|
31
31
|
const [portal1Element, setPortal1Element] = useState<HTMLDivElement | null>(null)
|
|
32
32
|
const [boundaryElement, setBoundaryElement] = useState<HTMLDivElement | null>(null)
|
|
33
|
+
const __unstable_elements = useMemo(
|
|
34
|
+
() => ({
|
|
35
|
+
portal1: portal1Element,
|
|
36
|
+
}),
|
|
37
|
+
[portal1Element],
|
|
38
|
+
)
|
|
33
39
|
|
|
34
40
|
return (
|
|
35
|
-
<PortalProvider
|
|
36
|
-
__unstable_elements={{
|
|
37
|
-
portal1: portal1Element,
|
|
38
|
-
}}
|
|
39
|
-
>
|
|
41
|
+
<PortalProvider __unstable_elements={__unstable_elements}>
|
|
40
42
|
<Flex align="center" height="fill" justify="center">
|
|
41
43
|
<BoundaryElementProvider element={useBoundaryElement ? boundaryElement : null}>
|
|
42
44
|
<Card
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {createGlobalScopedContext} from '../../../lib/createGlobalScopedContext'
|
|
2
2
|
import {TooltipDelayGroupContextValue} from './types'
|
|
3
3
|
|
|
4
|
-
const key = Symbol.for('@sanity/ui/context/tooltipDelayGroup')
|
|
5
|
-
|
|
6
4
|
/**
|
|
7
5
|
* @beta
|
|
8
6
|
*/
|
|
9
7
|
export const TooltipDelayGroupContext =
|
|
10
|
-
createGlobalScopedContext<TooltipDelayGroupContextValue | null>(
|
|
8
|
+
createGlobalScopedContext<TooltipDelayGroupContextValue | null>(
|
|
9
|
+
'@sanity/ui/context/tooltipDelayGroup',
|
|
10
|
+
null,
|
|
11
|
+
)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {createGlobalScopedContext} from '../lib/createGlobalScopedContext'
|
|
2
2
|
import {ThemeContextValue} from './types'
|
|
3
3
|
|
|
4
|
-
const key = Symbol.for('@sanity/ui/context/theme')
|
|
5
|
-
|
|
6
4
|
/**
|
|
7
5
|
* @internal
|
|
8
6
|
*/
|
|
9
|
-
export const ThemeContext = createGlobalScopedContext<ThemeContextValue | null>(
|
|
7
|
+
export const ThemeContext = createGlobalScopedContext<ThemeContextValue | null>(
|
|
8
|
+
'@sanity/ui/context/theme',
|
|
9
|
+
null,
|
|
10
|
+
)
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import {createGlobalScopedContext} from '../../lib/createGlobalScopedContext'
|
|
2
2
|
import {BoundaryElementContextValue} from './types'
|
|
3
3
|
|
|
4
|
-
const key = Symbol.for('@sanity/ui/context/boundaryElement')
|
|
5
|
-
|
|
6
4
|
export const BoundaryElementContext = createGlobalScopedContext<BoundaryElementContextValue | null>(
|
|
7
|
-
|
|
5
|
+
'@sanity/ui/context/boundaryElement',
|
|
8
6
|
null,
|
|
9
7
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {createGlobalScopedContext} from '../../lib/createGlobalScopedContext'
|
|
2
2
|
import {LayerContextValue} from './types'
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
export const LayerContext = createGlobalScopedContext<LayerContextValue | null>(
|
|
5
|
+
'@sanity/ui/context/layer',
|
|
6
|
+
null,
|
|
7
|
+
)
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import {Card, Container, Portal, PortalProvider, Stack, Text} from '@sanity/ui'
|
|
2
|
-
import {useState} from 'react'
|
|
2
|
+
import {useMemo, useState} from 'react'
|
|
3
3
|
|
|
4
4
|
export default function NamedStory() {
|
|
5
5
|
const [portal1Element, setPortal1Element] = useState<HTMLDivElement | null>(null)
|
|
6
6
|
const [portal2Element, setPortal2Element] = useState<HTMLDivElement | null>(null)
|
|
7
7
|
const [portal3Element, setPortal3Element] = useState<HTMLDivElement | null>(null)
|
|
8
|
+
const __unstable_elements = useMemo(
|
|
9
|
+
() => ({
|
|
10
|
+
portal1: portal1Element,
|
|
11
|
+
portal2: portal2Element,
|
|
12
|
+
portal3: portal3Element,
|
|
13
|
+
}),
|
|
14
|
+
[portal1Element, portal2Element, portal3Element],
|
|
15
|
+
)
|
|
8
16
|
|
|
9
17
|
return (
|
|
10
|
-
<PortalProvider
|
|
11
|
-
__unstable_elements={{
|
|
12
|
-
portal1: portal1Element,
|
|
13
|
-
portal2: portal2Element,
|
|
14
|
-
portal3: portal3Element,
|
|
15
|
-
}}
|
|
16
|
-
>
|
|
18
|
+
<PortalProvider __unstable_elements={__unstable_elements}>
|
|
17
19
|
<Container width={1}>
|
|
18
20
|
<Card height="fill" padding={4}>
|
|
19
21
|
<Stack space={2}>
|
|
@@ -2,8 +2,8 @@ import {createGlobalScopedContext} from '../../lib/createGlobalScopedContext'
|
|
|
2
2
|
import {globalScope} from '../../lib/globalScope'
|
|
3
3
|
import {PortalContextValue} from './types'
|
|
4
4
|
|
|
5
|
-
const key =
|
|
6
|
-
const elementKey = Symbol.for(
|
|
5
|
+
const key = '@sanity/ui/context/portal'
|
|
6
|
+
const elementKey = Symbol.for(`${key}/element`)
|
|
7
7
|
|
|
8
8
|
globalScope[elementKey] = null
|
|
9
9
|
|