@sanity/ui 2.7.0 → 2.7.1-canary.1
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 +43 -4
- package/dist/index.d.ts +43 -4
- package/dist/index.esm.js +16 -36
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +15 -35
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +16 -36
- package/dist/index.mjs.map +1 -1
- package/package.json +26 -34
- package/src/core/components/dialog/dialog.tsx +1 -1
- package/src/core/components/menu/menu.tsx +1 -1
- package/src/core/components/menu/menuButton.tsx +1 -1
- package/src/core/components/menu/menuContext.ts +1 -1
- package/src/core/hooks/useClickOutside.test.tsx +70 -0
- package/src/core/hooks/useClickOutside.ts +90 -65
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/ui",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.1-canary.1",
|
|
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
|
-
"prepack": "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
|
+
}
|
|
@@ -217,7 +217,7 @@ const DialogCard = forwardRef(function DialogCard(
|
|
|
217
217
|
|
|
218
218
|
useClickOutside(
|
|
219
219
|
useCallback(
|
|
220
|
-
(event: MouseEvent) => {
|
|
220
|
+
(event: MouseEvent | TouchEvent) => {
|
|
221
221
|
if (!isTopLayer || !onClickOutside) return
|
|
222
222
|
|
|
223
223
|
const target = event.target as Node | null
|
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
|
|
3
|
+
import {renderHook, act} from '@testing-library/react'
|
|
4
|
+
import {useClickOutside} from './useClickOutside'
|
|
5
|
+
|
|
6
|
+
describe('useClickOutside', () => {
|
|
7
|
+
it('should update state immediately if delay is not provided', () => {
|
|
8
|
+
const {result} = renderHook(() => {
|
|
9
|
+
const setElement = useClickOutside(false)
|
|
10
|
+
setElement()
|
|
11
|
+
})
|
|
12
|
+
const [, setState] = result.current
|
|
13
|
+
|
|
14
|
+
act(() => {
|
|
15
|
+
setState(true)
|
|
16
|
+
})
|
|
17
|
+
expect(result.current[0]).toBe(true)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should update state after delay if delay is provided', async () => {
|
|
21
|
+
jest.useFakeTimers()
|
|
22
|
+
const {result} = renderHook(() => useDelayedState(false))
|
|
23
|
+
const [, setState] = result.current
|
|
24
|
+
|
|
25
|
+
act(() => {
|
|
26
|
+
setState(true, 1000)
|
|
27
|
+
})
|
|
28
|
+
expect(result.current[0]).toBe(false)
|
|
29
|
+
act(() => {
|
|
30
|
+
jest.advanceTimersByTime(500)
|
|
31
|
+
})
|
|
32
|
+
expect(result.current[0]).toBe(false)
|
|
33
|
+
|
|
34
|
+
act(() => {
|
|
35
|
+
jest.advanceTimersByTime(500)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
expect(result.current[0]).toBe(true)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should update state with callback function', () => {
|
|
42
|
+
const {result} = renderHook(() => useDelayedState(false))
|
|
43
|
+
const [, setState] = result.current
|
|
44
|
+
|
|
45
|
+
act(() => {
|
|
46
|
+
setState((prev: boolean) => !prev)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
expect(result.current[0]).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should cancel update if the set state was called with a new state', () => {
|
|
53
|
+
const {result} = renderHook(() => useDelayedState(false))
|
|
54
|
+
const [, setState] = result.current
|
|
55
|
+
|
|
56
|
+
act(() => {
|
|
57
|
+
setState(true, 1000)
|
|
58
|
+
})
|
|
59
|
+
expect(result.current[0]).toBe(false)
|
|
60
|
+
|
|
61
|
+
jest.advanceTimersByTime(500)
|
|
62
|
+
|
|
63
|
+
act(() => {
|
|
64
|
+
setState(false)
|
|
65
|
+
})
|
|
66
|
+
jest.advanceTimersByTime(600)
|
|
67
|
+
// Even after 1.1 seconds, the state should continue being false, because it was cancelled by a next setState call
|
|
68
|
+
expect(result.current[0]).toBe(false)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -1,99 +1,124 @@
|
|
|
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
|
/**
|
|
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
|
-
}
|
|
8
|
+
export type ClickOutsideListener = (event: MouseEvent | TouchEvent) => void
|
|
22
9
|
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
/**
|
|
11
|
+
* @public
|
|
12
|
+
* @deprecated -- do not use
|
|
13
|
+
*/
|
|
14
|
+
export type UseClickOutsideSetElement = (el: HTMLElement | null) => void
|
|
25
15
|
|
|
26
16
|
/**
|
|
27
17
|
* @public
|
|
28
18
|
*/
|
|
29
19
|
export function useClickOutside(
|
|
30
20
|
listener: ClickOutsideListener,
|
|
31
|
-
elementsArg:
|
|
21
|
+
elementsArg: () => (HTMLElement | HTMLElement[] | null)[],
|
|
32
22
|
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)
|
|
23
|
+
): void
|
|
41
24
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
25
|
+
/**
|
|
26
|
+
* @public
|
|
27
|
+
* @deprecated
|
|
28
|
+
* Instead of:
|
|
29
|
+
* ```tsx
|
|
30
|
+
* const buttonRef = useRef(null)
|
|
31
|
+
* const setElement = useClickOutside(() => {}, [buttonRef.current])
|
|
32
|
+
* return (
|
|
33
|
+
* <>
|
|
34
|
+
* <button ref={buttonRef} />
|
|
35
|
+
* {open && <div ref={setElement} />}
|
|
36
|
+
* </>
|
|
37
|
+
* )
|
|
38
|
+
* ```
|
|
39
|
+
* Use:
|
|
40
|
+
* ```tsx
|
|
41
|
+
* const buttonRef = useRef()
|
|
42
|
+
* const [element, setElement] = useState(null)
|
|
43
|
+
* useClickOutside(() => {}, () => [buttonRef.current, element])
|
|
44
|
+
* return (
|
|
45
|
+
* <>
|
|
46
|
+
* <button ref={buttonRef} />
|
|
47
|
+
* {open && <div ref={setElement} />}
|
|
48
|
+
* </>
|
|
49
|
+
* )
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function useClickOutside(
|
|
53
|
+
listener: ClickOutsideListener,
|
|
54
|
+
elementsArg: (HTMLElement | HTMLElement[] | null)[],
|
|
55
|
+
boundaryElement?: HTMLElement | null,
|
|
56
|
+
): UseClickOutsideSetElement
|
|
45
57
|
|
|
58
|
+
/**
|
|
59
|
+
* @public
|
|
60
|
+
*/
|
|
61
|
+
export function useClickOutside(
|
|
62
|
+
listener: ClickOutsideListener,
|
|
63
|
+
elementsArg:
|
|
64
|
+
| (HTMLElement | HTMLElement[] | null)[]
|
|
65
|
+
| (() => (HTMLElement | HTMLElement[] | null)[]) = EMPTY_ARRAY,
|
|
66
|
+
boundaryElement?: HTMLElement | null,
|
|
67
|
+
): UseClickOutsideSetElement | void {
|
|
68
|
+
/** @deprecated */
|
|
69
|
+
const [legacyElement, setLegacyElement] = useState<HTMLElement | null>(null)
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The `useEffectEvent` hook allow us to always see the latest value of `listener`, `elementsArg` and `boundaryElement` without needing to
|
|
73
|
+
* juggle `useState`, `useRef` and `useState` to make sure the `mousedown` event listener isn't constantly being added and removed.
|
|
74
|
+
*/
|
|
75
|
+
const onEvent = useEffectEvent((evt: MouseEvent | TouchEvent) => {
|
|
76
|
+
const target = evt.target
|
|
77
|
+
|
|
78
|
+
if (!(target instanceof Node)) {
|
|
46
79
|
return
|
|
47
80
|
}
|
|
48
81
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
setElements(nextElements)
|
|
52
|
-
elementsRef.current = nextElements
|
|
53
|
-
|
|
54
|
-
return
|
|
55
|
-
}
|
|
82
|
+
if (boundaryElement && !boundaryElement.contains(target)) {
|
|
83
|
+
return
|
|
56
84
|
}
|
|
57
85
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
86
|
+
const resolvedElements = Array.isArray(elementsArg)
|
|
87
|
+
? [legacyElement, ...elementsArg]
|
|
88
|
+
: elementsArg()
|
|
89
|
+
const elements = resolvedElements.flat()
|
|
90
|
+
|
|
91
|
+
for (const el of elements) {
|
|
92
|
+
if (!el) continue
|
|
62
93
|
|
|
94
|
+
if (target === el || el.contains(target)) {
|
|
63
95
|
return
|
|
64
96
|
}
|
|
65
97
|
}
|
|
66
|
-
}, [element, elementsArg])
|
|
67
|
-
|
|
68
|
-
useEffect(() => {
|
|
69
|
-
if (!listener) return undefined
|
|
70
98
|
|
|
71
|
-
|
|
72
|
-
|
|
99
|
+
listener(evt)
|
|
100
|
+
})
|
|
73
101
|
|
|
74
|
-
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (boundaryElement && !boundaryElement.contains(target)) {
|
|
79
|
-
return
|
|
80
|
-
}
|
|
102
|
+
const hasListener = Boolean(listener)
|
|
81
103
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
}
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!hasListener) return undefined
|
|
87
106
|
|
|
88
|
-
|
|
89
|
-
}
|
|
107
|
+
const handleEvent = (evt: MouseEvent | TouchEvent) => onEvent(evt)
|
|
90
108
|
|
|
91
|
-
|
|
109
|
+
document.addEventListener('mousedown', handleEvent)
|
|
110
|
+
document.addEventListener('touchstart', handleEvent)
|
|
92
111
|
|
|
93
112
|
return () => {
|
|
94
|
-
|
|
113
|
+
document.removeEventListener('mousedown', handleEvent)
|
|
114
|
+
document.removeEventListener('touchstart', handleEvent)
|
|
95
115
|
}
|
|
96
|
-
}, [
|
|
116
|
+
}, [hasListener, onEvent])
|
|
97
117
|
|
|
98
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Only return the legacy setElement function if the elementsArg is an array
|
|
120
|
+
*/
|
|
121
|
+
if (Array.isArray(elementsArg)) {
|
|
122
|
+
return setLegacyElement as UseClickOutsideSetElement
|
|
123
|
+
}
|
|
99
124
|
}
|