@sanity/ui 2.6.4-canary.2 → 2.6.4
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 +156 -90
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +154 -88
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +156 -90
- 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 +21 -25
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/ui",
|
|
3
|
-
"version": "2.6.4
|
|
3
|
+
"version": "2.6.4",
|
|
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
|
+
"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
|
+
},
|
|
54
80
|
"commitlint": {
|
|
55
81
|
"extends": [
|
|
56
82
|
"@commitlint/config-conventional"
|
|
@@ -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
|
+
}
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Children,
|
|
3
|
-
forwardRef,
|
|
4
|
-
Fragment,
|
|
5
|
-
isValidElement,
|
|
6
|
-
useCallback,
|
|
7
|
-
useMemo,
|
|
8
|
-
useRef,
|
|
9
|
-
useState,
|
|
10
|
-
} from 'react'
|
|
1
|
+
import {Children, forwardRef, Fragment, isValidElement, useCallback, useMemo, useState} from 'react'
|
|
11
2
|
import {useArrayProp, useClickOutside} from '../../hooks'
|
|
12
3
|
import {Box, Popover, Stack, Text} from '../../primitives'
|
|
13
4
|
import {ExpandButton, Root} from './breadcrumbs.styles'
|
|
@@ -31,13 +22,13 @@ export const Breadcrumbs = forwardRef(function Breadcrumbs(
|
|
|
31
22
|
const {children, maxLength, separator, space: spaceRaw = 2, ...restProps} = props
|
|
32
23
|
const space = useArrayProp(spaceRaw)
|
|
33
24
|
const [open, setOpen] = useState(false)
|
|
34
|
-
const
|
|
35
|
-
const
|
|
25
|
+
const [expandElement, setExpandElement] = useState<HTMLButtonElement | null>(null)
|
|
26
|
+
const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
|
|
36
27
|
|
|
37
28
|
const collapse = useCallback(() => setOpen(false), [])
|
|
38
29
|
const expand = useCallback(() => setOpen(true), [])
|
|
39
30
|
|
|
40
|
-
useClickOutside(collapse,
|
|
31
|
+
useClickOutside(collapse, [expandElement, popoverElement])
|
|
41
32
|
|
|
42
33
|
const rawItems = useMemo(() => Children.toArray(children).filter(isValidElement), [children])
|
|
43
34
|
|
|
@@ -61,14 +52,14 @@ export const Breadcrumbs = forwardRef(function Breadcrumbs(
|
|
|
61
52
|
open={open}
|
|
62
53
|
placement="top"
|
|
63
54
|
portal
|
|
64
|
-
ref={
|
|
55
|
+
ref={setPopoverElement}
|
|
65
56
|
>
|
|
66
57
|
<ExpandButton
|
|
67
58
|
fontSize={1}
|
|
68
59
|
mode="bleed"
|
|
69
60
|
onClick={open ? collapse : expand}
|
|
70
61
|
padding={1}
|
|
71
|
-
ref={
|
|
62
|
+
ref={setExpandElement}
|
|
72
63
|
selected={open}
|
|
73
64
|
text="…"
|
|
74
65
|
/>
|
|
@@ -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} from 'react'
|
|
3
|
+
import {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'
|
|
4
4
|
import {styled} from 'styled-components'
|
|
5
5
|
import {
|
|
6
6
|
containsOrEqualsElement,
|
|
@@ -170,6 +170,7 @@ 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)
|
|
173
174
|
const contentRef = useRef<HTMLDivElement | null>(null)
|
|
174
175
|
const layer = useLayer()
|
|
175
176
|
const {isTopLayer} = layer
|
|
@@ -215,24 +216,32 @@ const DialogCard = forwardRef(function DialogCard(
|
|
|
215
216
|
)
|
|
216
217
|
|
|
217
218
|
useClickOutside(
|
|
218
|
-
(
|
|
219
|
-
|
|
219
|
+
useCallback(
|
|
220
|
+
(event: MouseEvent) => {
|
|
221
|
+
if (!isTopLayer || !onClickOutside) return
|
|
220
222
|
|
|
221
|
-
|
|
223
|
+
const target = event.target as Node | null
|
|
222
224
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
if (target && !isTargetWithinScope(boundaryElement, portalElement, target)) {
|
|
226
|
+
// Ignore clicks outside of the scope
|
|
227
|
+
return
|
|
228
|
+
}
|
|
227
229
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
230
|
+
onClickOutside()
|
|
231
|
+
},
|
|
232
|
+
[boundaryElement, isTopLayer, onClickOutside, portalElement],
|
|
233
|
+
),
|
|
234
|
+
[rootElement],
|
|
231
235
|
)
|
|
232
236
|
|
|
237
|
+
const setRef = useCallback((el: HTMLDivElement | null) => {
|
|
238
|
+
setRootElement(el)
|
|
239
|
+
ref.current = el
|
|
240
|
+
}, [])
|
|
241
|
+
|
|
233
242
|
return (
|
|
234
243
|
<DialogContainer data-ui="DialogCard" width={width}>
|
|
235
|
-
<DialogCardRoot radius={radius} ref={
|
|
244
|
+
<DialogCardRoot radius={radius} ref={setRef} scheme={scheme} shadow={shadow}>
|
|
236
245
|
<DialogLayout direction="column">
|
|
237
246
|
{showHeader && (
|
|
238
247
|
<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
|
|
22
|
+
onClickOutside?: (event: MouseEvent) => void
|
|
23
23
|
onEscape?: () => void
|
|
24
24
|
onItemClick?: () => void
|
|
25
25
|
onItemSelect?: (index: number) => void
|
|
@@ -75,18 +75,16 @@ export const Menu = forwardRef(function Menu(
|
|
|
75
75
|
handleItemMouseLeave,
|
|
76
76
|
handleKeyDown,
|
|
77
77
|
mount,
|
|
78
|
-
|
|
78
|
+
rootElement,
|
|
79
|
+
setRootElement,
|
|
80
|
+
} = useMenuController({onKeyDown, originElement, shouldFocus})
|
|
79
81
|
|
|
80
82
|
const handleRefChange = useCallback(
|
|
81
83
|
(el: HTMLDivElement | null) => {
|
|
84
|
+
setRootElement(el)
|
|
82
85
|
ref.current = el
|
|
83
|
-
|
|
84
|
-
// Register root element (for nested menus)
|
|
85
|
-
if (ref.current && registerElement) {
|
|
86
|
-
registerElement(ref.current)
|
|
87
|
-
}
|
|
88
86
|
},
|
|
89
|
-
[
|
|
87
|
+
[setRootElement],
|
|
90
88
|
)
|
|
91
89
|
|
|
92
90
|
// Trigger `onItemSelect` when active index changes
|
|
@@ -96,8 +94,11 @@ export const Menu = forwardRef(function Menu(
|
|
|
96
94
|
|
|
97
95
|
// Close menu when clicking outside
|
|
98
96
|
useClickOutside(
|
|
99
|
-
(
|
|
100
|
-
|
|
97
|
+
useCallback(
|
|
98
|
+
(event) => isTopLayer && onClickOutside && onClickOutside(event),
|
|
99
|
+
[isTopLayer, onClickOutside],
|
|
100
|
+
),
|
|
101
|
+
[rootElement],
|
|
101
102
|
)
|
|
102
103
|
|
|
103
104
|
// Close menu when pressing Escape
|
|
@@ -115,7 +116,13 @@ export const Menu = forwardRef(function Menu(
|
|
|
115
116
|
),
|
|
116
117
|
)
|
|
117
118
|
|
|
118
|
-
//
|
|
119
|
+
// Register root element (for nested menus)
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!rootElement || !registerElement) return
|
|
122
|
+
|
|
123
|
+
return registerElement(rootElement)
|
|
124
|
+
}, [registerElement, rootElement])
|
|
125
|
+
|
|
119
126
|
const value: MenuContextValue = useMemo(
|
|
120
127
|
() => ({
|
|
121
128
|
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) => {
|
|
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
|
|
8
|
+
onClickOutside?: (event: MouseEvent) => 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,
|
|
1
|
+
import {useCallback, useEffect, useRef, useState} from 'react'
|
|
2
2
|
import {_getFocusableElements, _sortElements} from './helpers'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -11,6 +11,8 @@ 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
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -22,14 +24,14 @@ export function useMenuController(props: {
|
|
|
22
24
|
onKeyDown?: React.KeyboardEventHandler
|
|
23
25
|
originElement?: HTMLElement | null
|
|
24
26
|
shouldFocus: 'first' | 'last' | null
|
|
25
|
-
rootElementRef: React.MutableRefObject<HTMLDivElement | null>
|
|
26
27
|
}): MenuController {
|
|
27
|
-
const {onKeyDown, originElement, shouldFocus
|
|
28
|
+
const {onKeyDown, originElement, shouldFocus} = props
|
|
28
29
|
const elementsRef = useRef<HTMLElement[]>([])
|
|
30
|
+
const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null)
|
|
29
31
|
const [activeIndex, _setActiveIndex] = useState(-1)
|
|
30
32
|
const activeIndexRef = useRef(activeIndex)
|
|
31
|
-
const activeElement =
|
|
32
|
-
const mounted = Boolean(
|
|
33
|
+
const activeElement = elementsRef.current[activeIndex] || null
|
|
34
|
+
const mounted = Boolean(rootElement)
|
|
33
35
|
|
|
34
36
|
const setActiveIndex = useCallback((nextActiveIndex: number) => {
|
|
35
37
|
_setActiveIndex(nextActiveIndex)
|
|
@@ -42,7 +44,7 @@ export function useMenuController(props: {
|
|
|
42
44
|
|
|
43
45
|
if (elementsRef.current.indexOf(element) === -1) {
|
|
44
46
|
elementsRef.current.push(element)
|
|
45
|
-
_sortElements(
|
|
47
|
+
_sortElements(rootElement, elementsRef.current)
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
if (selected) {
|
|
@@ -59,7 +61,7 @@ export function useMenuController(props: {
|
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
},
|
|
62
|
-
[
|
|
64
|
+
[rootElement, setActiveIndex],
|
|
63
65
|
)
|
|
64
66
|
|
|
65
67
|
const handleKeyDown = useCallback(
|
|
@@ -177,15 +179,17 @@ export function useMenuController(props: {
|
|
|
177
179
|
// which would be incorrect when the user hovers over a gap
|
|
178
180
|
// between two menu items or a menu divider.
|
|
179
181
|
setActiveIndex(-2)
|
|
180
|
-
|
|
181
|
-
}, [
|
|
182
|
+
rootElement?.focus()
|
|
183
|
+
}, [setActiveIndex, rootElement])
|
|
182
184
|
|
|
183
185
|
// Set focus on the currently active element
|
|
184
186
|
useEffect(() => {
|
|
185
187
|
if (!mounted) return
|
|
186
188
|
|
|
187
189
|
const rafId = window.requestAnimationFrame(() => {
|
|
188
|
-
|
|
190
|
+
const _activeIndex = activeIndexRef.current
|
|
191
|
+
|
|
192
|
+
if (_activeIndex === -1) {
|
|
189
193
|
if (shouldFocus === 'first') {
|
|
190
194
|
const focusableElements = _getFocusableElements(elementsRef.current)
|
|
191
195
|
const el = focusableElements[0]
|
|
@@ -213,7 +217,7 @@ export function useMenuController(props: {
|
|
|
213
217
|
return
|
|
214
218
|
}
|
|
215
219
|
|
|
216
|
-
const element = elementsRef.current[
|
|
220
|
+
const element = elementsRef.current[_activeIndex] || null
|
|
217
221
|
|
|
218
222
|
element?.focus()
|
|
219
223
|
})
|
|
@@ -230,5 +234,7 @@ export function useMenuController(props: {
|
|
|
230
234
|
handleItemMouseLeave,
|
|
231
235
|
handleKeyDown,
|
|
232
236
|
mount,
|
|
237
|
+
rootElement,
|
|
238
|
+
setRootElement,
|
|
233
239
|
}
|
|
234
240
|
}
|
|
@@ -14,12 +14,11 @@ 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
|
|
23
22
|
export function rootStyles(
|
|
24
23
|
props: {$duration?: number; tone: ThemeColorStateToneKey} & ThemeProps,
|
|
25
24
|
): ReturnType<typeof css> {
|
|
@@ -13,7 +13,6 @@ 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
|
|
17
16
|
// NOTE: This check is for future-compatiblity
|
|
18
17
|
// - If the value is not an object, it’s not compatible with the current version
|
|
19
18
|
// - If the value is an object, but doesn’t have `version: 0.0`, it’s not compatible with the current version
|
|
@@ -76,7 +76,6 @@ 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
|
|
80
79
|
const contextValue = useMemo(
|
|
81
80
|
() => ({...tree, level: tree.level + 1, path: itemPath}),
|
|
82
81
|
[itemPath, tree],
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './useUnique'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {useRef} from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This is a React hook to make sure that a record identity is the same on every render. Uses strict
|
|
5
|
+
* equality comparison (eg by identity), and only goes one level deep.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
type Comparable = Record<string | number | symbol, unknown> | undefined | null
|
|
10
|
+
|
|
11
|
+
export function useUnique<ValueType extends Comparable = Comparable>(value: ValueType): ValueType {
|
|
12
|
+
const valueRef = useRef<ValueType>(value)
|
|
13
|
+
|
|
14
|
+
if (!_isEqual(valueRef.current, value)) {
|
|
15
|
+
valueRef.current = value
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return valueRef.current
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function _isEqual(objA: Comparable, objB: Comparable): boolean {
|
|
22
|
+
if (!objA || !objB) {
|
|
23
|
+
return objA === objB
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const keysA = Object.keys(objA)
|
|
27
|
+
const keysB = Object.keys(objB)
|
|
28
|
+
|
|
29
|
+
if (keysA.length !== keysB.length) {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return keysA.every((key) => objA[key] === objB[key])
|
|
34
|
+
}
|
package/src/core/hooks/index.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
export * from './useArrayProp'
|
|
2
2
|
export * from './useClickOutside'
|
|
3
|
-
export * from './useCustomValidity'
|
|
4
3
|
export * from './useElementRect'
|
|
5
4
|
export * from './useElementSize'
|
|
6
|
-
export * from './useForwardedRef'
|
|
7
5
|
export * from './useGlobalKeyDown'
|
|
8
|
-
export * from './useMatchMedia'
|
|
9
6
|
export * from './useMediaIndex'
|
|
10
7
|
export * from './usePrefersDark'
|
|
11
8
|
export * from './usePrefersReducedMotion'
|
|
9
|
+
export * from './useForwardedRef'
|
|
10
|
+
export * from './useCustomValidity'
|
|
@@ -1,65 +1,99 @@
|
|
|
1
|
-
import {useEffect} from 'react'
|
|
2
|
-
import {useEffectEvent} from 'use-effect-event'
|
|
1
|
+
import {useEffect, useRef, useState} from 'react'
|
|
3
2
|
import {EMPTY_ARRAY} from '../constants'
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* @public
|
|
7
6
|
*/
|
|
8
|
-
export type ClickOutsideListener = (event: MouseEvent
|
|
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
|
+
}
|
|
9
25
|
|
|
10
26
|
/**
|
|
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
|
-
*
|
|
18
27
|
* @public
|
|
19
28
|
*/
|
|
20
29
|
export function useClickOutside(
|
|
21
30
|
listener: ClickOutsideListener,
|
|
22
|
-
elementsArg:
|
|
23
|
-
| Array<HTMLElement | HTMLElement[] | null>
|
|
24
|
-
| (() => Array<HTMLElement | HTMLElement[] | null>) = EMPTY_ARRAY,
|
|
31
|
+
elementsArg: Array<HTMLElement | HTMLElement[] | null> = EMPTY_ARRAY,
|
|
25
32
|
boundaryElement?: HTMLElement | null,
|
|
26
|
-
): void {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
): (el: HTMLElement | null) => void {
|
|
34
|
+
const [element, setElement] = useState<HTMLElement | null>(null)
|
|
35
|
+
const [elements, setElements] = useState(() => _getElements(element, elementsArg))
|
|
36
|
+
const elementsRef = useRef(elements)
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const prevElements = elementsRef.current
|
|
40
|
+
const nextElements = _getElements(element, elementsArg)
|
|
41
|
+
|
|
42
|
+
if (prevElements.length !== nextElements.length) {
|
|
43
|
+
setElements(nextElements)
|
|
44
|
+
elementsRef.current = nextElements
|
|
37
45
|
|
|
38
|
-
if (boundaryElement && !boundaryElement.contains(target)) {
|
|
39
46
|
return
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!el) continue
|
|
49
|
+
for (const el of prevElements) {
|
|
50
|
+
if (!nextElements.includes(el)) {
|
|
51
|
+
setElements(nextElements)
|
|
52
|
+
elementsRef.current = nextElements
|
|
47
53
|
|
|
48
|
-
if (target === el || el.contains(target)) {
|
|
49
54
|
return
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
for (const el of nextElements) {
|
|
59
|
+
if (!prevElements.includes(el)) {
|
|
60
|
+
setElements(nextElements)
|
|
61
|
+
elementsRef.current = nextElements
|
|
62
|
+
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}, [element, elementsArg])
|
|
55
67
|
|
|
56
68
|
useEffect(() => {
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
if (!listener) return undefined
|
|
70
|
+
|
|
71
|
+
const handleWindowMouseDown = (evt: MouseEvent) => {
|
|
72
|
+
const target = evt.target
|
|
73
|
+
|
|
74
|
+
if (!(target instanceof Node)) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (boundaryElement && !boundaryElement.contains(target)) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const el of elements) {
|
|
83
|
+
if (target === el || el.contains(target)) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
listener(evt)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
window.addEventListener('mousedown', handleWindowMouseDown)
|
|
59
92
|
|
|
60
93
|
return () => {
|
|
61
|
-
|
|
62
|
-
document.removeEventListener('touchstart', eventHandler)
|
|
94
|
+
window.removeEventListener('mousedown', handleWindowMouseDown)
|
|
63
95
|
}
|
|
64
|
-
}, [
|
|
96
|
+
}, [boundaryElement, listener, elements])
|
|
97
|
+
|
|
98
|
+
return setElement
|
|
65
99
|
}
|
|
@@ -6,7 +6,6 @@ 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
|
|
10
9
|
const [size, setSize] = useState<ElementSize | null>(null)
|
|
11
10
|
|
|
12
11
|
useEffect(() => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {useSyncExternalStore} from 'react'
|
|
2
2
|
import {useTheme_v2} from '../../theme'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -9,6 +9,8 @@ export interface _MediaStore {
|
|
|
9
9
|
getSnapshot: () => number
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
const MEDIA_STORE_CACHE = new WeakMap<number[], _MediaStore>()
|
|
13
|
+
|
|
12
14
|
type MediaQueryMinWidth = `(min-width: ${number}px)`
|
|
13
15
|
type MediaQueryMaxWidth = `(max-width: ${number}px)`
|
|
14
16
|
type MediaQueryMinMaxWidth = `${MediaQueryMinWidth} and ${MediaQueryMaxWidth}`
|
|
@@ -95,7 +97,13 @@ function getServerSnapshot() {
|
|
|
95
97
|
*/
|
|
96
98
|
export function useMediaIndex(): number {
|
|
97
99
|
const {media} = useTheme_v2()
|
|
98
|
-
|
|
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
|
+
}
|
|
99
107
|
|
|
100
108
|
return useSyncExternalStore(store.subscribe, store.getSnapshot, getServerSnapshot)
|
|
101
109
|
}
|