@sanity/ui 1.0.7 → 1.0.8
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.ts +8 -0
- package/dist/index.esm.js +75 -78
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +74 -77
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/toast/toastProvider.tsx +55 -52
- package/src/hooks/useArrayProp.ts +2 -1
- package/src/hooks/useMediaIndex/useMediaIndex.test.tsx +31 -0
- package/src/hooks/useMediaIndex/useMediaIndex.ts +98 -6
- package/src/hooks/useMounted.ts +14 -0
- package/src/hooks/usePrefersDark.hydration.test.tsx +56 -0
- package/src/hooks/usePrefersDark.test.tsx +18 -0
- package/src/hooks/usePrefersDark.ts +51 -18
- package/src/observers/resizeObserver.ts +1 -1
- package/src/utils/portal/portalContext.ts +1 -1
- package/src/utils/portal/portalProvider.tsx +1 -1
- package/src/hooks/useMediaIndex/lib/media.ts +0 -88
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"source": "./src/index.ts",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@sanity/color": "^2.2.0",
|
|
50
50
|
"@sanity/icons": "^2.2.0",
|
|
51
51
|
"csstype": "^3.1.1",
|
|
52
|
-
"framer-motion": "^
|
|
52
|
+
"framer-motion": "^8.1.1",
|
|
53
53
|
"react-refractor": "^2.1.7"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import {AnimatePresence, motion} from 'framer-motion'
|
|
2
|
-
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
|
2
|
+
import {useCallback, useEffect, useMemo, useRef, useState, startTransition} from 'react'
|
|
3
3
|
import styled from 'styled-components'
|
|
4
|
+
import {useMounted} from '../../hooks/useMounted'
|
|
4
5
|
import {Box} from '../../primitives'
|
|
5
6
|
import {Layer} from '../../utils'
|
|
6
7
|
import {Toast} from './toast'
|
|
7
8
|
import {ToastContext} from './toastContext'
|
|
8
9
|
import {ToastContextValue, ToastParams} from './types'
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}[]
|
|
16
|
-
}
|
|
11
|
+
type ToastState = {
|
|
12
|
+
dismiss: () => void
|
|
13
|
+
id: string
|
|
14
|
+
params: ToastParams
|
|
15
|
+
}[]
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* @public
|
|
@@ -51,25 +50,29 @@ let toastId = 0
|
|
|
51
50
|
*/
|
|
52
51
|
export function ToastProvider(props: ToastProviderProps): React.ReactElement {
|
|
53
52
|
const {children, padding = 4, paddingX, paddingY, zOffset} = props
|
|
54
|
-
const [state,
|
|
53
|
+
const [state, _setState] = useState<ToastState>([])
|
|
54
|
+
|
|
55
55
|
const toastsRef = useRef<{[key: string]: {timeoutId: NodeJS.Timeout}}>({})
|
|
56
56
|
|
|
57
57
|
const push = useCallback((params: ToastParams) => {
|
|
58
|
+
// Wrap setState in startTransition to allow React to give input state updates higher priority
|
|
59
|
+
const setState: typeof _setState = (state) => startTransition(() => _setState(state))
|
|
60
|
+
|
|
58
61
|
const id = params.id || String(toastId++)
|
|
59
62
|
const duration = params.duration || 5000
|
|
60
63
|
|
|
61
64
|
const dismiss = () => {
|
|
62
65
|
const timeoutId = toastsRef.current[id]?.timeoutId
|
|
63
66
|
|
|
64
|
-
setState((prevState) => {
|
|
65
|
-
const idx = prevState.
|
|
67
|
+
setState((prevState): ToastState => {
|
|
68
|
+
const idx = prevState.findIndex((t) => t.id === id)
|
|
66
69
|
|
|
67
70
|
if (idx > -1) {
|
|
68
|
-
const toasts = prevState.
|
|
71
|
+
const toasts = prevState.slice(0)
|
|
69
72
|
|
|
70
73
|
toasts.splice(idx, 1)
|
|
71
74
|
|
|
72
|
-
return
|
|
75
|
+
return toasts
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
return prevState
|
|
@@ -81,19 +84,16 @@ export function ToastProvider(props: ToastProviderProps): React.ReactElement {
|
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
setState((prevState) => {
|
|
85
|
-
return
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
},
|
|
95
|
-
]),
|
|
96
|
-
}
|
|
87
|
+
setState((prevState): ToastState => {
|
|
88
|
+
return prevState
|
|
89
|
+
.filter((t) => t.id !== id)
|
|
90
|
+
.concat([
|
|
91
|
+
{
|
|
92
|
+
dismiss,
|
|
93
|
+
id,
|
|
94
|
+
params: {...params, duration},
|
|
95
|
+
},
|
|
96
|
+
])
|
|
97
97
|
})
|
|
98
98
|
|
|
99
99
|
if (toastsRef.current[id]) {
|
|
@@ -119,37 +119,40 @@ export function ToastProvider(props: ToastProviderProps): React.ReactElement {
|
|
|
119
119
|
)
|
|
120
120
|
|
|
121
121
|
const value: ToastContextValue = useMemo(() => ({version: 0.0, push}), [push])
|
|
122
|
+
const mounted = useMounted()
|
|
122
123
|
|
|
123
124
|
return (
|
|
124
125
|
<ToastContext.Provider value={value}>
|
|
125
126
|
{children}
|
|
126
127
|
|
|
127
|
-
|
|
128
|
-
<
|
|
129
|
-
<
|
|
130
|
-
<
|
|
131
|
-
{
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
128
|
+
{mounted && (
|
|
129
|
+
<Root data-ui="ToastProvider" zOffset={zOffset}>
|
|
130
|
+
<ToastContainer>
|
|
131
|
+
<Box padding={padding} paddingX={paddingX} paddingY={paddingY}>
|
|
132
|
+
<AnimatePresence initial={false}>
|
|
133
|
+
{state.map(({dismiss, id, params}) => (
|
|
134
|
+
<motion.div
|
|
135
|
+
animate={{opacity: 1, y: 0, scale: 1}}
|
|
136
|
+
exit={{opacity: 0, scale: 0.5, transition: {duration: 0.2}}}
|
|
137
|
+
initial={{opacity: 0, y: 32, scale: 0.25}}
|
|
138
|
+
key={id}
|
|
139
|
+
layout="position"
|
|
140
|
+
transition={{type: 'spring', damping: 30, stiffness: 400}}
|
|
141
|
+
>
|
|
142
|
+
<Toast
|
|
143
|
+
closable={params.closable}
|
|
144
|
+
description={params.description}
|
|
145
|
+
onClose={dismiss}
|
|
146
|
+
status={params.status}
|
|
147
|
+
title={params.title}
|
|
148
|
+
/>
|
|
149
|
+
</motion.div>
|
|
150
|
+
))}
|
|
151
|
+
</AnimatePresence>
|
|
152
|
+
</Box>
|
|
153
|
+
</ToastContainer>
|
|
154
|
+
</Root>
|
|
155
|
+
)}
|
|
153
156
|
</ToastContext.Provider>
|
|
154
157
|
)
|
|
155
158
|
}
|
|
@@ -12,7 +12,8 @@ export function useArrayProp<T extends ArrayPropPrimitive = ArrayPropPrimitive>(
|
|
|
12
12
|
val: T | T[] | undefined,
|
|
13
13
|
defaultVal?: T[]
|
|
14
14
|
): T[] {
|
|
15
|
-
|
|
15
|
+
// JSON.stringify is fast, but it's not faster than useMemo's referencial equality check
|
|
16
|
+
const __perf_hash__ = useMemo(() => JSON.stringify(val ?? defaultVal), [defaultVal, val])
|
|
16
17
|
|
|
17
18
|
return useMemo(
|
|
18
19
|
() => _getArrayProp(val, defaultVal),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** @jest-environment node */
|
|
2
|
+
import {renderToString, renderToStaticMarkup} from 'react-dom/server'
|
|
3
|
+
import {studioTheme, ThemeProvider} from '../../theme'
|
|
4
|
+
import {useMediaIndex} from './useMediaIndex'
|
|
5
|
+
|
|
6
|
+
function Log() {
|
|
7
|
+
const mediaIndex = useMediaIndex()
|
|
8
|
+
|
|
9
|
+
return <>mediaIndex: {JSON.stringify(mediaIndex)}</>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('useMediaIndex', () => {
|
|
13
|
+
it(`SSR to static markup returns 0`, () => {
|
|
14
|
+
expect(
|
|
15
|
+
renderToStaticMarkup(
|
|
16
|
+
<ThemeProvider theme={studioTheme}>
|
|
17
|
+
<Log />
|
|
18
|
+
</ThemeProvider>
|
|
19
|
+
)
|
|
20
|
+
).toBe('mediaIndex: 0')
|
|
21
|
+
})
|
|
22
|
+
it(`SSR to markup for hydration doesn't throw`, () => {
|
|
23
|
+
expect(
|
|
24
|
+
renderToString(
|
|
25
|
+
<ThemeProvider theme={studioTheme}>
|
|
26
|
+
<Log />
|
|
27
|
+
</ThemeProvider>
|
|
28
|
+
)
|
|
29
|
+
).toMatchInlineSnapshot(`"mediaIndex: <!-- -->0"`)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -1,6 +1,95 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {useSyncExternalStore} from 'react'
|
|
2
2
|
import {useTheme} from '../../theme'
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export interface _MediaStore {
|
|
8
|
+
subscribe: (onStoreChange: () => void) => () => void
|
|
9
|
+
getSnapshot: () => number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const MEDIA_STORE_CACHE = new WeakMap<number[], _MediaStore>()
|
|
13
|
+
|
|
14
|
+
type MediaQueryMinWidth = `(min-width: ${number}px)`
|
|
15
|
+
type MediaQueryMaxWidth = `(max-width: ${number}px)`
|
|
16
|
+
type MediaQueryMinMaxWidth = `${MediaQueryMinWidth} and ${MediaQueryMaxWidth}`
|
|
17
|
+
type MediaQuery = `screen and ${MediaQueryMinWidth | MediaQueryMaxWidth | MediaQueryMinMaxWidth}`
|
|
18
|
+
|
|
19
|
+
function _getMediaQuery(media: number[], index: number): MediaQuery {
|
|
20
|
+
if (index === 0) {
|
|
21
|
+
return `screen and (max-width: ${media[index] - 1}px)`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (index === media.length) {
|
|
25
|
+
return `screen and (min-width: ${media[index - 1]}px)`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return `screen and (min-width: ${media[index - 1]}px) and (max-width: ${media[index] - 1}px)`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function _createMediaStore(media: number[]): _MediaStore {
|
|
32
|
+
const mediaLen = media.length
|
|
33
|
+
let sizes: {mq: MediaQueryList; index: number}[]
|
|
34
|
+
|
|
35
|
+
// The _createMediaStore function is called in both server and client environments.
|
|
36
|
+
// However since subscribe and getSnapshot are only called on the client we lazy init what we need for them
|
|
37
|
+
// so that we don't need to run checks for wether it's safe to call `window.matchMedia`
|
|
38
|
+
const getSizes = () => {
|
|
39
|
+
if (!sizes) {
|
|
40
|
+
sizes = []
|
|
41
|
+
|
|
42
|
+
for (let index = mediaLen; index > -1; index -= 1) {
|
|
43
|
+
const mediaQuery = _getMediaQuery(media, index)
|
|
44
|
+
|
|
45
|
+
sizes.push({index, mq: window.matchMedia(mediaQuery)})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return sizes
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const getSnapshot = () => {
|
|
53
|
+
for (const {index, mq} of getSizes()) {
|
|
54
|
+
if (mq.matches) return index
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return 0
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const subscribe = (onStoreChange: () => void) => {
|
|
61
|
+
const disposeFns: (() => void)[] = []
|
|
62
|
+
|
|
63
|
+
for (const {mq} of getSizes()) {
|
|
64
|
+
const handleChange = () => {
|
|
65
|
+
if (mq.matches) onStoreChange()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
mq.addEventListener('change', handleChange)
|
|
69
|
+
|
|
70
|
+
disposeFns.push(() => mq.removeEventListener('change', handleChange))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return () => {
|
|
74
|
+
for (const disposeFn of disposeFns) {
|
|
75
|
+
disposeFn()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {getSnapshot, subscribe}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Only called during server-side rendering, and hydration if using hydrateRoot
|
|
85
|
+
* Since the server environment doesn't have access to the DOM, we can't determine the current value of the media query
|
|
86
|
+
* and we assume `(prefers-color-scheme: light)` since it's the most common scheme
|
|
87
|
+
*
|
|
88
|
+
* @link https://beta.reactjs.org/apis/react/useSyncExternalStore#adding-support-for-server-rendering
|
|
89
|
+
*/
|
|
90
|
+
function getServerSnapshot() {
|
|
91
|
+
return 0
|
|
92
|
+
}
|
|
4
93
|
|
|
5
94
|
/**
|
|
6
95
|
* This API might change. DO NOT USE IN PRODUCTION.
|
|
@@ -9,10 +98,13 @@ import {_getMediaManager} from './lib/media'
|
|
|
9
98
|
export function useMediaIndex(): number {
|
|
10
99
|
const theme = useTheme()
|
|
11
100
|
const {media} = theme.sanity
|
|
12
|
-
const manager = useMemo(() => _getMediaManager(media), [media])
|
|
13
|
-
const [index, setIndex] = useState(manager.getCurrentIndex)
|
|
14
101
|
|
|
15
|
-
|
|
102
|
+
let store = MEDIA_STORE_CACHE.get(media)
|
|
103
|
+
|
|
104
|
+
if (!store) {
|
|
105
|
+
store = _createMediaStore(media)
|
|
106
|
+
MEDIA_STORE_CACHE.set(media, store)
|
|
107
|
+
}
|
|
16
108
|
|
|
17
|
-
return
|
|
109
|
+
return useSyncExternalStore(store.subscribe, store.getSnapshot, getServerSnapshot)
|
|
18
110
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {useEffect, useReducer} from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Some components should only render after mounting to the DOM, and not be rendered at all during SSR renderToString or equivalent.
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export function useMounted(): boolean {
|
|
8
|
+
// Use useReducer instead of useState as it's more low-level and creates the least amount of functions for the garbage collector to clean up
|
|
9
|
+
const [mounted, mount] = useReducer(() => true, false)
|
|
10
|
+
|
|
11
|
+
useEffect(mount, [mount])
|
|
12
|
+
|
|
13
|
+
return mounted
|
|
14
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
/**
|
|
3
|
+
* As this hook is used for top-level theming it's likely to be called while server-rendering
|
|
4
|
+
* and that's why it's worth it to have a testing suite for hydration
|
|
5
|
+
*/
|
|
6
|
+
import {waitFor} from '@testing-library/dom'
|
|
7
|
+
import {hydrateRoot} from 'react-dom/client'
|
|
8
|
+
|
|
9
|
+
import {usePrefersDark} from './usePrefersDark'
|
|
10
|
+
|
|
11
|
+
function Log() {
|
|
12
|
+
const dark = usePrefersDark()
|
|
13
|
+
|
|
14
|
+
return <>dark: {JSON.stringify(dark)}</>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const originalMatchMedia = window.matchMedia
|
|
18
|
+
|
|
19
|
+
describe('usePrefersDark SSR hydration', () => {
|
|
20
|
+
beforeAll(() => {
|
|
21
|
+
window.matchMedia = () =>
|
|
22
|
+
({
|
|
23
|
+
addEventListener: jest.fn(),
|
|
24
|
+
removeEventListener: jest.fn(),
|
|
25
|
+
matches: true,
|
|
26
|
+
} as any)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
afterAll(() => {
|
|
30
|
+
window.matchMedia = originalMatchMedia
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it(`hydrates without any warnings`, async () => {
|
|
34
|
+
const spy = jest.spyOn(console, 'error').mockImplementation()
|
|
35
|
+
|
|
36
|
+
const node = document.createElement('div')
|
|
37
|
+
|
|
38
|
+
document.body.appendChild(node)
|
|
39
|
+
|
|
40
|
+
node.innerHTML = `dark: <!-- -->false`
|
|
41
|
+
|
|
42
|
+
hydrateRoot(node, <Log />)
|
|
43
|
+
|
|
44
|
+
// It's false initially
|
|
45
|
+
await waitFor(() => expect(node.innerHTML).toBe('dark: <!-- -->false'))
|
|
46
|
+
|
|
47
|
+
// After hydration it should switch to true
|
|
48
|
+
await waitFor(() => expect(node.innerHTML).toBe('dark: <!-- -->true'))
|
|
49
|
+
|
|
50
|
+
// eslint-disable-next-line no-console
|
|
51
|
+
expect(console.error).not.toHaveBeenCalled()
|
|
52
|
+
|
|
53
|
+
spy.mockReset()
|
|
54
|
+
spy.mockRestore()
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** @jest-environment node */
|
|
2
|
+
import {renderToString, renderToStaticMarkup} from 'react-dom/server'
|
|
3
|
+
import {usePrefersDark} from './usePrefersDark'
|
|
4
|
+
|
|
5
|
+
function Log() {
|
|
6
|
+
const dark = usePrefersDark()
|
|
7
|
+
|
|
8
|
+
return <>dark: {JSON.stringify(dark)}</>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('usePrefersDark', () => {
|
|
12
|
+
it(`SSR to static markup returns false`, () => {
|
|
13
|
+
expect(renderToStaticMarkup(<Log />)).toBe('dark: false')
|
|
14
|
+
})
|
|
15
|
+
it(`SSR to markup for hydration doesn't throw`, () => {
|
|
16
|
+
expect(renderToString(<Log />)).toMatchInlineSnapshot(`"dark: <!-- -->false"`)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -1,28 +1,61 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {useSyncExternalStore} from 'react'
|
|
2
|
+
|
|
3
|
+
let MEDIA_QUERY_CACHE: MediaQueryList | undefined
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
6
|
+
* Lazy init the matchMedia instance
|
|
5
7
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
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
|
+
}
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
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()
|
|
17
30
|
|
|
18
|
-
|
|
31
|
+
matchMedia.addEventListener('change', onStoreChange)
|
|
19
32
|
|
|
20
|
-
|
|
33
|
+
return () => matchMedia.removeEventListener('change', onStoreChange)
|
|
34
|
+
}
|
|
21
35
|
|
|
22
|
-
|
|
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
|
+
}
|
|
23
44
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
49
|
+
*
|
|
50
|
+
* @link https://beta.reactjs.org/apis/react/useSyncExternalStore#adding-support-for-server-rendering
|
|
51
|
+
*/
|
|
52
|
+
function getServerSnapshot() {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
26
55
|
|
|
27
|
-
|
|
56
|
+
/**
|
|
57
|
+
* @public
|
|
58
|
+
*/
|
|
59
|
+
export function usePrefersDark(): boolean {
|
|
60
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
|
28
61
|
}
|
|
@@ -4,6 +4,6 @@ import {ResizeObserver as ResizeObserverPolyfill} from '@juggle/resize-observer'
|
|
|
4
4
|
* @internal
|
|
5
5
|
*/
|
|
6
6
|
export const _ResizeObserver: typeof ResizeObserver =
|
|
7
|
-
typeof
|
|
7
|
+
typeof document !== 'undefined' && window.ResizeObserver
|
|
8
8
|
? window.ResizeObserver
|
|
9
9
|
: ResizeObserverPolyfill
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @internal
|
|
3
|
-
*/
|
|
4
|
-
export interface _MediaManager {
|
|
5
|
-
getCurrentIndex: () => number
|
|
6
|
-
subscribe: (subscriber: (index: number) => void) => () => void
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const MEDIA_MANAGER_CACHE = new WeakMap<number[], _MediaManager>()
|
|
10
|
-
|
|
11
|
-
function _getMediaQuery(media: number[], index: number) {
|
|
12
|
-
if (index === 0) {
|
|
13
|
-
return `screen and (max-width: ${media[index] - 1}px)`
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (index === media.length) {
|
|
17
|
-
return `screen and (min-width: ${media[index - 1]}px)`
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return `screen and (min-width: ${media[index - 1]}px) and (max-width: ${media[index] - 1}px)`
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function _createMediaManager(media: number[]): _MediaManager {
|
|
24
|
-
const mediaLen = media.length
|
|
25
|
-
const sizes: {mq: MediaQueryList; index: number}[] = []
|
|
26
|
-
|
|
27
|
-
if (typeof window !== 'undefined') {
|
|
28
|
-
for (let index = mediaLen; index > -1; index -= 1) {
|
|
29
|
-
const mediaQuery = _getMediaQuery(media, index)
|
|
30
|
-
|
|
31
|
-
sizes.push({index, mq: window.matchMedia(mediaQuery)})
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const getCurrentIndex = () => {
|
|
36
|
-
for (const {index, mq} of sizes) {
|
|
37
|
-
if (mq.matches) return index
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return 0
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const subscribe = (subscriber: (index: number) => void) => {
|
|
44
|
-
const disposeFns: (() => void)[] = []
|
|
45
|
-
|
|
46
|
-
for (const {index, mq} of sizes) {
|
|
47
|
-
const handleChange = () => {
|
|
48
|
-
if (mq.matches) subscriber(index)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (mq.addEventListener) {
|
|
52
|
-
mq.addEventListener('change', handleChange)
|
|
53
|
-
} else {
|
|
54
|
-
mq.addListener(handleChange)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
disposeFns.push(() => {
|
|
58
|
-
if (mq.removeEventListener) {
|
|
59
|
-
mq.removeEventListener('change', handleChange)
|
|
60
|
-
} else {
|
|
61
|
-
mq.removeListener(handleChange)
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return () => {
|
|
67
|
-
for (const disposeFn of disposeFns) {
|
|
68
|
-
disposeFn()
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return {getCurrentIndex, subscribe}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* @internal
|
|
78
|
-
*/
|
|
79
|
-
export function _getMediaManager(media: number[]): _MediaManager {
|
|
80
|
-
let manager = MEDIA_MANAGER_CACHE.get(media)
|
|
81
|
-
|
|
82
|
-
if (!manager) {
|
|
83
|
-
manager = _createMediaManager(media)
|
|
84
|
-
MEDIA_MANAGER_CACHE.set(media, manager)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return manager
|
|
88
|
-
}
|