@sanity/ui 1.8.3 → 1.9.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.esm.js +40 -42
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +39 -41
- package/dist/index.js.map +1 -1
- package/package.json +4 -6
- package/src/primitives/tooltip/__workshop__/index.ts +10 -0
- package/src/primitives/tooltip/__workshop__/overflowingBoundary.tsx +53 -0
- package/src/primitives/tooltip/__workshop__/resizableBoundary.tsx +85 -0
- package/src/primitives/tooltip/constants.ts +1 -0
- package/src/primitives/tooltip/tooltip.test.tsx +67 -0
- package/src/primitives/tooltip/tooltip.tsx +35 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"source": "./src/index.ts",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"test": "jest",
|
|
45
45
|
"test:browser": "start-server-and-test 'run-s workshop:build workshop:start' http://localhost:1337 'run-s cypress:run'",
|
|
46
46
|
"watch": "pkg watch --strict",
|
|
47
|
-
"workshop:build": "
|
|
48
|
-
"workshop:dev": "
|
|
47
|
+
"workshop:build": "workshop build",
|
|
48
|
+
"workshop:dev": "workshop dev",
|
|
49
49
|
"workshop:start": "http-server -a localhost -c-0 -p 1337 -s -P http://localhost:1337/index.html? dist"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"@juggle/resize-observer": "^3.4.0",
|
|
67
67
|
"@sanity/pkg-utils": "^2.4.8",
|
|
68
68
|
"@sanity/semantic-release-preset": "^4.1.4",
|
|
69
|
-
"@sanity/ui-workshop": "^
|
|
69
|
+
"@sanity/ui-workshop": "^2.0.0",
|
|
70
70
|
"@testing-library/dom": "^9.3.1",
|
|
71
71
|
"@testing-library/jest-dom": "^5.17.0",
|
|
72
72
|
"@testing-library/react": "^14.0.0",
|
|
@@ -84,8 +84,6 @@
|
|
|
84
84
|
"cypress": "^13.1.0",
|
|
85
85
|
"cypress-real-events": "^1.10.1",
|
|
86
86
|
"cz-conventional-changelog": "^3.3.0",
|
|
87
|
-
"esbuild": "^0.19.2",
|
|
88
|
-
"esbuild-register": "^3.4.2",
|
|
89
87
|
"eslint": "^8.48.0",
|
|
90
88
|
"eslint-config-prettier": "^9.0.0",
|
|
91
89
|
"eslint-plugin-import": "^2.28.1",
|
|
@@ -10,5 +10,15 @@ export default defineScope({
|
|
|
10
10
|
title: 'Props',
|
|
11
11
|
component: lazy(() => import('./props')),
|
|
12
12
|
},
|
|
13
|
+
{
|
|
14
|
+
name: 'resizableBoundary',
|
|
15
|
+
title: 'Resizable Boundary',
|
|
16
|
+
component: lazy(() => import('./resizableBoundary')),
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'overflowBoundary',
|
|
20
|
+
title: 'Overflowing Boundary',
|
|
21
|
+
component: lazy(() => import('./overflowingBoundary')),
|
|
22
|
+
},
|
|
13
23
|
],
|
|
14
24
|
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {BoundaryElementProvider, Button, Card, Code, Flex, Text, Tooltip} from '@sanity/ui'
|
|
2
|
+
import {useBoolean, useSelect, useText} from '@sanity/ui-workshop'
|
|
3
|
+
import {useState} from 'react'
|
|
4
|
+
import {WORKSHOP_PLACEMENT_OPTIONS} from '../../../__workshop__/constants'
|
|
5
|
+
|
|
6
|
+
export default function OverflowingBoundaryStory() {
|
|
7
|
+
const content = useText(
|
|
8
|
+
'Content',
|
|
9
|
+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut mollis consectetur malesuada. Sed lobortis est dolor, eget imperdiet velit placerat et. Aenean posuere mi non aliquet iaculis. Donec fermentum pulvinar purus at sagittis. Ut tincidunt massa odio, sed finibus justo ullamcorper id. Nam venenatis justo non ligula elementum cursus. Pellentesque laoreet justo in mollis sagittis. In lacinia ornare ultrices. Suspendisse potenti.',
|
|
10
|
+
)
|
|
11
|
+
const placement = useSelect('Placement', WORKSHOP_PLACEMENT_OPTIONS, 'top')
|
|
12
|
+
const portal = useBoolean('Portal', false)
|
|
13
|
+
const useBoundaryElement = useBoolean('Use boundary element', true)
|
|
14
|
+
|
|
15
|
+
const [boundaryElement, setBoundaryElement] = useState<HTMLDivElement | null>(null)
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Flex align="center" height="fill" justify="center" style={{height: '200vh'}}>
|
|
19
|
+
<BoundaryElementProvider element={useBoundaryElement ? boundaryElement : null}>
|
|
20
|
+
<Card
|
|
21
|
+
border
|
|
22
|
+
ref={setBoundaryElement}
|
|
23
|
+
style={{
|
|
24
|
+
height: 'calc(100vh - 100px)',
|
|
25
|
+
overflow: 'hidden',
|
|
26
|
+
position: 'relative',
|
|
27
|
+
resize: 'both',
|
|
28
|
+
width: '500px',
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
<Flex align="center" height="fill" justify="center">
|
|
32
|
+
<Flex justify="center">
|
|
33
|
+
<Code size={1}>Placement: {placement}</Code>
|
|
34
|
+
</Flex>
|
|
35
|
+
</Flex>
|
|
36
|
+
|
|
37
|
+
<Tooltip
|
|
38
|
+
content={<Text size={1}>{content}</Text>}
|
|
39
|
+
padding={2}
|
|
40
|
+
placement={placement}
|
|
41
|
+
portal={portal}
|
|
42
|
+
>
|
|
43
|
+
<Button
|
|
44
|
+
mode="bleed"
|
|
45
|
+
style={{position: 'absolute', top: 10, right: 10}}
|
|
46
|
+
text="Tooltip"
|
|
47
|
+
/>
|
|
48
|
+
</Tooltip>
|
|
49
|
+
</Card>
|
|
50
|
+
</BoundaryElementProvider>
|
|
51
|
+
</Flex>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {BoundaryElementProvider, Button, Card, Code, Flex, Text, Tooltip} from '@sanity/ui'
|
|
2
|
+
import {useBoolean, useSelect, useText} from '@sanity/ui-workshop'
|
|
3
|
+
import {useState} from 'react'
|
|
4
|
+
import {WORKSHOP_PLACEMENT_OPTIONS} from '../../../__workshop__/constants'
|
|
5
|
+
|
|
6
|
+
export default function ResizableBoundaryStory() {
|
|
7
|
+
const content = useText(
|
|
8
|
+
'Content',
|
|
9
|
+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut mollis consectetur malesuada. Sed lobortis est dolor, eget imperdiet velit placerat et. Aenean posuere mi non aliquet iaculis. Donec fermentum pulvinar purus at sagittis. Ut tincidunt massa odio, sed finibus justo ullamcorper id. Nam venenatis justo non ligula elementum cursus. Pellentesque laoreet justo in mollis sagittis. In lacinia ornare ultrices. Suspendisse potenti.',
|
|
10
|
+
)
|
|
11
|
+
const placement = useSelect('Placement', WORKSHOP_PLACEMENT_OPTIONS, 'right')
|
|
12
|
+
const portal = useBoolean('Portal', true)
|
|
13
|
+
const useBoundaryElement = useBoolean('Use boundary element', true)
|
|
14
|
+
|
|
15
|
+
const [boundaryElement, setBoundaryElement] = useState<HTMLDivElement | null>(null)
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Flex align="center" height="fill" justify="center">
|
|
19
|
+
<BoundaryElementProvider element={useBoundaryElement ? boundaryElement : null}>
|
|
20
|
+
<Card
|
|
21
|
+
border
|
|
22
|
+
ref={setBoundaryElement}
|
|
23
|
+
style={{
|
|
24
|
+
height: 'calc(100vh - 100px)',
|
|
25
|
+
overflow: 'hidden',
|
|
26
|
+
position: 'relative',
|
|
27
|
+
resize: 'both',
|
|
28
|
+
width: '500px',
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
<Flex align="center" height="fill" justify="center">
|
|
32
|
+
<Flex justify="center">
|
|
33
|
+
<Code size={1}>Placement: {placement}</Code>
|
|
34
|
+
</Flex>
|
|
35
|
+
</Flex>
|
|
36
|
+
|
|
37
|
+
<Tooltip
|
|
38
|
+
content={<Text size={1}>{content}</Text>}
|
|
39
|
+
padding={2}
|
|
40
|
+
placement={placement}
|
|
41
|
+
portal={portal}
|
|
42
|
+
>
|
|
43
|
+
<Button mode="bleed" style={{position: 'absolute', top: 10, left: 10}} text="Tooltip" />
|
|
44
|
+
</Tooltip>
|
|
45
|
+
<Tooltip
|
|
46
|
+
content={<Text size={1}>{content}</Text>}
|
|
47
|
+
padding={2}
|
|
48
|
+
placement={placement}
|
|
49
|
+
portal={portal}
|
|
50
|
+
>
|
|
51
|
+
<Button
|
|
52
|
+
mode="bleed"
|
|
53
|
+
style={{position: 'absolute', top: 10, right: 10}}
|
|
54
|
+
text="Tooltip"
|
|
55
|
+
/>
|
|
56
|
+
</Tooltip>
|
|
57
|
+
<Tooltip
|
|
58
|
+
content={<Text size={1}>{content}</Text>}
|
|
59
|
+
padding={2}
|
|
60
|
+
placement={placement}
|
|
61
|
+
portal={portal}
|
|
62
|
+
>
|
|
63
|
+
<Button
|
|
64
|
+
mode="bleed"
|
|
65
|
+
style={{position: 'absolute', bottom: 10, left: 10}}
|
|
66
|
+
text="Tooltip"
|
|
67
|
+
/>
|
|
68
|
+
</Tooltip>
|
|
69
|
+
<Tooltip
|
|
70
|
+
content={<Text size={1}>{content}</Text>}
|
|
71
|
+
padding={2}
|
|
72
|
+
placement={placement}
|
|
73
|
+
portal={portal}
|
|
74
|
+
>
|
|
75
|
+
<Button
|
|
76
|
+
mode="bleed"
|
|
77
|
+
style={{position: 'absolute', bottom: 10, right: 10}}
|
|
78
|
+
text="Tooltip"
|
|
79
|
+
/>
|
|
80
|
+
</Tooltip>
|
|
81
|
+
</Card>
|
|
82
|
+
</BoundaryElementProvider>
|
|
83
|
+
</Flex>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {Placement} from '@floating-ui/react-dom'
|
|
2
2
|
|
|
3
|
+
export const DEFAULT_TOOLTIP_PADDING = 4
|
|
3
4
|
export const DEFAULT_FALLBACK_PLACEMENTS: Record<Placement, Placement[]> = {
|
|
4
5
|
top: ['bottom', 'left', 'right'],
|
|
5
6
|
'top-start': ['bottom-start', 'left-start', 'right-start'],
|
|
@@ -275,4 +275,71 @@ describe('Tooltip', () => {
|
|
|
275
275
|
jest.clearAllMocks()
|
|
276
276
|
})
|
|
277
277
|
})
|
|
278
|
+
describe('Closing the <Tooltip /> with the Escape key', () => {
|
|
279
|
+
it('Standalone tooltip closes immediately with Escape key', () => {
|
|
280
|
+
const delay = 150
|
|
281
|
+
|
|
282
|
+
jest.useFakeTimers()
|
|
283
|
+
|
|
284
|
+
render(
|
|
285
|
+
<Tooltip
|
|
286
|
+
content={<Text size={1}>{'Tooltip content'}</Text>}
|
|
287
|
+
placement={'top'}
|
|
288
|
+
delay={delay}
|
|
289
|
+
>
|
|
290
|
+
<Button mode="bleed" text="Hover me" />
|
|
291
|
+
</Tooltip>,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
const button = screen.getByText('Hover me')
|
|
295
|
+
|
|
296
|
+
// Validate tooltip content is not rendered
|
|
297
|
+
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
|
|
298
|
+
fireEvent.focus(button)
|
|
299
|
+
act(() => jest.advanceTimersByTime(delay))
|
|
300
|
+
|
|
301
|
+
// Validate tooltip content is rendered
|
|
302
|
+
screen.getByText('Tooltip content')
|
|
303
|
+
|
|
304
|
+
act(() => {
|
|
305
|
+
fireEvent.keyDown(button, {key: 'Escape', code: 'Escape'})
|
|
306
|
+
})
|
|
307
|
+
// Validate tooltip content is not rendered anymore
|
|
308
|
+
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
|
|
309
|
+
})
|
|
310
|
+
it('With <TooltipDelayGroupProvider /> closes immediately with Escape key', () => {
|
|
311
|
+
const delay = 150
|
|
312
|
+
|
|
313
|
+
jest.useFakeTimers()
|
|
314
|
+
|
|
315
|
+
render(
|
|
316
|
+
<TooltipDelayGroupProvider delay={{close: delay}}>
|
|
317
|
+
<Tooltip
|
|
318
|
+
content={<Text size={1}>{'Tooltip content'}</Text>}
|
|
319
|
+
placement={'top'}
|
|
320
|
+
delay={{close: delay}}
|
|
321
|
+
>
|
|
322
|
+
<Button mode="bleed" text="Hover me" />
|
|
323
|
+
</Tooltip>
|
|
324
|
+
</TooltipDelayGroupProvider>,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
const button = screen.getByText('Hover me')
|
|
328
|
+
|
|
329
|
+
// Validate tooltip content is not rendered
|
|
330
|
+
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
|
|
331
|
+
fireEvent.focus(button)
|
|
332
|
+
|
|
333
|
+
act(() => jest.advanceTimersByTime(delay))
|
|
334
|
+
|
|
335
|
+
// Validate tooltip content is rendered
|
|
336
|
+
screen.getByText('Tooltip content')
|
|
337
|
+
|
|
338
|
+
act(() => {
|
|
339
|
+
fireEvent.keyDown(button, {key: 'Escape', code: 'Escape'})
|
|
340
|
+
})
|
|
341
|
+
// Validate tooltip content is not rendered anymore
|
|
342
|
+
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
|
|
343
|
+
})
|
|
344
|
+
})
|
|
278
345
|
})
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
useFloating,
|
|
9
9
|
Middleware,
|
|
10
10
|
RootBoundary,
|
|
11
|
-
size,
|
|
12
11
|
} from '@floating-ui/react-dom'
|
|
13
12
|
import {
|
|
14
13
|
cloneElement,
|
|
@@ -31,7 +30,7 @@ import {Placement} from '../../types'
|
|
|
31
30
|
import {Layer, LayerProps, Portal, useBoundaryElement} from '../../utils'
|
|
32
31
|
import {Card} from '../card'
|
|
33
32
|
import {Delay} from '../types'
|
|
34
|
-
import {DEFAULT_FALLBACK_PLACEMENTS} from './constants'
|
|
33
|
+
import {DEFAULT_FALLBACK_PLACEMENTS, DEFAULT_TOOLTIP_PADDING} from './constants'
|
|
35
34
|
import {TooltipArrow} from './tooltipArrow'
|
|
36
35
|
import {useTooltipDelayGroup} from './tooltipDelayGroup'
|
|
37
36
|
|
|
@@ -63,6 +62,9 @@ export interface TooltipProps extends Omit<LayerProps, 'as'> {
|
|
|
63
62
|
|
|
64
63
|
const Root = styled(Layer)`
|
|
65
64
|
pointer-events: none;
|
|
65
|
+
// Note that 100vw doesn't exclude system scrollbars.
|
|
66
|
+
// This should be sufficiently small enough to not trigger overflow-x scrollbars when portalled.
|
|
67
|
+
max-width: calc(100vw - ${DEFAULT_TOOLTIP_PADDING * 10}px);
|
|
66
68
|
`
|
|
67
69
|
|
|
68
70
|
/**
|
|
@@ -102,35 +104,22 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
102
104
|
// Flip the floating element when leaving the boundary box
|
|
103
105
|
ret.push(
|
|
104
106
|
flip({
|
|
105
|
-
boundary: boundaryElement || undefined,
|
|
107
|
+
boundary: portal ? undefined : boundaryElement || undefined,
|
|
106
108
|
fallbackPlacements,
|
|
107
|
-
padding:
|
|
109
|
+
padding: DEFAULT_TOOLTIP_PADDING,
|
|
108
110
|
rootBoundary,
|
|
109
|
-
mainAxis: false,
|
|
110
111
|
}),
|
|
111
112
|
)
|
|
112
113
|
|
|
113
114
|
// Define distance between reference and floating element
|
|
114
115
|
ret.push(offset({mainAxis: 3}))
|
|
115
116
|
|
|
116
|
-
//
|
|
117
|
-
ret.push(
|
|
118
|
-
size({
|
|
119
|
-
apply({availableWidth, availableHeight, elements}) {
|
|
120
|
-
Object.assign(elements.floating.style, {
|
|
121
|
-
maxWidth: `${availableWidth - 4 * 2}px`, // the padding is `4px`
|
|
122
|
-
maxHeight: `${availableHeight - 4 * 2}px`, // the padding is `4px`
|
|
123
|
-
})
|
|
124
|
-
},
|
|
125
|
-
}),
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
// Shift the tooltip so its sits with the boundary eleement
|
|
117
|
+
// Shift the tooltip so its sits with the boundary element
|
|
129
118
|
ret.push(
|
|
130
119
|
shift({
|
|
131
|
-
boundary: boundaryElement || undefined,
|
|
120
|
+
boundary: portal ? undefined : boundaryElement || undefined,
|
|
132
121
|
rootBoundary,
|
|
133
|
-
padding:
|
|
122
|
+
padding: DEFAULT_TOOLTIP_PADDING,
|
|
134
123
|
}),
|
|
135
124
|
)
|
|
136
125
|
|
|
@@ -138,7 +127,7 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
138
127
|
ret.push(arrow({element: arrowRef, padding: 2}))
|
|
139
128
|
|
|
140
129
|
return ret
|
|
141
|
-
}, [boundaryElement, fallbackPlacements])
|
|
130
|
+
}, [boundaryElement, fallbackPlacements, portal])
|
|
142
131
|
|
|
143
132
|
const {floatingStyles, placement, middlewareData, refs, update} = useFloating({
|
|
144
133
|
middleware,
|
|
@@ -177,23 +166,27 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
177
166
|
const closeDelay = isInsideGroup ? delayGroupContext.closeDelay : closeDelayProp
|
|
178
167
|
|
|
179
168
|
const handleIsOpenChange = useCallback(
|
|
180
|
-
(open: boolean) => {
|
|
169
|
+
(open: boolean, immediate?: boolean) => {
|
|
181
170
|
if (isInsideGroup) {
|
|
182
171
|
// When it's inside a group, the open or close status will be handled by the group.
|
|
183
172
|
if (open) {
|
|
184
|
-
|
|
185
|
-
|
|
173
|
+
const groupedOpenDelay = immediate ? 0 : openDelay
|
|
174
|
+
|
|
175
|
+
delayGroupContext.setIsGroupActive(open, groupedOpenDelay)
|
|
176
|
+
delayGroupContext.setOpenTooltipId(tooltipId, groupedOpenDelay)
|
|
186
177
|
} else {
|
|
187
178
|
const minimumGroupDeactivateDelay = 200 // We should provide some delay to allow the user to reach the next tooltip.
|
|
188
179
|
const groupDeactivateDelay =
|
|
189
180
|
closeDelay > minimumGroupDeactivateDelay ? closeDelay : minimumGroupDeactivateDelay
|
|
190
181
|
|
|
191
182
|
delayGroupContext.setIsGroupActive(open, groupDeactivateDelay)
|
|
192
|
-
delayGroupContext.setOpenTooltipId(null, closeDelay)
|
|
183
|
+
delayGroupContext.setOpenTooltipId(null, immediate ? 0 : closeDelay)
|
|
193
184
|
}
|
|
194
185
|
} else {
|
|
186
|
+
const standaloneDelay = immediate ? 0 : open ? openDelay : closeDelay
|
|
187
|
+
|
|
195
188
|
// When it's not inside a group, the open or close status will be handled by the tooltip itself.
|
|
196
|
-
setIsOpen(open,
|
|
189
|
+
setIsOpen(open, standaloneDelay)
|
|
197
190
|
}
|
|
198
191
|
},
|
|
199
192
|
[isInsideGroup, delayGroupContext, openDelay, tooltipId, closeDelay, setIsOpen],
|
|
@@ -243,6 +236,22 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
243
236
|
// Update reference
|
|
244
237
|
useEffect(() => refs.setReference(referenceElement), [referenceElement, refs])
|
|
245
238
|
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
// If the user clicks on escape key, close the tooltip.
|
|
241
|
+
if (!showTooltip) return
|
|
242
|
+
|
|
243
|
+
function handleWindowKeyDown(event: KeyboardEvent) {
|
|
244
|
+
if (event.key === 'Escape') {
|
|
245
|
+
handleIsOpenChange(false, true)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
window.addEventListener('keydown', handleWindowKeyDown)
|
|
250
|
+
|
|
251
|
+
return () => {
|
|
252
|
+
window.removeEventListener('keydown', handleWindowKeyDown)
|
|
253
|
+
}
|
|
254
|
+
}, [handleIsOpenChange, showTooltip])
|
|
246
255
|
const setArrow = useCallback(
|
|
247
256
|
(arrowEl: HTMLDivElement | null) => {
|
|
248
257
|
arrowRef.current = arrowEl
|