@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ui",
3
- "version": "2.7.0",
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, useRef, useState} from 'react'
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
- return ret.filter(Boolean) as HTMLElement[]
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: Array<HTMLElement | HTMLElement[] | null> = EMPTY_ARRAY,
21
+ elementsArg: () => (HTMLElement | HTMLElement[] | null)[],
32
22
  boundaryElement?: HTMLElement | null,
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)
23
+ ): void
41
24
 
42
- if (prevElements.length !== nextElements.length) {
43
- setElements(nextElements)
44
- elementsRef.current = nextElements
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
- for (const el of prevElements) {
50
- if (!nextElements.includes(el)) {
51
- setElements(nextElements)
52
- elementsRef.current = nextElements
53
-
54
- return
55
- }
82
+ if (boundaryElement && !boundaryElement.contains(target)) {
83
+ return
56
84
  }
57
85
 
58
- for (const el of nextElements) {
59
- if (!prevElements.includes(el)) {
60
- setElements(nextElements)
61
- elementsRef.current = nextElements
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
- const handleWindowMouseDown = (evt: MouseEvent) => {
72
- const target = evt.target
99
+ listener(evt)
100
+ })
73
101
 
74
- if (!(target instanceof Node)) {
75
- return
76
- }
77
-
78
- if (boundaryElement && !boundaryElement.contains(target)) {
79
- return
80
- }
102
+ const hasListener = Boolean(listener)
81
103
 
82
- for (const el of elements) {
83
- if (target === el || el.contains(target)) {
84
- return
85
- }
86
- }
104
+ useEffect(() => {
105
+ if (!hasListener) return undefined
87
106
 
88
- listener(evt)
89
- }
107
+ const handleEvent = (evt: MouseEvent | TouchEvent) => onEvent(evt)
90
108
 
91
- window.addEventListener('mousedown', handleWindowMouseDown)
109
+ document.addEventListener('mousedown', handleEvent)
110
+ document.addEventListener('touchstart', handleEvent)
92
111
 
93
112
  return () => {
94
- window.removeEventListener('mousedown', handleWindowMouseDown)
113
+ document.removeEventListener('mousedown', handleEvent)
114
+ document.removeEventListener('touchstart', handleEvent)
95
115
  }
96
- }, [boundaryElement, listener, elements])
116
+ }, [hasListener, onEvent])
97
117
 
98
- return setElement
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
  }