@sanity/ui 2.7.1-canary.0 → 2.7.1-canary.2
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 +67 -3
- package/dist/index.d.ts +67 -3
- package/dist/index.esm.js +29 -61
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +28 -60
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +29 -61
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/core/components/dialog/dialog.tsx +9 -12
- package/src/core/components/menu/menu.tsx +1 -7
- package/src/core/hooks/useClickOutside.test.tsx +592 -0
- package/src/core/hooks/useClickOutside.ts +112 -63
- package/src/core/hooks/useMatchMedia.ts +3 -9
- package/src/core/primitives/popover/__workshop__/AlignedStory.tsx +9 -7
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {useEffect,
|
|
1
|
+
import {useEffect, useState} from 'react'
|
|
2
|
+
import {useEffectEvent} from 'use-effect-event'
|
|
2
3
|
import {EMPTY_ARRAY} from '../constants'
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -6,94 +7,142 @@ import {EMPTY_ARRAY} from '../constants'
|
|
|
6
7
|
*/
|
|
7
8
|
export type ClickOutsideListener = (event: MouseEvent) => void
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
}
|
|
10
|
+
/**
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export type ClickOutsideElements = (HTMLElement | null | (HTMLElement | null)[])[]
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
/**
|
|
16
|
+
* @public
|
|
17
|
+
* @deprecated - use your own `const [element, setElement] = useState(null)` logic instead
|
|
18
|
+
*/
|
|
19
|
+
export type UseClickOutsideSetElement = (el: HTMLElement | null) => void
|
|
25
20
|
|
|
26
21
|
/**
|
|
27
22
|
* @public
|
|
28
23
|
*/
|
|
29
24
|
export function useClickOutside(
|
|
30
|
-
listener: ClickOutsideListener,
|
|
31
|
-
elementsArg:
|
|
25
|
+
listener: ClickOutsideListener | false | undefined,
|
|
26
|
+
elementsArg: () => ClickOutsideElements,
|
|
27
|
+
boundaryElement?: () => HTMLElement | null,
|
|
28
|
+
): void
|
|
29
|
+
/**
|
|
30
|
+
* @public
|
|
31
|
+
* @deprecated - change `useClickOutside(handler, () => [...], boundary)` to `useClickOutside(handler, () => [...], () => boundary)`
|
|
32
|
+
*/
|
|
33
|
+
export function useClickOutside(
|
|
34
|
+
listener: ClickOutsideListener | false | undefined,
|
|
35
|
+
elementsArg: () => ClickOutsideElements,
|
|
36
|
+
boundaryElement: HTMLElement | null,
|
|
37
|
+
): void
|
|
38
|
+
/**
|
|
39
|
+
* @public
|
|
40
|
+
* @deprecated - change `useClickOutside(handler, [...], () => boundary)` to `useClickOutside(handler, () => [...], () => boundary)`
|
|
41
|
+
*/
|
|
42
|
+
export function useClickOutside(
|
|
43
|
+
listener: ClickOutsideListener | false | undefined,
|
|
44
|
+
elementsArg: ClickOutsideElements,
|
|
45
|
+
boundaryElement: () => HTMLElement | null,
|
|
46
|
+
): UseClickOutsideSetElement
|
|
47
|
+
/**
|
|
48
|
+
* @public
|
|
49
|
+
* @deprecated
|
|
50
|
+
* Instead of:
|
|
51
|
+
* ```tsx
|
|
52
|
+
* const buttonRef = useRef(null)
|
|
53
|
+
* const setElement = useClickOutside(() => {}, [buttonRef.current])
|
|
54
|
+
* return (
|
|
55
|
+
* <>
|
|
56
|
+
* <button ref={buttonRef} />
|
|
57
|
+
* {open && <div ref={setElement} />}
|
|
58
|
+
* </>
|
|
59
|
+
* )
|
|
60
|
+
* ```
|
|
61
|
+
* Use:
|
|
62
|
+
* ```tsx
|
|
63
|
+
* const buttonRef = useRef()
|
|
64
|
+
* const [element, setElement] = useState(null)
|
|
65
|
+
* useClickOutside(() => {}, () => [buttonRef.current, element])
|
|
66
|
+
* return (
|
|
67
|
+
* <>
|
|
68
|
+
* <button ref={buttonRef} />
|
|
69
|
+
* {open && <div ref={setElement} />}
|
|
70
|
+
* </>
|
|
71
|
+
* )
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export function useClickOutside(
|
|
75
|
+
listener: ClickOutsideListener | false | undefined,
|
|
76
|
+
elementsArg: ClickOutsideElements,
|
|
32
77
|
boundaryElement?: HTMLElement | null,
|
|
33
|
-
):
|
|
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
|
|
78
|
+
): UseClickOutsideSetElement
|
|
45
79
|
|
|
80
|
+
/**
|
|
81
|
+
* @public
|
|
82
|
+
*/
|
|
83
|
+
export function useClickOutside(
|
|
84
|
+
listener: ClickOutsideListener | false | undefined,
|
|
85
|
+
elementsArg: ClickOutsideElements | (() => ClickOutsideElements) = EMPTY_ARRAY,
|
|
86
|
+
boundaryElement?: HTMLElement | null | (() => HTMLElement | null),
|
|
87
|
+
): UseClickOutsideSetElement | void {
|
|
88
|
+
/** @deprecated */
|
|
89
|
+
const [legacyElement, setLegacyElement] = useState<HTMLElement | null>(null)
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The `useEffectEvent` hook allow us to always see the latest value of `listener`, `elementsArg` and `boundaryElement` without needing to
|
|
93
|
+
* juggle `useState`, `useRef` and `useState` to make sure the `mousedown` event listener isn't constantly being added and removed.
|
|
94
|
+
*/
|
|
95
|
+
const onEvent = useEffectEvent((evt: MouseEvent) => {
|
|
96
|
+
if (!listener) {
|
|
46
97
|
return
|
|
47
98
|
}
|
|
48
99
|
|
|
49
|
-
|
|
50
|
-
if (!nextElements.includes(el)) {
|
|
51
|
-
setElements(nextElements)
|
|
52
|
-
elementsRef.current = nextElements
|
|
100
|
+
const target = evt.target
|
|
53
101
|
|
|
54
|
-
|
|
55
|
-
|
|
102
|
+
if (!(target instanceof Node)) {
|
|
103
|
+
return
|
|
56
104
|
}
|
|
57
105
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return
|
|
64
|
-
}
|
|
106
|
+
const resolvedBoundaryElement =
|
|
107
|
+
typeof boundaryElement === 'function' ? boundaryElement() : boundaryElement
|
|
108
|
+
if (resolvedBoundaryElement && !resolvedBoundaryElement.contains(target)) {
|
|
109
|
+
return
|
|
65
110
|
}
|
|
66
|
-
}, [element, elementsArg])
|
|
67
111
|
|
|
68
|
-
|
|
69
|
-
|
|
112
|
+
const resolvedElements = Array.isArray(elementsArg)
|
|
113
|
+
? [legacyElement, ...elementsArg]
|
|
114
|
+
: elementsArg()
|
|
115
|
+
const elements = resolvedElements.flat()
|
|
70
116
|
|
|
71
|
-
const
|
|
72
|
-
|
|
117
|
+
for (const el of elements) {
|
|
118
|
+
if (!el) continue
|
|
73
119
|
|
|
74
|
-
if (
|
|
120
|
+
if (target === el || el.contains(target)) {
|
|
75
121
|
return
|
|
76
122
|
}
|
|
123
|
+
}
|
|
77
124
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
125
|
+
listener(evt)
|
|
126
|
+
})
|
|
81
127
|
|
|
82
|
-
|
|
83
|
-
if (target === el || el.contains(target)) {
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
}
|
|
128
|
+
const hasListener = Boolean(listener)
|
|
87
129
|
|
|
88
|
-
|
|
89
|
-
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!hasListener) return undefined
|
|
90
132
|
|
|
91
|
-
|
|
133
|
+
const handleEvent = (evt: MouseEvent) => onEvent(evt)
|
|
134
|
+
|
|
135
|
+
document.addEventListener('mousedown', handleEvent)
|
|
92
136
|
|
|
93
137
|
return () => {
|
|
94
|
-
|
|
138
|
+
document.removeEventListener('mousedown', handleEvent)
|
|
95
139
|
}
|
|
96
|
-
}, [
|
|
140
|
+
}, [hasListener, onEvent])
|
|
97
141
|
|
|
98
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Only return the legacy setElement function if the elementsArg is an array
|
|
144
|
+
*/
|
|
145
|
+
if (Array.isArray(elementsArg)) {
|
|
146
|
+
return setLegacyElement as UseClickOutsideSetElement
|
|
147
|
+
}
|
|
99
148
|
}
|
|
@@ -18,8 +18,8 @@ export function useMatchMedia(
|
|
|
18
18
|
*/
|
|
19
19
|
let MEDIA_QUERY_CACHE: MediaQueryList | undefined
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
if (MEDIA_QUERY_CACHE
|
|
21
|
+
const getMatchMedia = (): MediaQueryList => {
|
|
22
|
+
if (!MEDIA_QUERY_CACHE) {
|
|
23
23
|
// As this function is only called during `subscribe` and `getSnapshot`, we can assume that the
|
|
24
24
|
// the `window` global is available and we're in a browser environment
|
|
25
25
|
MEDIA_QUERY_CACHE = window.matchMedia(mediaQueryString)
|
|
@@ -36,13 +36,7 @@ export function useMatchMedia(
|
|
|
36
36
|
|
|
37
37
|
return () => matchMedia.removeEventListener('change', onStoreChange)
|
|
38
38
|
},
|
|
39
|
-
getSnapshot: () =>
|
|
40
|
-
try {
|
|
41
|
-
return getMatchMedia().matches
|
|
42
|
-
} catch (err) {
|
|
43
|
-
throw new Error(`Failed to match media query: ${mediaQueryString}`, {cause: err})
|
|
44
|
-
}
|
|
45
|
-
},
|
|
39
|
+
getSnapshot: () => getMatchMedia().matches,
|
|
46
40
|
}
|
|
47
41
|
}, [mediaQueryString])
|
|
48
42
|
|
|
@@ -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, useState} from 'react'
|
|
4
|
+
import {useCallback, useRef, 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 buttonElementRef = useRef<HTMLButtonElement | null>(null)
|
|
24
|
+
const popoverElementRef = useRef<HTMLDivElement | null>(null)
|
|
25
25
|
|
|
26
26
|
const content = (
|
|
27
27
|
<Text>
|
|
@@ -39,9 +39,11 @@ export default function AlignedStory() {
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
const handleToggleOpen = useCallback(() => setOpen((v) => !v), [])
|
|
42
|
-
const handleClose = useCallback(() => setOpen(false), [])
|
|
43
42
|
|
|
44
|
-
useClickOutside(
|
|
43
|
+
useClickOutside(
|
|
44
|
+
() => setOpen(false),
|
|
45
|
+
() => [buttonElementRef.current, popoverElementRef.current],
|
|
46
|
+
)
|
|
45
47
|
|
|
46
48
|
return (
|
|
47
49
|
<Card height="fill" padding={[4, 5, 6]} sizing="border" tone="transparent">
|
|
@@ -56,14 +58,14 @@ export default function AlignedStory() {
|
|
|
56
58
|
padding={3}
|
|
57
59
|
portal={portal}
|
|
58
60
|
placement={placement}
|
|
59
|
-
ref={
|
|
61
|
+
ref={popoverElementRef}
|
|
60
62
|
width={width}
|
|
61
63
|
>
|
|
62
64
|
<Button
|
|
63
65
|
icon={EllipsisVerticalIcon}
|
|
64
66
|
mode="bleed"
|
|
65
67
|
onClick={handleToggleOpen}
|
|
66
|
-
ref={
|
|
68
|
+
ref={buttonElementRef}
|
|
67
69
|
selected={open}
|
|
68
70
|
/>
|
|
69
71
|
</Popover>
|