@sanity/ui 2.6.8-canary.0 → 2.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ui",
3
- "version": "2.6.8-canary.0",
3
+ "version": "2.7.0",
4
4
  "keywords": [
5
5
  "sanity",
6
6
  "ui",
@@ -51,6 +51,32 @@
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
+ "prepack": "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
+ },
54
80
  "commitlint": {
55
81
  "extends": [
56
82
  "@commitlint/config-conventional"
@@ -88,32 +114,32 @@
88
114
  "use-effect-event": "^1.0.2"
89
115
  },
90
116
  "devDependencies": {
91
- "@babel/core": "^7.24.7",
92
- "@babel/preset-env": "^7.24.7",
117
+ "@babel/core": "^7.24.9",
118
+ "@babel/preset-env": "^7.24.8",
93
119
  "@babel/preset-react": "^7.24.7",
94
120
  "@babel/preset-typescript": "^7.24.7",
95
121
  "@commitlint/cli": "^19.3.0",
96
122
  "@commitlint/config-conventional": "^19.2.2",
97
123
  "@juggle/resize-observer": "^3.4.0",
98
- "@sanity/pkg-utils": "^6.10.3",
124
+ "@sanity/pkg-utils": "^6.10.4",
99
125
  "@sanity/prettier-config": "^1.0.2",
100
126
  "@sanity/semantic-release-preset": "^5.0.0",
101
127
  "@sanity/ui-workshop": "^2.0.15",
102
- "@storybook/addon-a11y": "^8.2.1",
103
- "@storybook/addon-docs": "^8.2.1",
104
- "@storybook/addon-essentials": "^8.2.1",
105
- "@storybook/addon-interactions": "^8.2.1",
106
- "@storybook/addon-links": "^8.2.1",
107
- "@storybook/addon-mdx-gfm": "^8.2.1",
108
- "@storybook/addon-storysource": "^8.2.1",
109
- "@storybook/addon-themes": "^8.2.1",
110
- "@storybook/blocks": "^8.2.1",
111
- "@storybook/manager-api": "^8.2.1",
112
- "@storybook/react": "^8.2.1",
113
- "@storybook/react-vite": "^8.2.1",
114
- "@storybook/test": "^8.2.1",
115
- "@storybook/theming": "^8.2.1",
116
- "@testing-library/dom": "^10.3.1",
128
+ "@storybook/addon-a11y": "^8.2.4",
129
+ "@storybook/addon-docs": "^8.2.4",
130
+ "@storybook/addon-essentials": "^8.2.4",
131
+ "@storybook/addon-interactions": "^8.2.4",
132
+ "@storybook/addon-links": "^8.2.4",
133
+ "@storybook/addon-mdx-gfm": "^8.2.4",
134
+ "@storybook/addon-storysource": "^8.2.4",
135
+ "@storybook/addon-themes": "^8.2.4",
136
+ "@storybook/blocks": "^8.2.4",
137
+ "@storybook/manager-api": "^8.2.4",
138
+ "@storybook/react": "^8.2.4",
139
+ "@storybook/react-vite": "^8.2.4",
140
+ "@storybook/test": "^8.2.4",
141
+ "@storybook/theming": "^8.2.4",
142
+ "@testing-library/dom": "^10.3.2",
117
143
  "@testing-library/jest-dom": "^6.4.6",
118
144
  "@testing-library/react": "^16.0.0",
119
145
  "@types/jest": "^29.5.12",
@@ -123,8 +149,8 @@
123
149
  "@types/react-dom": "^18.3.0",
124
150
  "@types/react-is": "^18.3.0",
125
151
  "@types/refractor": "^3.4.1",
126
- "@typescript-eslint/eslint-plugin": "^7.16.0",
127
- "@typescript-eslint/parser": "^7.16.0",
152
+ "@typescript-eslint/eslint-plugin": "^7.16.1",
153
+ "@typescript-eslint/parser": "^7.16.1",
128
154
  "commitizen": "^4.3.0",
129
155
  "cypress": "^13.13.0",
130
156
  "cypress-real-events": "^1.13.0",
@@ -135,7 +161,7 @@
135
161
  "eslint-plugin-import": "^2.29.1",
136
162
  "eslint-plugin-jsx-a11y": "^6.9.0",
137
163
  "eslint-plugin-prettier": "^5.1.3",
138
- "eslint-plugin-react": "^7.34.3",
164
+ "eslint-plugin-react": "^7.34.4",
139
165
  "eslint-plugin-react-compiler": "0.0.0-experimental-a97cca1-20240529",
140
166
  "eslint-plugin-react-hooks": "^4.6.2",
141
167
  "eslint-plugin-storybook": "^0.8.0",
@@ -147,7 +173,7 @@
147
173
  "lint-staged": "^14.0.1",
148
174
  "module-alias": "^2.2.3",
149
175
  "npm-run-all2": "^5.0.2",
150
- "prettier": "^3.3.2",
176
+ "prettier": "^3.3.3",
151
177
  "react": "^18.3.1",
152
178
  "react-dom": "^18.3.1",
153
179
  "react-is": "^18.3.1",
@@ -155,11 +181,11 @@
155
181
  "rimraf": "^5.0.5",
156
182
  "semantic-release": "^24.0.0",
157
183
  "start-server-and-test": "^2.0.4",
158
- "storybook": "^8.2.1",
184
+ "storybook": "^8.2.4",
159
185
  "styled-components": "^6.1.11",
160
186
  "tsconfig-paths": "^4.2.0",
161
187
  "typescript": "5.5.3",
162
- "vite": "^5.3.3",
188
+ "vite": "^5.3.4",
163
189
  "vite-tsconfig-paths": "^4.3.2"
164
190
  },
165
191
  "peerDependencies": {
@@ -168,37 +194,19 @@
168
194
  "react-is": "^18",
169
195
  "styled-components": "^5.2 || ^6"
170
196
  },
197
+ "packageManager": "pnpm@9.5.0",
171
198
  "engines": {
172
199
  "node": ">=14.0.0"
173
200
  },
174
201
  "publishConfig": {
175
202
  "access": "public"
176
203
  },
204
+ "pnpm": {
205
+ "overrides": {
206
+ "conventional-changelog-conventionalcommits": ">= 8.0.0"
207
+ }
208
+ },
177
209
  "esm.sh": {
178
210
  "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"
203
211
  }
204
- }
212
+ }
@@ -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>(key, {
20
- version: 0.0,
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 key = Symbol.for('@sanity/ui/context/menu')
27
-
28
- export const MenuContext = createGlobalScopedContext<MenuContextValue | null>(key, null)
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 key = Symbol.for('@sanity/ui/context/toast')
5
-
6
- export const ToastContext = createGlobalScopedContext<ToastContextValue | null>(key, null)
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>(key, null)
7
+ export const TreeContext = createGlobalScopedContext<TreeContextValue | null>(
8
+ '@sanity/ui/context/tree',
9
+ null,
10
+ )
@@ -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,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,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 {useSyncExternalStore} from 'react'
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
- * 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-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 useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
14
+ export function usePrefersDark(getServerSnapshot = () => false): boolean {
15
+ return useMatchMedia('(prefers-color-scheme: dark)', getServerSnapshot)
61
16
  }
@@ -1,62 +1,16 @@
1
- import {useSyncExternalStore} from 'react'
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 useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
14
+ export function usePrefersReducedMotion(getServerSnapshot = () => false): boolean {
15
+ return useMatchMedia('(prefers-reduced-motion: reduce)', getServerSnapshot)
62
16
  }
@@ -1,16 +1,25 @@
1
1
  /**
2
- * To improve performance and DevEx for things such as Hot Module Replacement,
3
- * we want to preserve created contexts between reloads.
4
- * It also helps with module duplication and other issues.
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
- key: symbol,
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[key] = globalScope[key] || createContext<T>(defaultValue)
30
+ globalScope[symbol] = globalScope[symbol] || createContext<T>(defaultValue)
22
31
 
23
- return globalScope[key]
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>(key, 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>(key, 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
- key,
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 key = Symbol.for('@sanity/ui/context/layer')
5
-
6
- export const LayerContext = createGlobalScopedContext<LayerContextValue | null>(key, null)
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 = Symbol.for('@sanity/ui/context/portal')
6
- const elementKey = Symbol.for('@sanity/ui/context/portal/element')
5
+ const key = '@sanity/ui/context/portal'
6
+ const elementKey = Symbol.for(`${key}/element`)
7
7
 
8
8
  globalScope[elementKey] = null
9
9