@sanity/ui 2.6.5 → 2.6.7-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 +42 -6
- package/dist/index.d.ts +42 -6
- package/dist/index.esm.js +80 -147
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +79 -146
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +80 -147
- package/dist/index.mjs.map +1 -1
- package/package.json +26 -34
- package/src/core/components/breadcrumbs/breadcrumbs.tsx +15 -6
- package/src/core/components/dialog/dialog.tsx +12 -21
- package/src/core/components/menu/menu.tsx +11 -18
- 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 +11 -17
- package/src/core/components/toast/styles.ts +2 -1
- package/src/core/components/toast/useToast.ts +1 -0
- package/src/core/components/tree/tree.tsx +1 -0
- package/src/core/components/tree/treeItem.tsx +1 -0
- package/src/core/helpers/focus.ts +1 -0
- package/src/core/helpers/scroll.ts +1 -0
- package/src/core/hooks/index.ts +3 -2
- package/src/core/hooks/useClickOutside.ts +38 -72
- package/src/core/hooks/useElementSize.ts +1 -0
- package/src/core/hooks/useMatchMedia.ts +46 -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/primitives/popover/__workshop__/AlignedStory.tsx +9 -7
- package/src/core/primitives/tooltip/__workshop__/customPortal.tsx +8 -6
- package/src/core/primitives/tooltip/tooltip.tsx +7 -5
- package/src/core/primitives/tooltip/tooltipDelayGroup/tooltipDelayGroupProvider.tsx +1 -0
- package/src/core/utils/layer/layerProvider.tsx +1 -0
- package/src/core/utils/portal/__workshop__/named.tsx +10 -8
- package/src/core/utils/portal/portalProvider.tsx +2 -3
- 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.6.
|
|
3
|
+
"version": "2.6.7-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"
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
forwardRef,
|
|
4
|
+
Fragment,
|
|
5
|
+
isValidElement,
|
|
6
|
+
useCallback,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from 'react'
|
|
2
11
|
import {useArrayProp, useClickOutside} from '../../hooks'
|
|
3
12
|
import {Box, Popover, Stack, Text} from '../../primitives'
|
|
4
13
|
import {ExpandButton, Root} from './breadcrumbs.styles'
|
|
@@ -22,13 +31,13 @@ export const Breadcrumbs = forwardRef(function Breadcrumbs(
|
|
|
22
31
|
const {children, maxLength, separator, space: spaceRaw = 2, ...restProps} = props
|
|
23
32
|
const space = useArrayProp(spaceRaw)
|
|
24
33
|
const [open, setOpen] = useState(false)
|
|
25
|
-
const
|
|
26
|
-
const
|
|
34
|
+
const expandElementRef = useRef<HTMLButtonElement | null>(null)
|
|
35
|
+
const popoverElementRef = useRef<HTMLDivElement | null>(null)
|
|
27
36
|
|
|
28
37
|
const collapse = useCallback(() => setOpen(false), [])
|
|
29
38
|
const expand = useCallback(() => setOpen(true), [])
|
|
30
39
|
|
|
31
|
-
useClickOutside(collapse, [
|
|
40
|
+
useClickOutside(collapse, () => [expandElementRef.current, popoverElementRef.current])
|
|
32
41
|
|
|
33
42
|
const rawItems = useMemo(() => Children.toArray(children).filter(isValidElement), [children])
|
|
34
43
|
|
|
@@ -52,14 +61,14 @@ export const Breadcrumbs = forwardRef(function Breadcrumbs(
|
|
|
52
61
|
open={open}
|
|
53
62
|
placement="top"
|
|
54
63
|
portal
|
|
55
|
-
ref={
|
|
64
|
+
ref={popoverElementRef}
|
|
56
65
|
>
|
|
57
66
|
<ExpandButton
|
|
58
67
|
fontSize={1}
|
|
59
68
|
mode="bleed"
|
|
60
69
|
onClick={open ? collapse : expand}
|
|
61
70
|
padding={1}
|
|
62
|
-
ref={
|
|
71
|
+
ref={expandElementRef}
|
|
63
72
|
selected={open}
|
|
64
73
|
text="…"
|
|
65
74
|
/>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {CloseIcon} from '@sanity/icons'
|
|
2
2
|
import {ThemeColorSchemeKey} from '@sanity/ui/theme'
|
|
3
|
-
import {forwardRef, useCallback, useEffect, useImperativeHandle, useRef
|
|
3
|
+
import {forwardRef, useCallback, useEffect, useImperativeHandle, useRef} from 'react'
|
|
4
4
|
import {styled} from 'styled-components'
|
|
5
5
|
import {
|
|
6
6
|
containsOrEqualsElement,
|
|
@@ -170,7 +170,6 @@ const DialogCard = forwardRef(function DialogCard(
|
|
|
170
170
|
const shadow = useArrayProp(shadowProp)
|
|
171
171
|
const width = useArrayProp(widthProp)
|
|
172
172
|
const ref = useRef<HTMLDivElement | null>(null)
|
|
173
|
-
const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
|
|
174
173
|
const contentRef = useRef<HTMLDivElement | null>(null)
|
|
175
174
|
const layer = useLayer()
|
|
176
175
|
const {isTopLayer} = layer
|
|
@@ -216,32 +215,24 @@ const DialogCard = forwardRef(function DialogCard(
|
|
|
216
215
|
)
|
|
217
216
|
|
|
218
217
|
useClickOutside(
|
|
219
|
-
|
|
220
|
-
(
|
|
221
|
-
if (!isTopLayer || !onClickOutside) return
|
|
218
|
+
(event: MouseEvent | TouchEvent) => {
|
|
219
|
+
if (!isTopLayer || !onClickOutside) return
|
|
222
220
|
|
|
223
|
-
|
|
221
|
+
const target = event.target as Node | null
|
|
224
222
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
223
|
+
if (target && !isTargetWithinScope(boundaryElement, portalElement, target)) {
|
|
224
|
+
// Ignore clicks outside of the scope
|
|
225
|
+
return
|
|
226
|
+
}
|
|
229
227
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
),
|
|
234
|
-
[rootElement],
|
|
228
|
+
onClickOutside()
|
|
229
|
+
},
|
|
230
|
+
() => [ref.current],
|
|
235
231
|
)
|
|
236
232
|
|
|
237
|
-
const setRef = useCallback((el: HTMLDivElement | null) => {
|
|
238
|
-
setRootElement(el)
|
|
239
|
-
ref.current = el
|
|
240
|
-
}, [])
|
|
241
|
-
|
|
242
233
|
return (
|
|
243
234
|
<DialogContainer data-ui="DialogCard" width={width}>
|
|
244
|
-
<DialogCardRoot radius={radius} ref={
|
|
235
|
+
<DialogCardRoot radius={radius} ref={ref} scheme={scheme} shadow={shadow}>
|
|
245
236
|
<DialogLayout direction="column">
|
|
246
237
|
{showHeader && (
|
|
247
238
|
<DialogHeader>
|
|
@@ -19,7 +19,7 @@ export interface MenuProps extends ResponsivePaddingProps {
|
|
|
19
19
|
* @deprecated Use `shouldFocus="last"` instead.
|
|
20
20
|
*/
|
|
21
21
|
focusLast?: boolean
|
|
22
|
-
onClickOutside?: (event: MouseEvent) => void
|
|
22
|
+
onClickOutside?: (event: MouseEvent | TouchEvent) => void
|
|
23
23
|
onEscape?: () => void
|
|
24
24
|
onItemClick?: () => void
|
|
25
25
|
onItemSelect?: (index: number) => void
|
|
@@ -75,16 +75,18 @@ export const Menu = forwardRef(function Menu(
|
|
|
75
75
|
handleItemMouseLeave,
|
|
76
76
|
handleKeyDown,
|
|
77
77
|
mount,
|
|
78
|
-
|
|
79
|
-
setRootElement,
|
|
80
|
-
} = useMenuController({onKeyDown, originElement, shouldFocus})
|
|
78
|
+
} = useMenuController({onKeyDown, originElement, shouldFocus, rootElementRef: ref})
|
|
81
79
|
|
|
82
80
|
const handleRefChange = useCallback(
|
|
83
81
|
(el: HTMLDivElement | null) => {
|
|
84
|
-
setRootElement(el)
|
|
85
82
|
ref.current = el
|
|
83
|
+
|
|
84
|
+
// Register root element (for nested menus)
|
|
85
|
+
if (ref.current && registerElement) {
|
|
86
|
+
registerElement(ref.current)
|
|
87
|
+
}
|
|
86
88
|
},
|
|
87
|
-
[
|
|
89
|
+
[registerElement],
|
|
88
90
|
)
|
|
89
91
|
|
|
90
92
|
// Trigger `onItemSelect` when active index changes
|
|
@@ -94,11 +96,8 @@ export const Menu = forwardRef(function Menu(
|
|
|
94
96
|
|
|
95
97
|
// Close menu when clicking outside
|
|
96
98
|
useClickOutside(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
[isTopLayer, onClickOutside],
|
|
100
|
-
),
|
|
101
|
-
[rootElement],
|
|
99
|
+
(event) => isTopLayer && onClickOutside && onClickOutside(event),
|
|
100
|
+
() => [ref.current],
|
|
102
101
|
)
|
|
103
102
|
|
|
104
103
|
// Close menu when pressing Escape
|
|
@@ -116,13 +115,7 @@ export const Menu = forwardRef(function Menu(
|
|
|
116
115
|
),
|
|
117
116
|
)
|
|
118
117
|
|
|
119
|
-
//
|
|
120
|
-
useEffect(() => {
|
|
121
|
-
if (!rootElement || !registerElement) return
|
|
122
|
-
|
|
123
|
-
return registerElement(rootElement)
|
|
124
|
-
}, [registerElement, rootElement])
|
|
125
|
-
|
|
118
|
+
// @TODO split out into separate contexts
|
|
126
119
|
const value: MenuContextValue = useMemo(
|
|
127
120
|
() => ({
|
|
128
121
|
version: 0.0,
|
|
@@ -140,7 +140,7 @@ export const MenuButton = forwardRef(function MenuButton(
|
|
|
140
140
|
}, [])
|
|
141
141
|
|
|
142
142
|
const handleMenuClickOutside = useCallback(
|
|
143
|
-
(event: MouseEvent) => {
|
|
143
|
+
(event: MouseEvent | TouchEvent) => {
|
|
144
144
|
const target = event.target
|
|
145
145
|
|
|
146
146
|
if (!(target instanceof Node)) {
|
|
@@ -5,7 +5,7 @@ export interface MenuContextValue {
|
|
|
5
5
|
activeElement: HTMLElement | null
|
|
6
6
|
activeIndex: number
|
|
7
7
|
mount: (element: HTMLElement | null, selected?: boolean) => () => void
|
|
8
|
-
onClickOutside?: (event: MouseEvent) => void
|
|
8
|
+
onClickOutside?: (event: MouseEvent | TouchEvent) => void
|
|
9
9
|
onEscape?: () => void
|
|
10
10
|
onItemClick?: () => void
|
|
11
11
|
onItemMouseEnter?: (event: React.MouseEvent<HTMLElement>) => void
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {useCallback, useEffect, useRef, useState} from 'react'
|
|
1
|
+
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
|
2
2
|
import {_getFocusableElements, _sortElements} from './helpers'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -11,8 +11,6 @@ export interface MenuController {
|
|
|
11
11
|
handleItemMouseLeave: () => void
|
|
12
12
|
handleKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void
|
|
13
13
|
mount: (element: HTMLElement | null, selected?: boolean) => () => void
|
|
14
|
-
rootElement: HTMLDivElement | null
|
|
15
|
-
setRootElement: (el: HTMLDivElement | null) => void
|
|
16
14
|
}
|
|
17
15
|
|
|
18
16
|
/**
|
|
@@ -24,14 +22,14 @@ export function useMenuController(props: {
|
|
|
24
22
|
onKeyDown?: React.KeyboardEventHandler
|
|
25
23
|
originElement?: HTMLElement | null
|
|
26
24
|
shouldFocus: 'first' | 'last' | null
|
|
25
|
+
rootElementRef: React.MutableRefObject<HTMLDivElement | null>
|
|
27
26
|
}): MenuController {
|
|
28
|
-
const {onKeyDown, originElement, shouldFocus} = props
|
|
27
|
+
const {onKeyDown, originElement, shouldFocus, rootElementRef} = props
|
|
29
28
|
const elementsRef = useRef<HTMLElement[]>([])
|
|
30
|
-
const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
|
|
31
29
|
const [activeIndex, _setActiveIndex] = useState(-1)
|
|
32
30
|
const activeIndexRef = useRef(activeIndex)
|
|
33
|
-
const activeElement = elementsRef.current[activeIndex] || null
|
|
34
|
-
const mounted = Boolean(
|
|
31
|
+
const activeElement = useMemo(() => elementsRef.current[activeIndex] || null, [activeIndex])
|
|
32
|
+
const mounted = Boolean(rootElementRef.current)
|
|
35
33
|
|
|
36
34
|
const setActiveIndex = useCallback((nextActiveIndex: number) => {
|
|
37
35
|
_setActiveIndex(nextActiveIndex)
|
|
@@ -44,7 +42,7 @@ export function useMenuController(props: {
|
|
|
44
42
|
|
|
45
43
|
if (elementsRef.current.indexOf(element) === -1) {
|
|
46
44
|
elementsRef.current.push(element)
|
|
47
|
-
_sortElements(
|
|
45
|
+
_sortElements(rootElementRef.current, elementsRef.current)
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
if (selected) {
|
|
@@ -61,7 +59,7 @@ export function useMenuController(props: {
|
|
|
61
59
|
}
|
|
62
60
|
}
|
|
63
61
|
},
|
|
64
|
-
[
|
|
62
|
+
[rootElementRef, setActiveIndex],
|
|
65
63
|
)
|
|
66
64
|
|
|
67
65
|
const handleKeyDown = useCallback(
|
|
@@ -179,17 +177,15 @@ export function useMenuController(props: {
|
|
|
179
177
|
// which would be incorrect when the user hovers over a gap
|
|
180
178
|
// between two menu items or a menu divider.
|
|
181
179
|
setActiveIndex(-2)
|
|
182
|
-
|
|
183
|
-
}, [
|
|
180
|
+
rootElementRef.current?.focus()
|
|
181
|
+
}, [rootElementRef, setActiveIndex])
|
|
184
182
|
|
|
185
183
|
// Set focus on the currently active element
|
|
186
184
|
useEffect(() => {
|
|
187
185
|
if (!mounted) return
|
|
188
186
|
|
|
189
187
|
const rafId = window.requestAnimationFrame(() => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (_activeIndex === -1) {
|
|
188
|
+
if (activeIndex === -1) {
|
|
193
189
|
if (shouldFocus === 'first') {
|
|
194
190
|
const focusableElements = _getFocusableElements(elementsRef.current)
|
|
195
191
|
const el = focusableElements[0]
|
|
@@ -217,7 +213,7 @@ export function useMenuController(props: {
|
|
|
217
213
|
return
|
|
218
214
|
}
|
|
219
215
|
|
|
220
|
-
const element = elementsRef.current[
|
|
216
|
+
const element = elementsRef.current[activeIndex] || null
|
|
221
217
|
|
|
222
218
|
element?.focus()
|
|
223
219
|
})
|
|
@@ -234,7 +230,5 @@ export function useMenuController(props: {
|
|
|
234
230
|
handleItemMouseLeave,
|
|
235
231
|
handleKeyDown,
|
|
236
232
|
mount,
|
|
237
|
-
rootElement,
|
|
238
|
-
setRootElement,
|
|
239
233
|
}
|
|
240
234
|
}
|
|
@@ -14,11 +14,12 @@ const loadingAnimation = keyframes`
|
|
|
14
14
|
}
|
|
15
15
|
100% {
|
|
16
16
|
width: 100%;
|
|
17
|
-
}
|
|
17
|
+
}
|
|
18
18
|
`
|
|
19
19
|
|
|
20
20
|
const LOADING_BAR_HEIGHT = 2
|
|
21
21
|
|
|
22
|
+
// @TODO get rid of $duration modifier, set data attribute instead and use stable selector
|
|
22
23
|
export function rootStyles(
|
|
23
24
|
props: {$duration?: number; tone: ThemeColorStateToneKey} & ThemeProps,
|
|
24
25
|
): ReturnType<typeof css> {
|
|
@@ -13,6 +13,7 @@ export function useToast(): ToastContextValue {
|
|
|
13
13
|
throw new Error('useToast(): missing context value')
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// @TODO context and hooks doesn't really work like this, there will never be a mismatch between the provider and the consumer, we can remove these version specifiers
|
|
16
17
|
// NOTE: This check is for future-compatiblity
|
|
17
18
|
// - If the value is not an object, it’s not compatible with the current version
|
|
18
19
|
// - If the value is an object, but doesn’t have `version: 0.0`, it’s not compatible with the current version
|
|
@@ -76,6 +76,7 @@ export const TreeItem = memo(function TreeItem(
|
|
|
76
76
|
const focused = tree.focusedElement === rootRef.current
|
|
77
77
|
const expanded = itemState?.expanded === undefined ? expandedProp : itemState?.expanded || false
|
|
78
78
|
const tabIndex = tree.focusedElement && tree.focusedElement === rootRef.current ? 0 : -1
|
|
79
|
+
// @TODO split out into separate contexts
|
|
79
80
|
const contextValue = useMemo(
|
|
80
81
|
() => ({...tree, level: tree.level + 1, path: itemPath}),
|
|
81
82
|
[itemPath, tree],
|
package/src/core/hooks/index.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export * from './useArrayProp'
|
|
2
2
|
export * from './useClickOutside'
|
|
3
|
+
export * from './useCustomValidity'
|
|
3
4
|
export * from './useElementRect'
|
|
4
5
|
export * from './useElementSize'
|
|
6
|
+
export * from './useForwardedRef'
|
|
5
7
|
export * from './useGlobalKeyDown'
|
|
8
|
+
export * from './useMatchMedia'
|
|
6
9
|
export * from './useMediaIndex'
|
|
7
10
|
export * from './usePrefersDark'
|
|
8
11
|
export * from './usePrefersReducedMotion'
|
|
9
|
-
export * from './useForwardedRef'
|
|
10
|
-
export * from './useCustomValidity'
|
|
@@ -1,99 +1,65 @@
|
|
|
1
|
-
import {useEffect
|
|
1
|
+
import {useEffect} from 'react'
|
|
2
|
+
import {useEffectEvent} from 'use-effect-event'
|
|
2
3
|
import {EMPTY_ARRAY} from '../constants'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @public
|
|
6
7
|
*/
|
|
7
|
-
export type ClickOutsideListener = (event: MouseEvent) => void
|
|
8
|
-
|
|
9
|
-
function _getElements(
|
|
10
|
-
element: HTMLElement | null,
|
|
11
|
-
elementsArg: Array<HTMLElement | HTMLElement[] | null>,
|
|
12
|
-
): HTMLElement[] {
|
|
13
|
-
const ret = [element]
|
|
14
|
-
|
|
15
|
-
for (const el of elementsArg) {
|
|
16
|
-
if (Array.isArray(el)) {
|
|
17
|
-
ret.push(...el)
|
|
18
|
-
} else {
|
|
19
|
-
ret.push(el)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return ret.filter(Boolean) as HTMLElement[]
|
|
24
|
-
}
|
|
8
|
+
export type ClickOutsideListener = (event: MouseEvent | TouchEvent) => void
|
|
25
9
|
|
|
26
10
|
/**
|
|
11
|
+
* Use the callback version of `elementsArg` if you're using `useRef` to handle elements:
|
|
12
|
+
* ```tsx
|
|
13
|
+
* useClickOutside(
|
|
14
|
+
* () => {},
|
|
15
|
+
* () => [ref.current],
|
|
16
|
+
* )
|
|
17
|
+
*
|
|
27
18
|
* @public
|
|
28
19
|
*/
|
|
29
20
|
export function useClickOutside(
|
|
30
21
|
listener: ClickOutsideListener,
|
|
31
|
-
elementsArg:
|
|
22
|
+
elementsArg:
|
|
23
|
+
| Array<HTMLElement | HTMLElement[] | null>
|
|
24
|
+
| (() => Array<HTMLElement | HTMLElement[] | null>) = EMPTY_ARRAY,
|
|
32
25
|
boundaryElement?: HTMLElement | null,
|
|
33
|
-
):
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (prevElements.length !== nextElements.length) {
|
|
43
|
-
setElements(nextElements)
|
|
44
|
-
elementsRef.current = nextElements
|
|
45
|
-
|
|
26
|
+
): void {
|
|
27
|
+
/**
|
|
28
|
+
* The `useEffectEvent` hook allow us to always see the latest value of `listener`, `elementsArg` and `boundaryElement` without needing to
|
|
29
|
+
* juggle `useState`, `useRef` and `useState` to make sure the `mousedown` event listener isn't constantly being added and removed.
|
|
30
|
+
*/
|
|
31
|
+
const eventHandler = useEffectEvent((evt: MouseEvent | TouchEvent) => {
|
|
32
|
+
const target = evt.target
|
|
33
|
+
|
|
34
|
+
if (!(target instanceof Node)) {
|
|
46
35
|
return
|
|
47
36
|
}
|
|
48
37
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
setElements(nextElements)
|
|
52
|
-
elementsRef.current = nextElements
|
|
53
|
-
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
for (const el of nextElements) {
|
|
59
|
-
if (!prevElements.includes(el)) {
|
|
60
|
-
setElements(nextElements)
|
|
61
|
-
elementsRef.current = nextElements
|
|
62
|
-
|
|
63
|
-
return
|
|
64
|
-
}
|
|
38
|
+
if (boundaryElement && !boundaryElement.contains(target)) {
|
|
39
|
+
return
|
|
65
40
|
}
|
|
66
|
-
}, [element, elementsArg])
|
|
67
41
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const handleWindowMouseDown = (evt: MouseEvent) => {
|
|
72
|
-
const target = evt.target
|
|
42
|
+
const resolvedElements = Array.isArray(elementsArg) ? elementsArg : elementsArg()
|
|
43
|
+
const elements = resolvedElements.flat()
|
|
73
44
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
45
|
+
for (const el of elements) {
|
|
46
|
+
if (!el) continue
|
|
77
47
|
|
|
78
|
-
if (
|
|
48
|
+
if (target === el || el.contains(target)) {
|
|
79
49
|
return
|
|
80
50
|
}
|
|
81
|
-
|
|
82
|
-
for (const el of elements) {
|
|
83
|
-
if (target === el || el.contains(target)) {
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
listener(evt)
|
|
89
51
|
}
|
|
90
52
|
|
|
91
|
-
|
|
53
|
+
listener(evt)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
document.addEventListener('mousedown', eventHandler)
|
|
58
|
+
document.addEventListener('touchstart', eventHandler)
|
|
92
59
|
|
|
93
60
|
return () => {
|
|
94
|
-
|
|
61
|
+
document.removeEventListener('mousedown', eventHandler)
|
|
62
|
+
document.removeEventListener('touchstart', eventHandler)
|
|
95
63
|
}
|
|
96
|
-
}, [
|
|
97
|
-
|
|
98
|
-
return setElement
|
|
64
|
+
}, [eventHandler])
|
|
99
65
|
}
|
|
@@ -6,6 +6,7 @@ import {ElementSize, _elementSizeObserver} from '../observers/elementSizeObserve
|
|
|
6
6
|
* @beta
|
|
7
7
|
*/
|
|
8
8
|
export function useElementSize(element: HTMLElement | null): ElementSize | null {
|
|
9
|
+
// @TODO we can probably use something in framer-motion or @floating-ui instead of rolling our own
|
|
9
10
|
const [size, setSize] = useState<ElementSize | null>(null)
|
|
10
11
|
|
|
11
12
|
useEffect(() => {
|
|
@@ -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
|
}
|