@sanity/ui 2.0.0-alpha.20 → 2.0.0-alpha.21
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 +57 -33
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +56 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/primitives/tooltip/__workshop__/customPortal.tsx +99 -0
- package/src/primitives/tooltip/__workshop__/index.ts +15 -0
- package/src/primitives/tooltip/__workshop__/overflowingBoundary.tsx +73 -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 +125 -0
- package/src/primitives/tooltip/tooltip.tsx +81 -28
package/package.json
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BoundaryElementProvider,
|
|
3
|
+
Button,
|
|
4
|
+
Card,
|
|
5
|
+
Flex,
|
|
6
|
+
Portal,
|
|
7
|
+
PortalProvider,
|
|
8
|
+
Stack,
|
|
9
|
+
Text,
|
|
10
|
+
Tooltip,
|
|
11
|
+
} from '@sanity/ui'
|
|
12
|
+
import {useBoolean, useSelect, useText} from '@sanity/ui-workshop'
|
|
13
|
+
import {useState} from 'react'
|
|
14
|
+
import {WORKSHOP_PLACEMENT_OPTIONS} from '../../../__workshop__/constants'
|
|
15
|
+
|
|
16
|
+
const PORTAL_OPTIONS = {
|
|
17
|
+
'(true)': true,
|
|
18
|
+
'(false)': false,
|
|
19
|
+
portal1: 'portal1',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function CustomPortalStory() {
|
|
23
|
+
const content = useText(
|
|
24
|
+
'Content',
|
|
25
|
+
'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.',
|
|
26
|
+
)
|
|
27
|
+
const placement = useSelect('Placement', WORKSHOP_PLACEMENT_OPTIONS, 'top')
|
|
28
|
+
const portal = useSelect('Portal', PORTAL_OPTIONS, undefined, 'Props')
|
|
29
|
+
const useBoundaryElement = useBoolean('Use boundary element', true)
|
|
30
|
+
|
|
31
|
+
const [portal1Element, setPortal1Element] = useState<HTMLDivElement | null>(null)
|
|
32
|
+
const [boundaryElement, setBoundaryElement] = useState<HTMLDivElement | null>(null)
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<PortalProvider
|
|
36
|
+
__unstable_elements={{
|
|
37
|
+
portal1: portal1Element,
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<Flex align="center" height="fill" justify="center">
|
|
41
|
+
<BoundaryElementProvider element={useBoundaryElement ? boundaryElement : null}>
|
|
42
|
+
<Card
|
|
43
|
+
border
|
|
44
|
+
padding={4}
|
|
45
|
+
ref={setBoundaryElement}
|
|
46
|
+
style={{
|
|
47
|
+
height: 'calc(100vh - 100px)',
|
|
48
|
+
position: 'relative',
|
|
49
|
+
width: '500px',
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<Text>Boundary element</Text>
|
|
53
|
+
<Flex align="center" height="fill" justify="center">
|
|
54
|
+
<Flex justify="center">
|
|
55
|
+
<Stack space={2}>
|
|
56
|
+
<Tooltip
|
|
57
|
+
boundaryElement={useBoundaryElement ? boundaryElement : null}
|
|
58
|
+
content={<Text size={1}>{content}</Text>}
|
|
59
|
+
padding={2}
|
|
60
|
+
placement={placement}
|
|
61
|
+
portal={portal}
|
|
62
|
+
>
|
|
63
|
+
<Button mode="bleed" text="Tooltip" />
|
|
64
|
+
</Tooltip>
|
|
65
|
+
</Stack>
|
|
66
|
+
</Flex>
|
|
67
|
+
</Flex>
|
|
68
|
+
</Card>
|
|
69
|
+
</BoundaryElementProvider>
|
|
70
|
+
</Flex>
|
|
71
|
+
|
|
72
|
+
<Card
|
|
73
|
+
border
|
|
74
|
+
margin={4}
|
|
75
|
+
padding={4}
|
|
76
|
+
ref={setPortal1Element}
|
|
77
|
+
style={{
|
|
78
|
+
left: 0,
|
|
79
|
+
position: 'absolute',
|
|
80
|
+
top: 0,
|
|
81
|
+
width: '175px',
|
|
82
|
+
}}
|
|
83
|
+
tone="critical"
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
<Portal __unstable_name="portal1">
|
|
87
|
+
<Stack space={4}>
|
|
88
|
+
<Text size={1} weight="medium">
|
|
89
|
+
Portal 1 content
|
|
90
|
+
</Text>
|
|
91
|
+
<Text size={1}>
|
|
92
|
+
The tooltip's max-width should not exceed this container's width (minus padding) if it's
|
|
93
|
+
set to use this portal
|
|
94
|
+
</Text>
|
|
95
|
+
</Stack>
|
|
96
|
+
</Portal>
|
|
97
|
+
</PortalProvider>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
@@ -10,5 +10,20 @@ 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
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'customPortalStory',
|
|
25
|
+
title: 'Custom Portal',
|
|
26
|
+
component: lazy(() => import('./customPortal')),
|
|
27
|
+
},
|
|
13
28
|
],
|
|
14
29
|
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {BoundaryElementProvider, Button, Card, Code, Flex, Stack, Text, Tooltip} from '@sanity/ui'
|
|
2
|
+
import {useBoolean, useSelect, useText} from '@sanity/ui-workshop'
|
|
3
|
+
import {useCallback, 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
|
+
const [buttonsVisible, setButtonsVisible] = useState(true)
|
|
17
|
+
|
|
18
|
+
const handleHideButtons = useCallback(() => {
|
|
19
|
+
setButtonsVisible(false)
|
|
20
|
+
}, [])
|
|
21
|
+
|
|
22
|
+
const handleShowButtons = useCallback(() => {
|
|
23
|
+
setButtonsVisible(true)
|
|
24
|
+
}, [])
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Flex align="center" height="fill" justify="center" style={{height: '200vh'}}>
|
|
28
|
+
<BoundaryElementProvider element={useBoundaryElement ? boundaryElement : null}>
|
|
29
|
+
<Card
|
|
30
|
+
border
|
|
31
|
+
ref={setBoundaryElement}
|
|
32
|
+
style={{
|
|
33
|
+
height: 'calc(100vh - 100px)',
|
|
34
|
+
overflow: 'hidden',
|
|
35
|
+
position: 'relative',
|
|
36
|
+
resize: 'both',
|
|
37
|
+
width: '500px',
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<Flex align="center" height="fill" justify="center">
|
|
41
|
+
<Flex justify="center">
|
|
42
|
+
<Stack space={2}>
|
|
43
|
+
<Code size={1}>Placement: {placement}</Code>
|
|
44
|
+
<Button
|
|
45
|
+
disabled={buttonsVisible}
|
|
46
|
+
fontSize={1}
|
|
47
|
+
onClick={handleShowButtons}
|
|
48
|
+
text="Show buttons"
|
|
49
|
+
/>
|
|
50
|
+
</Stack>
|
|
51
|
+
</Flex>
|
|
52
|
+
</Flex>
|
|
53
|
+
|
|
54
|
+
{buttonsVisible && (
|
|
55
|
+
<Tooltip
|
|
56
|
+
content={<Text size={1}>{content}</Text>}
|
|
57
|
+
padding={2}
|
|
58
|
+
placement={placement}
|
|
59
|
+
portal={portal}
|
|
60
|
+
>
|
|
61
|
+
<Button
|
|
62
|
+
mode="bleed"
|
|
63
|
+
onClick={handleHideButtons}
|
|
64
|
+
style={{position: 'absolute', top: 10, right: 10}}
|
|
65
|
+
text="Tooltip"
|
|
66
|
+
/>
|
|
67
|
+
</Tooltip>
|
|
68
|
+
)}
|
|
69
|
+
</Card>
|
|
70
|
+
</BoundaryElementProvider>
|
|
71
|
+
</Flex>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -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'],
|
|
@@ -6,6 +6,17 @@ import {render} from '../../../test'
|
|
|
6
6
|
import {Text, Button} from '../../primitives'
|
|
7
7
|
import {Tooltip, TooltipDelayGroupProvider} from '../tooltip'
|
|
8
8
|
|
|
9
|
+
// Fake timers using Jest
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.useFakeTimers()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
// Running all pending timers and switching to real timers using Jest
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
jest.runOnlyPendingTimers()
|
|
17
|
+
jest.useRealTimers()
|
|
18
|
+
})
|
|
19
|
+
|
|
9
20
|
describe('Tooltip', () => {
|
|
10
21
|
describe('Using same delay for open and close', () => {
|
|
11
22
|
it('should hide and show the tooltip content when hovered, with no delay', () => {
|
|
@@ -275,6 +286,7 @@ describe('Tooltip', () => {
|
|
|
275
286
|
jest.clearAllMocks()
|
|
276
287
|
})
|
|
277
288
|
})
|
|
289
|
+
|
|
278
290
|
describe('Closing the <Tooltip /> with the Escape key', () => {
|
|
279
291
|
it('Standalone tooltip closes immediately with Escape key', () => {
|
|
280
292
|
const delay = 150
|
|
@@ -342,4 +354,117 @@ describe('Tooltip', () => {
|
|
|
342
354
|
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
|
|
343
355
|
})
|
|
344
356
|
})
|
|
357
|
+
|
|
358
|
+
describe('Clicking the <Tooltip /> child should close the tooltip', () => {
|
|
359
|
+
it('Should close the tooltip when clicked', () => {
|
|
360
|
+
const delay = 150
|
|
361
|
+
|
|
362
|
+
render(
|
|
363
|
+
<Tooltip content={<Text size={1}>{'Tooltip content'}</Text>} delay={delay}>
|
|
364
|
+
<Button mode="bleed" text="Hover me" />
|
|
365
|
+
</Tooltip>,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
const button = screen.getByText('Hover me')
|
|
369
|
+
|
|
370
|
+
// Assertion: tooltip does not exist in the document
|
|
371
|
+
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
|
|
372
|
+
fireEvent.focus(button)
|
|
373
|
+
|
|
374
|
+
act(() => jest.advanceTimersByTime(delay))
|
|
375
|
+
|
|
376
|
+
// Assertion: the tooltip is not visible
|
|
377
|
+
expect(screen.queryByText('Tooltip content')).toBeVisible()
|
|
378
|
+
|
|
379
|
+
act(() => fireEvent.click(button))
|
|
380
|
+
|
|
381
|
+
// Assertion: tooltip does not exist in the document
|
|
382
|
+
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('Should close the tooltip when the context menu is opened (right click)', () => {
|
|
386
|
+
const delay = 150
|
|
387
|
+
|
|
388
|
+
render(
|
|
389
|
+
<Tooltip content={<Text size={1}>{'Tooltip content'}</Text>} delay={delay}>
|
|
390
|
+
<Button mode="bleed" text="Hover me" />
|
|
391
|
+
</Tooltip>,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
const button = screen.getByText('Hover me')
|
|
395
|
+
|
|
396
|
+
// Assertion: tooltip does not exist in the document
|
|
397
|
+
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
|
|
398
|
+
fireEvent.focus(button)
|
|
399
|
+
|
|
400
|
+
act(() => jest.advanceTimersByTime(delay))
|
|
401
|
+
|
|
402
|
+
// Assertion: the tooltip is not visible
|
|
403
|
+
expect(screen.queryByText('Tooltip content')).toBeVisible()
|
|
404
|
+
|
|
405
|
+
act(() => fireEvent.contextMenu(button))
|
|
406
|
+
|
|
407
|
+
// Assertion: tooltip does not exist in the document
|
|
408
|
+
expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()
|
|
409
|
+
})
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
describe('Used defined events on <Tooltip /> child should fire correctly', () => {
|
|
413
|
+
const handleBlur = jest.fn()
|
|
414
|
+
const handleClick = jest.fn()
|
|
415
|
+
const handleContextMenu = jest.fn()
|
|
416
|
+
const handleFocus = jest.fn()
|
|
417
|
+
const handleMouseEnter = jest.fn()
|
|
418
|
+
const handleMouseLeave = jest.fn()
|
|
419
|
+
|
|
420
|
+
beforeEach(() => {
|
|
421
|
+
render(
|
|
422
|
+
<Tooltip content={<Text size={1}>{'Tooltip content'}</Text>}>
|
|
423
|
+
<Button
|
|
424
|
+
data-testid="btn"
|
|
425
|
+
mode="bleed"
|
|
426
|
+
onBlur={handleBlur}
|
|
427
|
+
onClick={handleClick}
|
|
428
|
+
onContextMenu={handleContextMenu}
|
|
429
|
+
onFocus={handleFocus}
|
|
430
|
+
onMouseEnter={handleMouseEnter}
|
|
431
|
+
onMouseLeave={handleMouseLeave}
|
|
432
|
+
text="Hover me"
|
|
433
|
+
/>
|
|
434
|
+
</Tooltip>,
|
|
435
|
+
)
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
afterEach(() => jest.clearAllMocks())
|
|
439
|
+
|
|
440
|
+
it('should fire the onBlur event', () => {
|
|
441
|
+
fireEvent.blur(screen.getByTestId('btn'))
|
|
442
|
+
expect(handleBlur).toHaveBeenCalledTimes(1)
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
it('should fire the onClick event', () => {
|
|
446
|
+
fireEvent.click(screen.getByTestId('btn'))
|
|
447
|
+
expect(handleClick).toHaveBeenCalledTimes(1)
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it('should fire the onContextMenu event', () => {
|
|
451
|
+
fireEvent.contextMenu(screen.getByTestId('btn'))
|
|
452
|
+
expect(handleContextMenu).toHaveBeenCalledTimes(1)
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
it('should fire the onFocus event', () => {
|
|
456
|
+
fireEvent.focus(screen.getByTestId('btn'))
|
|
457
|
+
expect(handleFocus).toHaveBeenCalledTimes(1)
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
it('should fire the onMouseEnter event', () => {
|
|
461
|
+
fireEvent.mouseEnter(screen.getByTestId('btn'))
|
|
462
|
+
expect(handleMouseEnter).toHaveBeenCalledTimes(1)
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('should fire the onMouseLeave event', () => {
|
|
466
|
+
fireEvent.mouseLeave(screen.getByTestId('btn'))
|
|
467
|
+
expect(handleMouseLeave).toHaveBeenCalledTimes(1)
|
|
468
|
+
})
|
|
469
|
+
})
|
|
345
470
|
})
|
|
@@ -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,
|
|
@@ -28,10 +27,10 @@ import {useArrayProp, useForwardedRef} from '../../hooks'
|
|
|
28
27
|
import {useDelayedState} from '../../hooks/useDelayedState'
|
|
29
28
|
import {ThemeColorSchemeKey, useTheme} from '../../theme'
|
|
30
29
|
import {Placement} from '../../types'
|
|
31
|
-
import {Layer, LayerProps, Portal, useBoundaryElement} from '../../utils'
|
|
30
|
+
import {Layer, LayerProps, Portal, useBoundaryElement, usePortal} 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
|
|
|
@@ -62,8 +61,9 @@ export interface TooltipProps extends Omit<LayerProps, 'as'> {
|
|
|
62
61
|
delay?: Delay
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
const Root = styled(Layer)
|
|
64
|
+
const Root = styled(Layer)<{$maxWidth: number}>`
|
|
66
65
|
pointer-events: none;
|
|
66
|
+
max-width: ${({$maxWidth}) => $maxWidth}px;
|
|
67
67
|
`
|
|
68
68
|
|
|
69
69
|
/**
|
|
@@ -86,7 +86,7 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
86
86
|
DEFAULT_FALLBACK_PLACEMENTS[props.placement ?? 'bottom'],
|
|
87
87
|
padding = 3,
|
|
88
88
|
placement: placementProp = 'bottom',
|
|
89
|
-
portal,
|
|
89
|
+
portal: portalProp,
|
|
90
90
|
scheme,
|
|
91
91
|
shadow = 2,
|
|
92
92
|
zOffset = theme.sanity.layer?.tooltip.zOffset,
|
|
@@ -99,6 +99,22 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
99
99
|
const arrowRef = useRef<HTMLDivElement | null>(null)
|
|
100
100
|
const rootBoundary: RootBoundary = 'viewport'
|
|
101
101
|
|
|
102
|
+
const portal = usePortal()
|
|
103
|
+
const portalElement =
|
|
104
|
+
typeof portalProp === 'string' ? portal.elements?.[portalProp] || null : portal.element
|
|
105
|
+
|
|
106
|
+
// Get the maximum tooltip width (sans tooltip padding)
|
|
107
|
+
// Tooltip width should never exceed the width of either any supplied boundary or portal element.
|
|
108
|
+
// If both portal and boundary elements are provided, use the smaller width of the two.
|
|
109
|
+
const tooltipWidth = useMemo(() => {
|
|
110
|
+
const availableWidths = [
|
|
111
|
+
...(boundaryElement ? [boundaryElement.offsetWidth] : []),
|
|
112
|
+
portalElement?.offsetWidth || document.body.offsetWidth,
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
return Math.min(...availableWidths) - DEFAULT_TOOLTIP_PADDING * 2
|
|
116
|
+
}, [boundaryElement, portalElement?.offsetWidth])
|
|
117
|
+
|
|
102
118
|
const middleware = useMemo(() => {
|
|
103
119
|
const ret: Middleware[] = []
|
|
104
120
|
|
|
@@ -107,33 +123,20 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
107
123
|
flip({
|
|
108
124
|
boundary: boundaryElement || undefined,
|
|
109
125
|
fallbackPlacements,
|
|
110
|
-
padding:
|
|
126
|
+
padding: DEFAULT_TOOLTIP_PADDING,
|
|
111
127
|
rootBoundary,
|
|
112
|
-
mainAxis: false,
|
|
113
128
|
}),
|
|
114
129
|
)
|
|
115
130
|
|
|
116
131
|
// Define distance between reference and floating element
|
|
117
132
|
ret.push(offset({mainAxis: 3}))
|
|
118
133
|
|
|
119
|
-
//
|
|
120
|
-
ret.push(
|
|
121
|
-
size({
|
|
122
|
-
apply({availableWidth, availableHeight, elements}) {
|
|
123
|
-
Object.assign(elements.floating.style, {
|
|
124
|
-
maxWidth: `${availableWidth - 4 * 2}px`, // the padding is `4px`
|
|
125
|
-
maxHeight: `${availableHeight - 4 * 2}px`, // the padding is `4px`
|
|
126
|
-
})
|
|
127
|
-
},
|
|
128
|
-
}),
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
// Shift the tooltip so its sits with the boundary eleement
|
|
134
|
+
// Shift the tooltip so its sits with the boundary element
|
|
132
135
|
ret.push(
|
|
133
136
|
shift({
|
|
134
137
|
boundary: boundaryElement || undefined,
|
|
135
138
|
rootBoundary,
|
|
136
|
-
padding:
|
|
139
|
+
padding: DEFAULT_TOOLTIP_PADDING,
|
|
137
140
|
}),
|
|
138
141
|
)
|
|
139
142
|
|
|
@@ -206,10 +209,48 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
206
209
|
[isInsideGroup, delayGroupContext, openDelay, tooltipId, closeDelay, setIsOpen],
|
|
207
210
|
)
|
|
208
211
|
|
|
209
|
-
const handleBlur = useCallback(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
212
|
+
const handleBlur = useCallback(
|
|
213
|
+
(e: FocusEvent) => {
|
|
214
|
+
handleIsOpenChange(false)
|
|
215
|
+
childProp?.props?.onBlur?.(e)
|
|
216
|
+
},
|
|
217
|
+
[childProp?.props, handleIsOpenChange],
|
|
218
|
+
)
|
|
219
|
+
const handleClick = useCallback(
|
|
220
|
+
(e: MouseEvent) => {
|
|
221
|
+
handleIsOpenChange(false, true), [handleIsOpenChange]
|
|
222
|
+
childProp?.props.onClick?.(e)
|
|
223
|
+
},
|
|
224
|
+
[childProp?.props, handleIsOpenChange],
|
|
225
|
+
)
|
|
226
|
+
const handleContextMenu = useCallback(
|
|
227
|
+
(e: MouseEvent) => {
|
|
228
|
+
handleIsOpenChange(false, true), [handleIsOpenChange]
|
|
229
|
+
childProp?.props.onContextMenu?.(e)
|
|
230
|
+
},
|
|
231
|
+
[childProp?.props, handleIsOpenChange],
|
|
232
|
+
)
|
|
233
|
+
const handleFocus = useCallback(
|
|
234
|
+
(e: FocusEvent) => {
|
|
235
|
+
handleIsOpenChange(true)
|
|
236
|
+
childProp?.props?.onFocus?.(e)
|
|
237
|
+
},
|
|
238
|
+
[childProp?.props, handleIsOpenChange],
|
|
239
|
+
)
|
|
240
|
+
const handleMouseEnter = useCallback(
|
|
241
|
+
(e: MouseEvent) => {
|
|
242
|
+
handleIsOpenChange(true)
|
|
243
|
+
childProp?.props?.onMouseEnter?.(e)
|
|
244
|
+
},
|
|
245
|
+
[childProp?.props, handleIsOpenChange],
|
|
246
|
+
)
|
|
247
|
+
const handleMouseLeave = useCallback(
|
|
248
|
+
(e: MouseEvent) => {
|
|
249
|
+
handleIsOpenChange(false)
|
|
250
|
+
childProp?.props?.onMouseLeave?.(e)
|
|
251
|
+
},
|
|
252
|
+
[childProp?.props, handleIsOpenChange],
|
|
253
|
+
)
|
|
213
254
|
|
|
214
255
|
// Detect whether the mouse is moving outside of the reference element. This is sometimes
|
|
215
256
|
// necessary, because the tooltip might not always close as it should (e.g. when clicking
|
|
@@ -306,9 +347,20 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
306
347
|
onFocus: handleFocus,
|
|
307
348
|
onMouseEnter: handleMouseEnter,
|
|
308
349
|
onMouseLeave: handleMouseLeave,
|
|
350
|
+
onClick: handleClick,
|
|
351
|
+
onContextMenu: handleContextMenu,
|
|
309
352
|
ref: setReference,
|
|
310
353
|
})
|
|
311
|
-
}, [
|
|
354
|
+
}, [
|
|
355
|
+
childProp,
|
|
356
|
+
handleBlur,
|
|
357
|
+
handleClick,
|
|
358
|
+
handleContextMenu,
|
|
359
|
+
handleFocus,
|
|
360
|
+
handleMouseEnter,
|
|
361
|
+
handleMouseLeave,
|
|
362
|
+
setReference,
|
|
363
|
+
])
|
|
312
364
|
|
|
313
365
|
if (!child) return <></>
|
|
314
366
|
|
|
@@ -321,6 +373,7 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
321
373
|
ref={setFloating}
|
|
322
374
|
style={floatingStyles}
|
|
323
375
|
zOffset={zOffset}
|
|
376
|
+
$maxWidth={tooltipWidth}
|
|
324
377
|
>
|
|
325
378
|
<Card
|
|
326
379
|
data-ui="Tooltip__card"
|
|
@@ -342,8 +395,8 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
342
395
|
|
|
343
396
|
{showTooltip && (
|
|
344
397
|
<>
|
|
345
|
-
{
|
|
346
|
-
<Portal __unstable_name={typeof
|
|
398
|
+
{portalProp ? (
|
|
399
|
+
<Portal __unstable_name={typeof portalProp === 'string' ? portalProp : undefined}>
|
|
347
400
|
{root}
|
|
348
401
|
</Portal>
|
|
349
402
|
) : (
|