@sanity/ui 2.7.1-canary.2 → 2.8.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.1-canary.2",
3
+ "version": "2.8.1",
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
+ "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
+ },
54
80
  "commitlint": {
55
81
  "extends": [
56
82
  "@commitlint/config-conventional"
@@ -169,37 +195,19 @@
169
195
  "react-is": "^18",
170
196
  "styled-components": "^5.2 || ^6"
171
197
  },
198
+ "packageManager": "pnpm@9.5.0",
172
199
  "engines": {
173
200
  "node": ">=14.0.0"
174
201
  },
175
202
  "publishConfig": {
176
203
  "access": "public"
177
204
  },
205
+ "pnpm": {
206
+ "overrides": {
207
+ "conventional-changelog-conventionalcommits": ">= 8.0.0"
208
+ }
209
+ },
178
210
  "esm.sh": {
179
211
  "bundle": false
180
- },
181
- "scripts": {
182
- "build": "run-s clean pkg:build pkg:check figma:pkg:build",
183
- "clean": "rimraf .workshop dist",
184
- "commit": "cz",
185
- "cypress:dev": "run-p dev cypress:open",
186
- "cypress:open": "cypress open",
187
- "cypress:run": "cypress run",
188
- "dev": "run-p storybook:dev workshop:dev",
189
- "figma:pkg:build": "cd figma && pnpm build",
190
- "format": "prettier --write --cache --ignore-unknown .",
191
- "lint": "eslint . --ext .cjs,.js,.jsx,.mjs,.ts,.tsx",
192
- "pkg:build": "pkg build --strict",
193
- "pkg:check": "pkg --strict",
194
- "release": "semantic-release",
195
- "storybook:build": "storybook build",
196
- "storybook:dev": "storybook dev -p 6006",
197
- "test": "jest",
198
- "test:browser": "start-server-and-test 'run-s workshop:build workshop:start' http://localhost:1337 'run-s cypress:run'",
199
- "ts:check": "tsc",
200
- "watch": "pkg watch --strict",
201
- "workshop:build": "workshop build",
202
- "workshop:dev": "workshop dev",
203
- "workshop:start": "http-server -a localhost -c-0 -p 1337 -s -P http://localhost:1337/index.html? dist"
204
212
  }
205
- }
213
+ }
@@ -1,5 +1,6 @@
1
1
  export * from './useArrayProp'
2
2
  export * from './useClickOutside'
3
+ export * from './useClickOutsideEvent'
3
4
  export * from './useCustomValidity'
4
5
  export * from './useElementRect'
5
6
  export * from './useElementSize'
@@ -8,115 +8,6 @@ import {useEffect, useRef, useState} from 'react'
8
8
  import {useClickOutside} from './useClickOutside'
9
9
 
10
10
  describe('useClickOutside', () => {
11
- describe('current API', () => {
12
- /**
13
- * This suite demonstrates the new API for `useClickOutside` that were introduced in v2.8.0
14
- */
15
-
16
- it('calls the handler when clicking outside of the array of elements', async () => {
17
- const user = userEvent.setup()
18
- const handler = jest.fn()
19
-
20
- const TestComponent = () => {
21
- const buttonRef = useRef<HTMLButtonElement | null>(null)
22
- const popoverRef = useRef<HTMLDivElement | null>(null)
23
-
24
- useClickOutside(handler, () => [buttonRef.current, popoverRef.current])
25
-
26
- return (
27
- <>
28
- <button data-testid="button" ref={buttonRef} />
29
- <div data-testid="popover" ref={popoverRef} />
30
- <div data-testid="outside" />
31
- </>
32
- )
33
- }
34
-
35
- render(<TestComponent />)
36
-
37
- await user.click(screen.getByTestId('button'))
38
- await user.click(screen.getByTestId('popover'))
39
- expect(handler).not.toHaveBeenCalled()
40
-
41
- await user.click(screen.getByTestId('outside'))
42
- expect(handler).toHaveBeenCalledTimes(1)
43
- })
44
-
45
- it('the elements array flattens nested arrays one level deep', async () => {
46
- const user = userEvent.setup()
47
- const handler = jest.fn()
48
-
49
- const TestComponent = () => {
50
- const buttonRef = useRef<HTMLButtonElement | null>(null)
51
- const popoverRef = useRef<HTMLDivElement | null>(null)
52
-
53
- useClickOutside(handler, () => [
54
- null,
55
- [null, buttonRef.current],
56
- [popoverRef.current, null],
57
- null,
58
- ])
59
-
60
- return (
61
- <>
62
- <button data-testid="button" ref={buttonRef} />
63
- <div data-testid="popover" ref={popoverRef} />
64
- <div data-testid="outside" />
65
- </>
66
- )
67
- }
68
-
69
- render(<TestComponent />)
70
-
71
- await user.click(screen.getByTestId('button'))
72
- await user.click(screen.getByTestId('popover'))
73
- expect(handler).not.toHaveBeenCalled()
74
-
75
- await user.click(screen.getByTestId('outside'))
76
- expect(handler).toHaveBeenCalledTimes(1)
77
- })
78
-
79
- it('it can set a boundary to scope outside click events', async () => {
80
- const user = userEvent.setup()
81
- const handler = jest.fn()
82
-
83
- const TestComponent = () => {
84
- const buttonRef = useRef<HTMLButtonElement | null>(null)
85
- const popoverRef = useRef<HTMLDivElement | null>(null)
86
- const boundaryRef = useRef<HTMLDivElement | null>(null)
87
-
88
- useClickOutside(
89
- handler,
90
- () => [buttonRef.current, popoverRef.current],
91
- () => boundaryRef.current,
92
- )
93
-
94
- return (
95
- <>
96
- <div ref={boundaryRef}>
97
- <button data-testid="button" ref={buttonRef} />
98
- <div data-testid="popover" ref={popoverRef} />
99
- <div data-testid="inside" />
100
- </div>
101
- <div data-testid="outside" />
102
- </>
103
- )
104
- }
105
-
106
- render(<TestComponent />)
107
-
108
- await user.click(screen.getByTestId('button'))
109
- await user.click(screen.getByTestId('popover'))
110
- // Since it's outside the boundary it should be ignored
111
- await user.click(screen.getByTestId('outside'))
112
- expect(handler).not.toHaveBeenCalled()
113
-
114
- await user.click(screen.getByTestId('inside'))
115
- expect(handler).toHaveBeenCalledTimes(1)
116
- })
117
- })
118
- })
119
- describe('legacy API', () => {
120
11
  /**
121
12
  * Specifying the elements array directly:
122
13
  * useClickOutside(handler, [buttonElement, popoverElement])
@@ -1,5 +1,4 @@
1
- import {useEffect, useState} 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
  /**
@@ -12,137 +11,106 @@ export type ClickOutsideListener = (event: MouseEvent) => void
12
11
  */
13
12
  export type ClickOutsideElements = (HTMLElement | null | (HTMLElement | null)[])[]
14
13
 
15
- /**
16
- * @public
17
- * @deprecated - use your own `const [element, setElement] = useState(null)` logic instead
18
- */
19
- export type UseClickOutsideSetElement = (el: HTMLElement | null) => void
20
-
21
- /**
22
- * @public
23
- */
24
- export function useClickOutside(
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,
14
+ function _getElements(
15
+ element: HTMLElement | null,
44
16
  elementsArg: ClickOutsideElements,
45
- boundaryElement: () => HTMLElement | null,
46
- ): UseClickOutsideSetElement
17
+ ): HTMLElement[] {
18
+ const ret = [element]
19
+
20
+ for (const el of elementsArg) {
21
+ if (Array.isArray(el)) {
22
+ ret.push(...el)
23
+ } else {
24
+ ret.push(el)
25
+ }
26
+ }
27
+
28
+ return ret.filter(Boolean) as HTMLElement[]
29
+ }
30
+
47
31
  /**
48
32
  * @public
49
- * @deprecated
50
- * Instead of:
33
+ * @deprecated replaced by the new `useClickOutsideEvent` hook, instead of:
51
34
  * ```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
- * )
35
+ * const [button, setButtonElement] = useState(null)
36
+ * useClickOutside((event) => {}, [button])
37
+ * return <button ref={setButtonElement} />
60
38
  * ```
61
- * Use:
39
+ * do:
62
40
  * ```tsx
63
41
  * 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
- * )
42
+ * useClickOutsideEvent((event) => {}, () => [buttonRef.current])
43
+ * return <button ref={buttonRef} />
72
44
  * ```
73
45
  */
74
46
  export function useClickOutside(
75
- listener: ClickOutsideListener | false | undefined,
76
- elementsArg: ClickOutsideElements,
47
+ listener: ClickOutsideListener,
48
+ elementsArg: ClickOutsideElements = EMPTY_ARRAY,
77
49
  boundaryElement?: HTMLElement | null,
78
- ): UseClickOutsideSetElement
50
+ ): (el: HTMLElement | null) => void {
51
+ const [element, setElement] = useState<HTMLElement | null>(null)
52
+ const [elements, setElements] = useState(() => _getElements(element, elementsArg))
53
+ const elementsRef = useRef(elements)
79
54
 
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) {
97
- return
98
- }
55
+ useEffect(() => {
56
+ const prevElements = elementsRef.current
57
+ const nextElements = _getElements(element, elementsArg)
99
58
 
100
- const target = evt.target
59
+ if (prevElements.length !== nextElements.length) {
60
+ setElements(nextElements)
61
+ elementsRef.current = nextElements
101
62
 
102
- if (!(target instanceof Node)) {
103
63
  return
104
64
  }
105
65
 
106
- const resolvedBoundaryElement =
107
- typeof boundaryElement === 'function' ? boundaryElement() : boundaryElement
108
- if (resolvedBoundaryElement && !resolvedBoundaryElement.contains(target)) {
109
- return
110
- }
66
+ for (const el of prevElements) {
67
+ if (!nextElements.includes(el)) {
68
+ setElements(nextElements)
69
+ elementsRef.current = nextElements
111
70
 
112
- const resolvedElements = Array.isArray(elementsArg)
113
- ? [legacyElement, ...elementsArg]
114
- : elementsArg()
115
- const elements = resolvedElements.flat()
71
+ return
72
+ }
73
+ }
116
74
 
117
- for (const el of elements) {
118
- if (!el) continue
75
+ for (const el of nextElements) {
76
+ if (!prevElements.includes(el)) {
77
+ setElements(nextElements)
78
+ elementsRef.current = nextElements
119
79
 
120
- if (target === el || el.contains(target)) {
121
80
  return
122
81
  }
123
82
  }
83
+ }, [element, elementsArg])
124
84
 
125
- listener(evt)
126
- })
85
+ useEffect(() => {
86
+ if (!listener) return undefined
127
87
 
128
- const hasListener = Boolean(listener)
88
+ const handleWindowMouseDown = (evt: MouseEvent) => {
89
+ const target = evt.target
129
90
 
130
- useEffect(() => {
131
- if (!hasListener) return undefined
91
+ if (!(target instanceof Node)) {
92
+ return
93
+ }
132
94
 
133
- const handleEvent = (evt: MouseEvent) => onEvent(evt)
95
+ if (boundaryElement && !boundaryElement.contains(target)) {
96
+ return
97
+ }
134
98
 
135
- document.addEventListener('mousedown', handleEvent)
99
+ for (const el of elements) {
100
+ if (target === el || el.contains(target)) {
101
+ return
102
+ }
103
+ }
104
+
105
+ listener(evt)
106
+ }
107
+
108
+ window.addEventListener('mousedown', handleWindowMouseDown)
136
109
 
137
110
  return () => {
138
- document.removeEventListener('mousedown', handleEvent)
111
+ window.removeEventListener('mousedown', handleWindowMouseDown)
139
112
  }
140
- }, [hasListener, onEvent])
113
+ }, [boundaryElement, listener, elements])
141
114
 
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
- }
115
+ return setElement
148
116
  }
@@ -0,0 +1,116 @@
1
+ /** @jest-environment jsdom */
2
+ /* eslint-disable padding-line-between-statements, react/display-name */
3
+
4
+ import {render, screen} from '@testing-library/react'
5
+ import userEvent from '@testing-library/user-event'
6
+ import {useRef} from 'react'
7
+
8
+ import {useClickOutsideEvent} from './useClickOutsideEvent'
9
+
10
+ describe('useClickOutsideEvent', () => {
11
+ /**
12
+ * This suite demonstrates the new hook `useClickOutsideEvent` that replaces `useClickOutside`
13
+ */
14
+
15
+ it('calls the handler when clicking outside of the array of elements', async () => {
16
+ const user = userEvent.setup()
17
+ const handler = jest.fn()
18
+
19
+ const TestComponent = () => {
20
+ const buttonRef = useRef<HTMLButtonElement | null>(null)
21
+ const popoverRef = useRef<HTMLDivElement | null>(null)
22
+
23
+ useClickOutsideEvent(handler, () => [buttonRef.current, popoverRef.current])
24
+
25
+ return (
26
+ <>
27
+ <button data-testid="button" ref={buttonRef} />
28
+ <div data-testid="popover" ref={popoverRef} />
29
+ <div data-testid="outside" />
30
+ </>
31
+ )
32
+ }
33
+
34
+ render(<TestComponent />)
35
+
36
+ await user.click(screen.getByTestId('button'))
37
+ await user.click(screen.getByTestId('popover'))
38
+ expect(handler).not.toHaveBeenCalled()
39
+
40
+ await user.click(screen.getByTestId('outside'))
41
+ expect(handler).toHaveBeenCalledTimes(1)
42
+ })
43
+
44
+ it('the elements array flattens nested arrays one level deep', async () => {
45
+ const user = userEvent.setup()
46
+ const handler = jest.fn()
47
+
48
+ const TestComponent = () => {
49
+ const buttonRef = useRef<HTMLButtonElement | null>(null)
50
+ const popoverRef = useRef<HTMLDivElement | null>(null)
51
+
52
+ useClickOutsideEvent(handler, () => [
53
+ null,
54
+ [null, buttonRef.current],
55
+ [popoverRef.current, null],
56
+ null,
57
+ ])
58
+
59
+ return (
60
+ <>
61
+ <button data-testid="button" ref={buttonRef} />
62
+ <div data-testid="popover" ref={popoverRef} />
63
+ <div data-testid="outside" />
64
+ </>
65
+ )
66
+ }
67
+
68
+ render(<TestComponent />)
69
+
70
+ await user.click(screen.getByTestId('button'))
71
+ await user.click(screen.getByTestId('popover'))
72
+ expect(handler).not.toHaveBeenCalled()
73
+
74
+ await user.click(screen.getByTestId('outside'))
75
+ expect(handler).toHaveBeenCalledTimes(1)
76
+ })
77
+
78
+ it('it can set a boundary to scope outside click events', async () => {
79
+ const user = userEvent.setup()
80
+ const handler = jest.fn()
81
+
82
+ const TestComponent = () => {
83
+ const buttonRef = useRef<HTMLButtonElement | null>(null)
84
+ const popoverRef = useRef<HTMLDivElement | null>(null)
85
+ const boundaryRef = useRef<HTMLDivElement | null>(null)
86
+
87
+ useClickOutsideEvent(
88
+ handler,
89
+ () => [buttonRef.current, popoverRef.current],
90
+ () => boundaryRef.current,
91
+ )
92
+
93
+ return (
94
+ <>
95
+ <div ref={boundaryRef}>
96
+ <button data-testid="button" ref={buttonRef} />
97
+ <div data-testid="popover" ref={popoverRef} />
98
+ <div data-testid="inside" />
99
+ </div>
100
+ <div data-testid="outside" />
101
+ </>
102
+ )
103
+ }
104
+
105
+ render(<TestComponent />)
106
+
107
+ await user.click(screen.getByTestId('button'))
108
+ await user.click(screen.getByTestId('popover'))
109
+ // Since it's outside the boundary it should be ignored
110
+ await user.click(screen.getByTestId('outside'))
111
+ expect(handler).not.toHaveBeenCalled()
112
+
113
+ await user.click(screen.getByTestId('inside'))
114
+ expect(handler).toHaveBeenCalledTimes(1)
115
+ })
116
+ })
@@ -0,0 +1,72 @@
1
+ import {useEffect} from 'react'
2
+ import {useEffectEvent} from 'use-effect-event'
3
+ import {EMPTY_ARRAY} from '../constants'
4
+
5
+ /**
6
+ * @public
7
+ */
8
+ export type ClickOutsideEventListener = (event: MouseEvent) => void
9
+
10
+ /**
11
+ * @public
12
+ */
13
+ export type ClickOutsideEventElements = (HTMLElement | null | (HTMLElement | null)[])[]
14
+
15
+ /**
16
+ * @public
17
+ */
18
+ export function useClickOutsideEvent(
19
+ listener: ClickOutsideEventListener | false | undefined,
20
+ elementsArg: () => ClickOutsideEventElements = () => EMPTY_ARRAY,
21
+ boundaryElement?: () => HTMLElement | null,
22
+ ): void {
23
+ /**
24
+ * The `useEffectEvent` hook allow us to always see the latest value of `listener`, `elementsArg` and `boundaryElement` without needing to
25
+ * juggle `useState`, `useRef` and `useState` to make sure the `mousedown` event listener isn't constantly being added and removed.
26
+ */
27
+ const onEvent = useEffectEvent((evt: MouseEvent) => {
28
+ if (!listener) {
29
+ return
30
+ }
31
+
32
+ const target = evt.target
33
+
34
+ if (!(target instanceof Node)) {
35
+ return
36
+ }
37
+
38
+ const resolvedBoundaryElement =
39
+ typeof boundaryElement === 'function' ? boundaryElement() : boundaryElement
40
+
41
+ if (resolvedBoundaryElement && !resolvedBoundaryElement.contains(target)) {
42
+ return
43
+ }
44
+
45
+ const resolvedElements = Array.isArray(elementsArg) ? elementsArg : elementsArg()
46
+ const elements = resolvedElements.flat()
47
+
48
+ for (const el of elements) {
49
+ if (!el) continue
50
+
51
+ if (target === el || el.contains(target)) {
52
+ return
53
+ }
54
+ }
55
+
56
+ listener(evt)
57
+ })
58
+
59
+ const hasListener = Boolean(listener)
60
+
61
+ useEffect(() => {
62
+ if (!hasListener) return undefined
63
+
64
+ const handleEvent = (evt: MouseEvent) => onEvent(evt)
65
+
66
+ document.addEventListener('mousedown', handleEvent)
67
+
68
+ return () => {
69
+ document.removeEventListener('mousedown', handleEvent)
70
+ }
71
+ }, [hasListener, onEvent])
72
+ }
@@ -1,5 +1,5 @@
1
1
  import {EllipsisVerticalIcon} from '@sanity/icons'
2
- import {Button, Card, Flex, Popover, Text, useClickOutside} from '@sanity/ui'
2
+ import {Button, Card, Flex, Popover, Text, useClickOutsideEvent} from '@sanity/ui'
3
3
  import {useBoolean, useSelect} from '@sanity/ui-workshop'
4
4
  import {useCallback, useRef, useState} from 'react'
5
5
  import {
@@ -40,7 +40,7 @@ export default function AlignedStory() {
40
40
 
41
41
  const handleToggleOpen = useCallback(() => setOpen((v) => !v), [])
42
42
 
43
- useClickOutside(
43
+ useClickOutsideEvent(
44
44
  () => setOpen(false),
45
45
  () => [buttonElementRef.current, popoverElementRef.current],
46
46
  )