@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.
@@ -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 {x, y, placement, middlewareData, refs, update, strategy} = useFloating({
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 [isOpen, setIsOpen] = useState(false)
148
- const handleBlur = useCallback(() => setIsOpen(false), [])
149
- const handleFocus = useCallback(() => setIsOpen(true), [])
150
- const handleMouseEnter = useCallback(() => setIsOpen(true), [])
151
- const handleMouseLeave = useCallback(() => setIsOpen(false), [])
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
- setIsOpen(false)
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) setIsOpen(false)
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) setIsOpen(false)
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 data-ui="Tooltip" {...restProps} ref={setFloating} style={rootStyle} zOffset={zOffset}>
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
- {isOpen && (
318
+ {showTooltip && (
261
319
  <>
262
320
  {portal ? (
263
321
  <Portal __unstable_name={typeof portal === 'string' ? portal : undefined}>
@@ -0,0 +1,4 @@
1
+ export * from './useTooltipDelayGroup'
2
+ export * from './tooltipDelayGroupContext'
3
+ export * from './tooltipDelayGroupProvider'
4
+ export * from './types'
@@ -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
+ }
@@ -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
+ }>