@sanity/ui 1.7.11 → 1.8.0
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.cjs.mjs +3 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.esm.js +133 -49
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +134 -47
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/src/hooks/useDelayed.test.ts +66 -0
- package/src/hooks/useDelayedState.ts +29 -0
- package/src/primitives/tooltip/__workshop__/props.tsx +86 -11
- package/src/primitives/tooltip/index.ts +1 -0
- package/src/primitives/tooltip/tooltip.test.tsx +278 -0
- package/src/primitives/tooltip/tooltip.tsx +81 -23
- package/src/primitives/tooltip/tooltipDelayGroup/index.ts +4 -0
- package/src/primitives/tooltip/tooltipDelayGroup/tooltipDelayGroupContext.tsx +13 -0
- package/src/primitives/tooltip/tooltipDelayGroup/tooltipDelayGroupProvider.tsx +61 -0
- package/src/primitives/tooltip/tooltipDelayGroup/types.ts +11 -0
- package/src/primitives/tooltip/tooltipDelayGroup/useTooltipDelayGroup.ts +12 -0
- package/src/primitives/types.ts +10 -0
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useFloating,
|
|
9
9
|
Middleware,
|
|
10
10
|
RootBoundary,
|
|
11
|
+
size,
|
|
11
12
|
} from '@floating-ui/react-dom'
|
|
12
13
|
import {
|
|
13
14
|
cloneElement,
|
|
@@ -19,15 +20,19 @@ import {
|
|
|
19
20
|
useState,
|
|
20
21
|
CSSProperties,
|
|
21
22
|
ForwardedRef,
|
|
23
|
+
useId,
|
|
22
24
|
} from 'react'
|
|
23
25
|
import styled from 'styled-components'
|
|
24
26
|
import {FLOATING_STATIC_SIDES} from '../../constants'
|
|
25
27
|
import {useArrayProp, useForwardedRef} from '../../hooks'
|
|
28
|
+
import {useDelayedState} from '../../hooks/useDelayedState'
|
|
26
29
|
import {ThemeColorSchemeKey, useTheme} from '../../theme'
|
|
27
30
|
import {Placement} from '../../types'
|
|
28
31
|
import {Layer, LayerProps, Portal, useBoundaryElement} from '../../utils'
|
|
29
32
|
import {Card} from '../card'
|
|
33
|
+
import {Delay} from '../types'
|
|
30
34
|
import {TooltipArrow} from './tooltipArrow'
|
|
35
|
+
import {useTooltipDelayGroup} from './tooltipDelayGroup'
|
|
31
36
|
|
|
32
37
|
/**
|
|
33
38
|
* @public
|
|
@@ -45,6 +50,14 @@ export interface TooltipProps extends Omit<LayerProps, 'as'> {
|
|
|
45
50
|
portal?: boolean | string
|
|
46
51
|
scheme?: ThemeColorSchemeKey
|
|
47
52
|
shadow?: number | number[]
|
|
53
|
+
/**
|
|
54
|
+
* @public Adds a delay to open or close the tooltip. Defaults to 0.
|
|
55
|
+
*
|
|
56
|
+
* If only a `number` is passed, it will be used for both opening and closing.
|
|
57
|
+
*
|
|
58
|
+
* If an object `{open: number; close:number}` is passed, it can be used to set different delays for each action.
|
|
59
|
+
*/
|
|
60
|
+
delay?: Delay
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
const Root = styled(Layer)`
|
|
@@ -72,6 +85,7 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
72
85
|
scheme,
|
|
73
86
|
shadow = 2,
|
|
74
87
|
zOffset = theme.sanity.layer?.tooltip.zOffset,
|
|
88
|
+
delay,
|
|
75
89
|
...restProps
|
|
76
90
|
} = props
|
|
77
91
|
const fallbackPlacements = useArrayProp(fallbackPlacementsProp)
|
|
@@ -90,12 +104,25 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
90
104
|
fallbackPlacements,
|
|
91
105
|
padding: 4,
|
|
92
106
|
rootBoundary,
|
|
107
|
+
mainAxis: false,
|
|
93
108
|
}),
|
|
94
109
|
)
|
|
95
110
|
|
|
96
111
|
// Define distance between reference and floating element
|
|
97
112
|
ret.push(offset({mainAxis: 3}))
|
|
98
113
|
|
|
114
|
+
// Set width and height on the floating element
|
|
115
|
+
ret.push(
|
|
116
|
+
size({
|
|
117
|
+
apply({availableWidth, availableHeight, elements}) {
|
|
118
|
+
Object.assign(elements.floating.style, {
|
|
119
|
+
maxWidth: `${availableWidth - 4 * 2}px`, // the padding is `4px`
|
|
120
|
+
maxHeight: `${availableHeight - 4 * 2}px`, // the padding is `4px`
|
|
121
|
+
})
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
)
|
|
125
|
+
|
|
99
126
|
// Shift the tooltip so its sits with the boundary eleement
|
|
100
127
|
ret.push(
|
|
101
128
|
shift({
|
|
@@ -111,21 +138,12 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
111
138
|
return ret
|
|
112
139
|
}, [boundaryElement, fallbackPlacements])
|
|
113
140
|
|
|
114
|
-
const {
|
|
141
|
+
const {floatingStyles, placement, middlewareData, refs, update} = useFloating({
|
|
115
142
|
middleware,
|
|
116
143
|
placement: placementProp,
|
|
117
144
|
whileElementsMounted: autoUpdate,
|
|
118
145
|
})
|
|
119
146
|
|
|
120
|
-
const rootStyle: CSSProperties = useMemo(
|
|
121
|
-
() => ({
|
|
122
|
-
position: strategy,
|
|
123
|
-
top: y ?? 0,
|
|
124
|
-
left: x ?? 0,
|
|
125
|
-
}),
|
|
126
|
-
[strategy, x, y],
|
|
127
|
-
)
|
|
128
|
-
|
|
129
147
|
const staticSide = placement && FLOATING_STATIC_SIDES[placement.split('-')[0]]
|
|
130
148
|
|
|
131
149
|
const arrowX = middlewareData.arrow?.x
|
|
@@ -144,11 +162,45 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
144
162
|
return style
|
|
145
163
|
}, [arrowX, arrowY, staticSide])
|
|
146
164
|
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
165
|
+
const tooltipId = useId()
|
|
166
|
+
const [isOpen, setIsOpen] = useDelayedState(false)
|
|
167
|
+
const delayGroupContext = useTooltipDelayGroup()
|
|
168
|
+
const showTooltip = isOpen || delayGroupContext?.openTooltipId === tooltipId
|
|
169
|
+
|
|
170
|
+
const isInsideGroup = delayGroupContext !== null
|
|
171
|
+
const openDelayProp = typeof delay === 'number' ? delay : delay?.open || 0
|
|
172
|
+
const closeDelayProp = typeof delay === 'number' ? delay : delay?.close || 0
|
|
173
|
+
|
|
174
|
+
const openDelay = isInsideGroup ? delayGroupContext.openDelay : openDelayProp
|
|
175
|
+
const closeDelay = isInsideGroup ? delayGroupContext.closeDelay : closeDelayProp
|
|
176
|
+
|
|
177
|
+
const handleIsOpenChange = useCallback(
|
|
178
|
+
(open: boolean) => {
|
|
179
|
+
if (isInsideGroup) {
|
|
180
|
+
// When it's inside a group, the open or close status will be handled by the group.
|
|
181
|
+
if (open) {
|
|
182
|
+
delayGroupContext.setIsGroupActive(open, openDelay)
|
|
183
|
+
delayGroupContext.setOpenTooltipId(tooltipId, openDelay)
|
|
184
|
+
} else {
|
|
185
|
+
const minimumGroupDeactivateDelay = 200 // We should provide some delay to allow the user to reach the next tooltip.
|
|
186
|
+
const groupDeactivateDelay =
|
|
187
|
+
closeDelay > minimumGroupDeactivateDelay ? closeDelay : minimumGroupDeactivateDelay
|
|
188
|
+
|
|
189
|
+
delayGroupContext.setIsGroupActive(open, groupDeactivateDelay)
|
|
190
|
+
delayGroupContext.setOpenTooltipId(null, closeDelay)
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
// When it's not inside a group, the open or close status will be handled by the tooltip itself.
|
|
194
|
+
setIsOpen(open, open ? openDelay : closeDelay)
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
[isInsideGroup, delayGroupContext, openDelay, tooltipId, closeDelay, setIsOpen],
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
const handleBlur = useCallback(() => handleIsOpenChange(false), [handleIsOpenChange])
|
|
201
|
+
const handleFocus = useCallback(() => handleIsOpenChange(true), [handleIsOpenChange])
|
|
202
|
+
const handleMouseEnter = useCallback(() => handleIsOpenChange(true), [handleIsOpenChange])
|
|
203
|
+
const handleMouseLeave = useCallback(() => handleIsOpenChange(false), [handleIsOpenChange])
|
|
152
204
|
|
|
153
205
|
// Detect whether the mouse is moving outside of the reference element. This is sometimes
|
|
154
206
|
// necessary, because the tooltip might not always close as it should (e.g. when clicking
|
|
@@ -164,7 +216,7 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
164
216
|
(event.target instanceof Node && referenceElement.contains(event.target))
|
|
165
217
|
|
|
166
218
|
if (!isHoveringReference) {
|
|
167
|
-
|
|
219
|
+
handleIsOpenChange(false)
|
|
168
220
|
window.removeEventListener('mousemove', handleWindowMouseMove)
|
|
169
221
|
}
|
|
170
222
|
}
|
|
@@ -174,17 +226,17 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
174
226
|
return () => {
|
|
175
227
|
window.removeEventListener('mousemove', handleWindowMouseMove)
|
|
176
228
|
}
|
|
177
|
-
}, [isOpen, referenceElement])
|
|
229
|
+
}, [isOpen, referenceElement, handleIsOpenChange])
|
|
178
230
|
|
|
179
231
|
// Close when `disabled` changes to `true`
|
|
180
232
|
useEffect(() => {
|
|
181
|
-
if (disabled)
|
|
182
|
-
}, [disabled])
|
|
233
|
+
if (disabled) handleIsOpenChange(false)
|
|
234
|
+
}, [disabled, handleIsOpenChange])
|
|
183
235
|
|
|
184
236
|
// Close when `content` changes to falsy
|
|
185
237
|
useEffect(() => {
|
|
186
|
-
if (!content)
|
|
187
|
-
}, [content])
|
|
238
|
+
if (!content) handleIsOpenChange(false)
|
|
239
|
+
}, [content, handleIsOpenChange])
|
|
188
240
|
|
|
189
241
|
// Update reference
|
|
190
242
|
useEffect(() => refs.setReference(referenceElement), [referenceElement, refs])
|
|
@@ -238,7 +290,13 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
238
290
|
if (disabled) return child
|
|
239
291
|
|
|
240
292
|
const root = (
|
|
241
|
-
<Root
|
|
293
|
+
<Root
|
|
294
|
+
data-ui="Tooltip"
|
|
295
|
+
{...restProps}
|
|
296
|
+
ref={setFloating}
|
|
297
|
+
style={floatingStyles}
|
|
298
|
+
zOffset={zOffset}
|
|
299
|
+
>
|
|
242
300
|
<Card
|
|
243
301
|
data-ui="Tooltip__card"
|
|
244
302
|
data-placement={placement}
|
|
@@ -257,7 +315,7 @@ export const Tooltip = forwardRef(function Tooltip(
|
|
|
257
315
|
<>
|
|
258
316
|
{child}
|
|
259
317
|
|
|
260
|
-
{
|
|
318
|
+
{showTooltip && (
|
|
261
319
|
<>
|
|
262
320
|
{portal ? (
|
|
263
321
|
<Portal __unstable_name={typeof portal === 'string' ? portal : undefined}>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {createContext} from 'react'
|
|
2
|
+
import {globalScope} from '../../../lib/globalScope'
|
|
3
|
+
import {TooltipDelayGroupContextValue} from './types'
|
|
4
|
+
|
|
5
|
+
const key = Symbol.for('@sanity/ui/context/tooltipDelayGroup')
|
|
6
|
+
|
|
7
|
+
globalScope[key] = globalScope[key] || createContext<TooltipDelayGroupContextValue | null>(null)
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @beta
|
|
11
|
+
*/
|
|
12
|
+
export const TooltipDelayGroupContext: React.Context<TooltipDelayGroupContextValue | null> =
|
|
13
|
+
globalScope[key]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {useMemo} from 'react'
|
|
2
|
+
import {useDelayedState} from '../../../hooks/useDelayedState'
|
|
3
|
+
import {Delay} from '../../types'
|
|
4
|
+
import {TooltipDelayGroupContext} from './tooltipDelayGroupContext'
|
|
5
|
+
import {TooltipDelayGroupContextValue} from './types'
|
|
6
|
+
import {useTooltipDelayGroup} from './useTooltipDelayGroup'
|
|
7
|
+
/**
|
|
8
|
+
* @public
|
|
9
|
+
* */
|
|
10
|
+
export interface TooltipDelayGroupProviderProps {
|
|
11
|
+
children?: React.ReactNode
|
|
12
|
+
/**
|
|
13
|
+
* @public Handles the delays to open or close a tooltip inside a group
|
|
14
|
+
*
|
|
15
|
+
* If only a `number` is passed, it will be used for both opening and closing.
|
|
16
|
+
*
|
|
17
|
+
* If an object `{open: number; close:number}` is passed, it can be used to set different delays for each action.
|
|
18
|
+
*/
|
|
19
|
+
delay: Delay
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @public
|
|
24
|
+
* Provides context for a group of tooltip elements that should share a delay
|
|
25
|
+
* which temporarily becomes 1 ms after the first floating element of the group opens.
|
|
26
|
+
*/
|
|
27
|
+
export function TooltipDelayGroupProvider(
|
|
28
|
+
props: TooltipDelayGroupProviderProps,
|
|
29
|
+
): React.ReactElement {
|
|
30
|
+
const {children, delay} = props
|
|
31
|
+
const [isGroupActive, setIsGroupActive] = useDelayedState(false)
|
|
32
|
+
const [openTooltipId, setOpenTooltipId] = useDelayedState<string | null>(null)
|
|
33
|
+
|
|
34
|
+
const isInsideContext = useTooltipDelayGroup()
|
|
35
|
+
|
|
36
|
+
if (isInsideContext) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
'TooltipDelayGroupProvider cannot be nested inside another TooltipDelayGroupProvider',
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const openDelay = typeof delay === 'number' ? delay : delay?.open || 0
|
|
43
|
+
const closeDelay = typeof delay === 'number' ? delay : delay?.close || 0
|
|
44
|
+
|
|
45
|
+
const value: TooltipDelayGroupContextValue = useMemo(
|
|
46
|
+
() => ({
|
|
47
|
+
isGroupActive: isGroupActive,
|
|
48
|
+
setIsGroupActive: setIsGroupActive,
|
|
49
|
+
openTooltipId: openTooltipId,
|
|
50
|
+
setOpenTooltipId: setOpenTooltipId,
|
|
51
|
+
// When the group is active, we want the next tooltip to open immediately.
|
|
52
|
+
openDelay: isGroupActive ? 1 : openDelay,
|
|
53
|
+
closeDelay: closeDelay,
|
|
54
|
+
}),
|
|
55
|
+
[closeDelay, isGroupActive, openDelay, openTooltipId, setIsGroupActive, setOpenTooltipId],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<TooltipDelayGroupContext.Provider value={value}>{children}</TooltipDelayGroupContext.Provider>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beta
|
|
3
|
+
*/
|
|
4
|
+
export interface TooltipDelayGroupContextValue {
|
|
5
|
+
isGroupActive: boolean
|
|
6
|
+
setIsGroupActive: (nextState: React.SetStateAction<boolean>, delay?: number | undefined) => void
|
|
7
|
+
setOpenTooltipId: (nextId: string | null, delay?: number | undefined) => void
|
|
8
|
+
openDelay: number
|
|
9
|
+
closeDelay: number
|
|
10
|
+
openTooltipId: string | null
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {useContext} from 'react'
|
|
2
|
+
import {TooltipDelayGroupContext} from './tooltipDelayGroupContext'
|
|
3
|
+
import {TooltipDelayGroupContextValue} from './types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @beta
|
|
7
|
+
*/
|
|
8
|
+
export function useTooltipDelayGroup(): TooltipDelayGroupContextValue | null {
|
|
9
|
+
const value = useContext(TooltipDelayGroupContext)
|
|
10
|
+
|
|
11
|
+
return value
|
|
12
|
+
}
|
package/src/primitives/types.ts
CHANGED
|
@@ -129,3 +129,13 @@ export interface ResponsiveShadowProps {
|
|
|
129
129
|
export interface ResponsiveWidthProps {
|
|
130
130
|
width?: number | 'auto' | (number | 'auto')[]
|
|
131
131
|
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @beta
|
|
135
|
+
*/
|
|
136
|
+
export type Delay =
|
|
137
|
+
| number
|
|
138
|
+
| Partial<{
|
|
139
|
+
open: number
|
|
140
|
+
close: number
|
|
141
|
+
}>
|