@sanity/ui 2.6.4-canary.2 → 2.6.5
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 +6 -42
- package/dist/index.d.ts +6 -42
- package/dist/index.esm.js +164 -97
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +162 -95
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +164 -97
- package/dist/index.mjs.map +1 -1
- package/package.json +34 -26
- package/src/core/components/breadcrumbs/breadcrumbs.tsx +6 -15
- package/src/core/components/dialog/dialog.tsx +21 -12
- package/src/core/components/menu/menu.tsx +18 -11
- package/src/core/components/menu/menuButton.tsx +1 -1
- package/src/core/components/menu/menuContext.ts +1 -1
- package/src/core/components/menu/useMenuController.ts +17 -11
- package/src/core/components/toast/styles.ts +1 -2
- package/src/core/components/toast/useToast.ts +0 -1
- package/src/core/components/tree/tree.tsx +0 -1
- package/src/core/components/tree/treeItem.tsx +0 -1
- package/src/core/helpers/focus.ts +0 -1
- package/src/core/helpers/scroll.ts +0 -1
- package/src/core/hooks/_internal/index.ts +1 -0
- package/src/core/hooks/_internal/useUnique.ts +34 -0
- package/src/core/hooks/index.ts +2 -3
- package/src/core/hooks/useClickOutside.ts +72 -38
- package/src/core/hooks/useElementSize.ts +0 -1
- package/src/core/hooks/useMediaIndex/useMediaIndex.ts +10 -2
- package/src/core/hooks/usePrefersDark.ts +55 -10
- package/src/core/hooks/usePrefersReducedMotion.ts +56 -10
- package/src/core/primitives/popover/__workshop__/AlignedStory.tsx +7 -9
- package/src/core/primitives/tooltip/__workshop__/customPortal.tsx +6 -8
- package/src/core/primitives/tooltip/tooltip.tsx +29 -31
- package/src/core/primitives/tooltip/tooltipDelayGroup/tooltipDelayGroupProvider.tsx +0 -1
- package/src/core/utils/layer/layerProvider.tsx +0 -1
- package/src/core/utils/portal/__workshop__/named.tsx +8 -10
- package/src/core/utils/portal/portalProvider.tsx +3 -2
- package/src/core/hooks/useMatchMedia.ts +0 -46
|
@@ -1,16 +1,61 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {useSyncExternalStore} from 'react'
|
|
2
|
+
|
|
3
|
+
let MEDIA_QUERY_CACHE: MediaQueryList | undefined
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
}
|
|
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-color-scheme: light)` since it's the most common scheme
|
|
11
49
|
*
|
|
50
|
+
* @link https://beta.reactjs.org/apis/react/useSyncExternalStore#adding-support-for-server-rendering
|
|
51
|
+
*/
|
|
52
|
+
function getServerSnapshot() {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
12
57
|
* @public
|
|
13
58
|
*/
|
|
14
|
-
export function usePrefersDark(
|
|
15
|
-
return
|
|
59
|
+
export function usePrefersDark(): boolean {
|
|
60
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
|
16
61
|
}
|
|
@@ -1,16 +1,62 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {useSyncExternalStore} from 'react'
|
|
2
|
+
|
|
3
|
+
let MEDIA_QUERY_CACHE: MediaQueryList | undefined
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
11
49
|
*
|
|
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
|
+
* Returns true if motion should be reduced
|
|
12
58
|
* @public
|
|
13
59
|
*/
|
|
14
|
-
export function usePrefersReducedMotion(
|
|
15
|
-
return
|
|
60
|
+
export function usePrefersReducedMotion(): boolean {
|
|
61
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
|
16
62
|
}
|
|
@@ -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,
|
|
4
|
+
import {useCallback, 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 [buttonElement, setButtonElement] = useState<HTMLButtonElement | null>(null)
|
|
24
|
+
const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
|
|
25
25
|
|
|
26
26
|
const content = (
|
|
27
27
|
<Text>
|
|
@@ -39,11 +39,9 @@ export default function AlignedStory() {
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
const handleToggleOpen = useCallback(() => setOpen((v) => !v), [])
|
|
42
|
+
const handleClose = useCallback(() => setOpen(false), [])
|
|
42
43
|
|
|
43
|
-
useClickOutside(
|
|
44
|
-
() => setOpen(false),
|
|
45
|
-
() => [buttonElementRef.current, popoverElementRef.current],
|
|
46
|
-
)
|
|
44
|
+
useClickOutside(handleClose, [buttonElement, popoverElement])
|
|
47
45
|
|
|
48
46
|
return (
|
|
49
47
|
<Card height="fill" padding={[4, 5, 6]} sizing="border" tone="transparent">
|
|
@@ -58,14 +56,14 @@ export default function AlignedStory() {
|
|
|
58
56
|
padding={3}
|
|
59
57
|
portal={portal}
|
|
60
58
|
placement={placement}
|
|
61
|
-
ref={
|
|
59
|
+
ref={setPopoverElement}
|
|
62
60
|
width={width}
|
|
63
61
|
>
|
|
64
62
|
<Button
|
|
65
63
|
icon={EllipsisVerticalIcon}
|
|
66
64
|
mode="bleed"
|
|
67
65
|
onClick={handleToggleOpen}
|
|
68
|
-
ref={
|
|
66
|
+
ref={setButtonElement}
|
|
69
67
|
selected={open}
|
|
70
68
|
/>
|
|
71
69
|
</Popover>
|
|
@@ -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 {
|
|
13
|
+
import {useState} from 'react'
|
|
14
14
|
import {WORKSHOP_PLACEMENT_OPTIONS} from '../../../__workshop__/constants'
|
|
15
15
|
|
|
16
16
|
const PORTAL_OPTIONS = {
|
|
@@ -30,15 +30,13 @@ 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
|
-
)
|
|
39
33
|
|
|
40
34
|
return (
|
|
41
|
-
<PortalProvider
|
|
35
|
+
<PortalProvider
|
|
36
|
+
__unstable_elements={{
|
|
37
|
+
portal1: portal1Element,
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
42
40
|
<Flex align="center" height="fill" justify="center">
|
|
43
41
|
<BoundaryElementProvider element={useBoundaryElement ? boundaryElement : null}>
|
|
44
42
|
<Card
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
flip,
|
|
6
6
|
offset,
|
|
7
7
|
shift,
|
|
8
|
-
size,
|
|
9
8
|
useFloating,
|
|
10
9
|
type Middleware,
|
|
11
10
|
type RootBoundary,
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
useState,
|
|
23
22
|
useId,
|
|
24
23
|
useImperativeHandle,
|
|
24
|
+
useLayoutEffect,
|
|
25
25
|
} from 'react'
|
|
26
26
|
import {styled} from 'styled-components'
|
|
27
27
|
import {useEffectEvent} from 'use-effect-event'
|
|
@@ -127,6 +127,7 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
127
127
|
const [referenceElement, setReferenceElement] = useState<HTMLElement | null>(null)
|
|
128
128
|
const arrowRef = useRef<HTMLDivElement | null>(null)
|
|
129
129
|
const rootBoundary: RootBoundary = 'viewport'
|
|
130
|
+
const [tooltipMaxWidth, setTooltipMaxWidth] = useState(0)
|
|
130
131
|
|
|
131
132
|
useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(forwardedRef, () => ref.current)
|
|
132
133
|
|
|
@@ -137,28 +138,6 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
137
138
|
const middleware = useMemo(() => {
|
|
138
139
|
const ret: Middleware[] = []
|
|
139
140
|
|
|
140
|
-
// Set the max width of the tooltip based on boundaries and portals
|
|
141
|
-
ret.push(
|
|
142
|
-
size({
|
|
143
|
-
apply({elements}) {
|
|
144
|
-
// Get the maximum tooltip width (sans tooltip padding)
|
|
145
|
-
// Tooltip width should never exceed the width of either any supplied boundary or portal element.
|
|
146
|
-
// If both portal and boundary elements are provided, use the smaller width of the two.
|
|
147
|
-
const availableWidths = [
|
|
148
|
-
...(boundaryElement ? [boundaryElement.offsetWidth] : []),
|
|
149
|
-
portalElement?.offsetWidth || document.body.offsetWidth,
|
|
150
|
-
]
|
|
151
|
-
|
|
152
|
-
const tooltipWidth = Math.min(...availableWidths) - DEFAULT_TOOLTIP_PADDING * 2
|
|
153
|
-
|
|
154
|
-
// Set the max width directly, efficiently, without another react effect + render loop
|
|
155
|
-
Object.assign(elements.floating.style, {
|
|
156
|
-
maxWidth: tooltipWidth > 0 ? `${tooltipWidth}px` : undefined,
|
|
157
|
-
})
|
|
158
|
-
},
|
|
159
|
-
}),
|
|
160
|
-
)
|
|
161
|
-
|
|
162
141
|
// Flip the floating element when leaving the boundary box
|
|
163
142
|
ret.push(
|
|
164
143
|
flip({
|
|
@@ -193,7 +172,7 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
193
172
|
}
|
|
194
173
|
|
|
195
174
|
return ret
|
|
196
|
-
}, [animate, arrowProp, boundaryElement, fallbackPlacements
|
|
175
|
+
}, [animate, arrowProp, boundaryElement, fallbackPlacements])
|
|
197
176
|
|
|
198
177
|
const {floatingStyles, placement, middlewareData, refs, update} = useFloating({
|
|
199
178
|
middleware,
|
|
@@ -322,6 +301,19 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
322
301
|
// Handle closing the tooltip when the mouse leaves the referenceElement
|
|
323
302
|
useCloseOnMouseLeave({handleIsOpenChange, referenceElement, showTooltip})
|
|
324
303
|
|
|
304
|
+
// // Set the max width of the tooltip based on boundaries and portals
|
|
305
|
+
useLayoutEffect(() => {
|
|
306
|
+
// Get the maximum tooltip width (sans tooltip padding)
|
|
307
|
+
// Tooltip width should never exceed the width of either any supplied boundary or portal element.
|
|
308
|
+
// If both portal and boundary elements are provided, use the smaller width of the two.
|
|
309
|
+
const availableWidths = [
|
|
310
|
+
...(boundaryElement ? [boundaryElement.offsetWidth] : []),
|
|
311
|
+
portalElement?.offsetWidth || document.body.offsetWidth,
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
setTooltipMaxWidth(Math.min(...availableWidths) - DEFAULT_TOOLTIP_PADDING * 2)
|
|
315
|
+
}, [boundaryElement, portalElement])
|
|
316
|
+
|
|
325
317
|
const setArrow = useCallback(
|
|
326
318
|
(arrowEl: HTMLDivElement | null) => {
|
|
327
319
|
arrowRef.current = arrowEl
|
|
@@ -384,7 +376,10 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
384
376
|
data-ui="Tooltip"
|
|
385
377
|
{...restProps}
|
|
386
378
|
ref={setFloating}
|
|
387
|
-
style={
|
|
379
|
+
style={{
|
|
380
|
+
...floatingStyles,
|
|
381
|
+
maxWidth: tooltipMaxWidth > 0 ? `${tooltipMaxWidth}px` : undefined,
|
|
382
|
+
}}
|
|
388
383
|
zOffset={zOffset}
|
|
389
384
|
>
|
|
390
385
|
<TooltipCard
|
|
@@ -451,12 +446,11 @@ function useCloseOnMouseLeave({
|
|
|
451
446
|
// Since we don't want the `mouseevent` events to be attached and removed if the `referenceElement` is changed
|
|
452
447
|
// we use a "effect event" (https://19.react.dev/learn/separating-events-from-effects#reading-latest-props-and-state-with-effect-events)
|
|
453
448
|
// in order to always see the latest `referenceElement` value inside the event handler itself.
|
|
454
|
-
const
|
|
449
|
+
const onMouseMove = useEffectEvent((target: EventTarget | null) => {
|
|
455
450
|
if (!referenceElement) return
|
|
456
451
|
|
|
457
452
|
const isHoveringReference =
|
|
458
|
-
referenceElement ===
|
|
459
|
-
(event.target instanceof Node && referenceElement.contains(event.target))
|
|
453
|
+
referenceElement === target || (target instanceof Node && referenceElement.contains(target))
|
|
460
454
|
|
|
461
455
|
if (!isHoveringReference) {
|
|
462
456
|
handleIsOpenChange(false)
|
|
@@ -469,8 +463,12 @@ function useCloseOnMouseLeave({
|
|
|
469
463
|
useEffect(() => {
|
|
470
464
|
if (!showTooltip) return
|
|
471
465
|
|
|
472
|
-
|
|
466
|
+
const handleMouseMove = (event: MouseEvent) => {
|
|
467
|
+
onMouseMove(event.target)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
window.addEventListener('mousemove', handleMouseMove)
|
|
473
471
|
|
|
474
|
-
return () => window.removeEventListener('mousemove',
|
|
475
|
-
}, [
|
|
472
|
+
return () => window.removeEventListener('mousemove', handleMouseMove)
|
|
473
|
+
}, [onMouseMove, showTooltip])
|
|
476
474
|
}
|
|
@@ -36,7 +36,6 @@ export function TooltipDelayGroupProvider(
|
|
|
36
36
|
const openDelay = typeof delay === 'number' ? delay : delay?.open || 0
|
|
37
37
|
const closeDelay = typeof delay === 'number' ? delay : delay?.close || 0
|
|
38
38
|
|
|
39
|
-
// @TODO split out into separate contexts
|
|
40
39
|
const value: TooltipDelayGroupContextValue = useMemo(
|
|
41
40
|
() => ({
|
|
42
41
|
isGroupActive: isGroupActive,
|
|
@@ -91,7 +91,6 @@ export function LayerProvider(props: LayerProviderProps): React.ReactElement {
|
|
|
91
91
|
// Register this layer on mount
|
|
92
92
|
useEffect(() => parentRegisterChild?.(level), [level, parentRegisterChild])
|
|
93
93
|
|
|
94
|
-
// @TODO split out into separate contexts
|
|
95
94
|
const value: LayerContextValue = useMemo(
|
|
96
95
|
() => ({
|
|
97
96
|
version: 0.0,
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import {Card, Container, Portal, PortalProvider, Stack, Text} from '@sanity/ui'
|
|
2
|
-
import {
|
|
2
|
+
import {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
|
-
)
|
|
16
8
|
|
|
17
9
|
return (
|
|
18
|
-
<PortalProvider
|
|
10
|
+
<PortalProvider
|
|
11
|
+
__unstable_elements={{
|
|
12
|
+
portal1: portal1Element,
|
|
13
|
+
portal2: portal2Element,
|
|
14
|
+
portal3: portal3Element,
|
|
15
|
+
}}
|
|
16
|
+
>
|
|
19
17
|
<Container width={1}>
|
|
20
18
|
<Card height="fill" padding={4}>
|
|
21
19
|
<Stack space={2}>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {useMemo} from 'react'
|
|
2
|
+
import {useUnique} from '../../hooks/_internal'
|
|
2
3
|
import {useMounted} from '../../hooks/useMounted'
|
|
3
4
|
import {PortalContext} from './portalContext'
|
|
4
5
|
import {PortalContextValue} from './types'
|
|
@@ -23,10 +24,10 @@ export interface PortalProviderProps {
|
|
|
23
24
|
* @public
|
|
24
25
|
*/
|
|
25
26
|
export function PortalProvider(props: PortalProviderProps): React.ReactElement {
|
|
26
|
-
const {boundaryElement, children, element, __unstable_elements:
|
|
27
|
+
const {boundaryElement, children, element, __unstable_elements: elementsProp} = props
|
|
28
|
+
const elements = useUnique(elementsProp)
|
|
27
29
|
const mounted = useMounted()
|
|
28
30
|
|
|
29
|
-
// @TODO split out into separate contexts
|
|
30
31
|
const value: PortalContextValue = useMemo(() => {
|
|
31
32
|
return {
|
|
32
33
|
version: 0.0,
|
|
@@ -1,46 +0,0 @@
|
|
|
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
|
-
}
|