@sanity/ui 0.38.3-next.30 → 0.38.3-next.36

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.
@@ -1,22 +1,27 @@
1
- import {Box, Button, Card, Flex, Text, Tooltip} from '@sanity/ui'
2
- import {useSelect, useText} from '@sanity/ui-workshop'
3
- import {WORKSHOP_PLACEMENT_OPTIONS} from '../../../__workshop__/constants'
1
+ import {Button, Card, Flex, Text, Tooltip} from '@sanity/ui'
2
+ import {useBoolean, useSelect, useText} from '@sanity/ui-workshop'
3
+ import {
4
+ WORKSHOP_PLACEMENT_OPTIONS,
5
+ WORKSHOP_SHADOW_OPTIONS,
6
+ WORKSHOP_SPACE_OPTIONS,
7
+ } from '../../../__workshop__/constants'
4
8
 
5
9
  export default function PropsStory() {
6
10
  const content = useText('Content', 'Tooltip content')
11
+ const padding = useSelect('Padding', WORKSHOP_SPACE_OPTIONS, 2, 'Props')
7
12
  const placement = useSelect('Placement', WORKSHOP_PLACEMENT_OPTIONS, 'top')
13
+ const portal = useBoolean('Portal', true)
14
+ const shadow = useSelect('Shadow', WORKSHOP_SHADOW_OPTIONS, 2)
8
15
 
9
16
  return (
10
17
  <Card height="fill">
11
18
  <Flex align="center" height="fill" justify="center" padding={4} sizing="border">
12
19
  <Tooltip
13
- content={
14
- <Box padding={2}>
15
- <Text size={1}>{content}</Text>
16
- </Box>
17
- }
20
+ content={<Text size={1}>{content}</Text>}
21
+ padding={padding}
18
22
  placement={placement}
19
- portal
23
+ portal={portal}
24
+ shadow={shadow}
20
25
  >
21
26
  <Button mode="bleed" text="Hover me" />
22
27
  </Tooltip>
@@ -1,6 +1,26 @@
1
- import {cloneElement, forwardRef, useCallback, useEffect, useRef, useState} from 'react'
2
- import {usePopper} from 'react-popper'
1
+ import {
2
+ arrow,
3
+ autoUpdate,
4
+ flip,
5
+ offset,
6
+ shift,
7
+ useFloating,
8
+ Middleware,
9
+ RootBoundary,
10
+ } from '@floating-ui/react-dom'
11
+ import {
12
+ cloneElement,
13
+ forwardRef,
14
+ useCallback,
15
+ useEffect,
16
+ useMemo,
17
+ useRef,
18
+ useState,
19
+ CSSProperties,
20
+ ForwardedRef,
21
+ } from 'react'
3
22
  import styled from 'styled-components'
23
+ import {FLOATING_STATIC_SIDES} from '../../constants'
4
24
  import {useArrayProp, useForwardedRef} from '../../hooks'
5
25
  import {ThemeColorSchemeKey, useTheme} from '../../theme'
6
26
  import {Placement} from '../../types'
@@ -12,15 +32,18 @@ import {TooltipArrow} from './tooltipArrow'
12
32
  * @public
13
33
  */
14
34
  export interface TooltipProps extends Omit<LayerProps, 'as'> {
35
+ /** @deprecated Use `fallbackPlacements` instead. */
15
36
  allowedAutoPlacements?: Placement[]
16
37
  boundaryElement?: HTMLElement | null
17
38
  children?: React.ReactElement
18
39
  content?: React.ReactNode
19
40
  disabled?: boolean
20
41
  fallbackPlacements?: Placement[]
42
+ padding?: number | number[]
21
43
  placement?: Placement
22
44
  portal?: boolean | string
23
45
  scheme?: ThemeColorSchemeKey
46
+ shadow?: number | number[]
24
47
  }
25
48
 
26
49
  const Root = styled(Layer)`
@@ -37,55 +60,89 @@ export const Tooltip = forwardRef(function Tooltip(
37
60
  const boundaryElementContext = useBoundaryElement()
38
61
  const theme = useTheme()
39
62
  const {
40
- allowedAutoPlacements,
41
63
  boundaryElement = boundaryElementContext?.element,
42
- children,
64
+ children: childProp,
43
65
  content,
44
66
  disabled,
45
67
  fallbackPlacements: fallbackPlacementsProp,
46
- placement = 'bottom',
68
+ padding,
69
+ placement: placementProp = 'bottom',
47
70
  portal,
48
71
  scheme,
72
+ shadow = 2,
49
73
  zOffset = theme.sanity.layer?.tooltip.zOffset,
50
74
  ...restProps
51
75
  } = props
52
76
  const fallbackPlacements = useArrayProp(fallbackPlacementsProp)
53
77
  const forwardedRef = useForwardedRef(ref)
54
78
  const [referenceElement, setReferenceElement] = useState<HTMLElement | null>(null)
55
- const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null)
56
- const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null)
57
- const popper = usePopper(referenceElement, popperElement, {
58
- placement,
59
- modifiers: [
60
- {
61
- name: 'arrow',
62
- options: {
63
- element: arrowElement,
64
- padding: 4,
65
- },
66
- },
67
- {
68
- name: 'preventOverflow',
69
- options: {
70
- altAxis: true,
71
- boundary: boundaryElement || undefined,
72
- padding: 4,
73
- },
74
- },
75
- {
76
- name: 'offset',
77
- options: {offset: [0, 3]},
78
- },
79
- {
80
- name: 'flip',
81
- options: {
82
- allowedAutoPlacements,
83
- fallbackPlacements,
84
- },
85
- },
86
- ],
79
+ const arrowRef = useRef<HTMLDivElement | null>(null)
80
+ const rootBoundary: RootBoundary = 'viewport'
81
+
82
+ const middleware = useMemo(() => {
83
+ const ret: Middleware[] = []
84
+
85
+ // Flip the floating element when leaving the boundary box
86
+ ret.push(
87
+ flip({
88
+ boundary: boundaryElement || undefined,
89
+ fallbackPlacements,
90
+ padding: 4,
91
+ rootBoundary,
92
+ })
93
+ )
94
+
95
+ // Define distance between reference and floating element
96
+ ret.push(offset({mainAxis: 3}))
97
+
98
+ // Shift the tooltip so its sits with the boundary eleement
99
+ ret.push(
100
+ shift({
101
+ boundary: boundaryElement || undefined,
102
+ rootBoundary,
103
+ padding: 4,
104
+ })
105
+ )
106
+
107
+ // Place arrow
108
+ ret.push(arrow({element: arrowRef, padding: 2}))
109
+
110
+ return ret
111
+ }, [boundaryElement, fallbackPlacements])
112
+
113
+ const {x, y, placement, reference, floating, middlewareData, update, strategy} = useFloating({
114
+ middleware,
115
+ placement: placementProp,
116
+ whileElementsMounted: autoUpdate,
87
117
  })
88
- const {forceUpdate} = popper
118
+
119
+ const rootStyle: CSSProperties = useMemo(
120
+ () => ({
121
+ position: strategy,
122
+ top: y ?? 0,
123
+ left: x ?? 0,
124
+ }),
125
+ [strategy, x, y]
126
+ )
127
+
128
+ const staticSide = placement && FLOATING_STATIC_SIDES[placement.split('-')[0]]
129
+
130
+ const arrowX = middlewareData.arrow?.x
131
+ const arrowY = middlewareData.arrow?.y
132
+
133
+ const arrowStyle: CSSProperties = useMemo(() => {
134
+ const style: CSSProperties = {
135
+ left: arrowX !== null ? arrowX : undefined,
136
+ top: arrowY !== null ? arrowY : undefined,
137
+ right: undefined,
138
+ bottom: undefined,
139
+ }
140
+
141
+ if (staticSide) style[staticSide] = -15
142
+
143
+ return style
144
+ }, [arrowX, arrowY, staticSide])
145
+
89
146
  const [isOpen, setIsOpen] = useState(false)
90
147
  const handleBlur = useCallback(() => setIsOpen(false), [])
91
148
  const handleFocus = useCallback(() => setIsOpen(true), [])
@@ -118,25 +175,6 @@ export const Tooltip = forwardRef(function Tooltip(
118
175
  }
119
176
  }, [isOpen, referenceElement])
120
177
 
121
- const updateTimeoutRef = useRef<NodeJS.Timeout | null>(null)
122
-
123
- useEffect(() => {
124
- if (updateTimeoutRef.current) {
125
- clearTimeout(updateTimeoutRef.current)
126
- updateTimeoutRef.current = null
127
- }
128
-
129
- updateTimeoutRef.current = setTimeout(() => {
130
- if (forceUpdate) {
131
- try {
132
- forceUpdate()
133
- } catch (_) {
134
- // ignore caught error
135
- }
136
- }
137
- }, 0)
138
- }, [forceUpdate, content])
139
-
140
178
  // Close when `disabled` changes to `true`
141
179
  useEffect(() => {
142
180
  if (disabled) setIsOpen(false)
@@ -147,54 +185,86 @@ export const Tooltip = forwardRef(function Tooltip(
147
185
  if (!content) setIsOpen(false)
148
186
  }, [content])
149
187
 
150
- const setRef = (el: HTMLDivElement | null) => {
151
- setPopperElement(el)
152
- forwardedRef.current = el
153
- }
154
-
155
- if (!children) return <></>
156
-
157
- if (disabled) return children
158
-
159
- const referenceProps = {
160
- onBlur: handleBlur,
161
- onFocus: handleFocus,
162
- onMouseEnter: handleMouseEnter,
163
- onMouseLeave: handleMouseLeave,
164
- ref: setReferenceElement,
165
- }
166
-
167
- const referenceNode = cloneElement(children, referenceProps)
168
-
169
- const popperNode = (
170
- <Root
171
- data-ui="Tooltip"
172
- {...restProps}
173
- {...popper.attributes.popper}
174
- ref={setRef}
175
- style={popper.styles.popper}
176
- zOffset={zOffset}
177
- >
178
- <Card radius={2} scheme={scheme} shadow={3}>
188
+ // Update reference
189
+ useEffect(() => reference(referenceElement), [reference, referenceElement])
190
+
191
+ const setArrow = useCallback(
192
+ (arrowEl: HTMLDivElement | null) => {
193
+ arrowRef.current = arrowEl
194
+ update()
195
+ },
196
+ [update]
197
+ )
198
+
199
+ const setFloating = useCallback(
200
+ (node: HTMLDivElement | null) => {
201
+ forwardedRef.current = node
202
+ floating(node)
203
+ },
204
+ [floating, forwardedRef]
205
+ )
206
+
207
+ const childRef: ForwardedRef<HTMLElement | null> = (childProp as any)?.ref
208
+
209
+ const setReference = useCallback(
210
+ (node: HTMLElement | null) => {
211
+ if (typeof childRef === 'function') {
212
+ childRef(node)
213
+ } else if (childRef) {
214
+ childRef.current = node
215
+ }
216
+
217
+ // childRef.current = node
218
+ setReferenceElement(node)
219
+ },
220
+ [childRef]
221
+ )
222
+
223
+ const child = useMemo(() => {
224
+ if (!childProp) return null
225
+
226
+ return cloneElement(childProp, {
227
+ onBlur: handleBlur,
228
+ onFocus: handleFocus,
229
+ onMouseEnter: handleMouseEnter,
230
+ onMouseLeave: handleMouseLeave,
231
+ ref: setReference,
232
+ })
233
+ }, [childProp, handleBlur, handleFocus, handleMouseEnter, handleMouseLeave, setReference])
234
+
235
+ if (!child) return <></>
236
+
237
+ if (disabled) return child
238
+
239
+ const root = (
240
+ <Root data-ui="Tooltip" {...restProps} ref={setFloating} style={rootStyle} zOffset={zOffset}>
241
+ <Card
242
+ data-ui="Tooltip__card"
243
+ data-placement={placement}
244
+ padding={padding}
245
+ radius={2}
246
+ scheme={scheme}
247
+ shadow={shadow}
248
+ >
179
249
  {content}
180
- <TooltipArrow ref={setArrowElement} style={popper.styles.arrow} />
250
+ <TooltipArrow ref={setArrow} style={arrowStyle} />
181
251
  </Card>
182
252
  </Root>
183
253
  )
184
254
 
185
255
  return (
186
256
  <>
187
- {referenceNode}
257
+ {child}
188
258
 
189
259
  {isOpen && (
190
260
  <>
191
- {portal && (
261
+ {portal ? (
192
262
  <Portal __unstable_name={typeof portal === 'string' ? portal : undefined}>
193
- {popperNode}
263
+ {root}
194
264
  </Portal>
265
+ ) : (
266
+ root
195
267
  )}
196
-
197
- {!portal && popperNode}
198
268
  </>
199
269
  )}
200
270
  </>
@@ -3,6 +3,21 @@ import styled from 'styled-components'
3
3
 
4
4
  const Root = styled.div`
5
5
  position: absolute;
6
+ pointer-events: none;
7
+ width: 15px;
8
+ height: 15px;
9
+ fill: none;
10
+
11
+ :empty + & {
12
+ display: none;
13
+ }
14
+
15
+ & > svg {
16
+ display: block;
17
+ transform-origin: 7.5px 7.5px;
18
+ }
19
+
20
+ /* position: absolute;
6
21
  width: 15px;
7
22
  height: 15px;
8
23
  fill: none;
@@ -16,30 +31,30 @@ const Root = styled.div`
16
31
  display: block;
17
32
  }
18
33
  transform-origin: 7.5px 7.5px;
19
- }
34
+ } */
20
35
 
21
- [data-popper-placement^='top'] > div > & {
22
- bottom: -15px;
36
+ [data-placement^='top'] > & {
37
+ bottom: -27px;
23
38
  }
24
39
 
25
- [data-popper-placement^='right'] > div > & {
26
- left: -15px;
40
+ [data-placement^='right'] > & {
41
+ left: -27px;
27
42
 
28
43
  & > svg {
29
44
  transform: rotate(90deg);
30
45
  }
31
46
  }
32
47
 
33
- [data-popper-placement^='left'] > div > & {
34
- right: -15px;
48
+ [data-placement^='left'] > & {
49
+ right: -27px;
35
50
 
36
51
  & > svg {
37
52
  transform: rotate(-90deg);
38
53
  }
39
54
  }
40
55
 
41
- [data-popper-placement^='bottom'] > div > & {
42
- top: -15px;
56
+ [data-placement^='bottom'] > & {
57
+ top: -27px;
43
58
 
44
59
  & > svg {
45
60
  transform: rotate(180deg);
@@ -64,7 +79,7 @@ export const TooltipArrow = forwardRef(function TooltipArrow(
64
79
  const {...restProps} = props
65
80
 
66
81
  return (
67
- <Root {...restProps} ref={ref}>
82
+ <Root data-ui="Tooltip__arrow" {...restProps} ref={ref}>
68
83
  <svg width="15" height="15" viewBox="0 0 15 15">
69
84
  <Border d="M11.5266 1C11.032 1.32802 10.5837 1.73105 10.1995 2.20057L9.04792 3.6081C8.24771 4.58614 6.7523 4.58614 5.95209 3.6081L4.80047 2.20057C4.41632 1.73105 3.96796 1.32802 3.47341 1H0.156727C1.65639 1 3.07687 1.67313 4.02651 2.83381L5.17813 4.24134C6.37844 5.70839 8.62156 5.70839 9.82187 4.24134L10.9735 2.83381C11.9231 1.67313 13.3436 1 14.8433 1H11.5266Z" />
70
85
  <Shape d="M0.156725 0C1.95632 0 3.66089 0.80776 4.80047 2.20057L5.95209 3.6081C6.75229 4.58614 8.24771 4.58614 9.04791 3.6081L10.1995 2.20057C11.3391 0.80776 13.0437 0 14.8433 0H15H0H0.156725Z" />
@@ -1,137 +0,0 @@
1
- import maxSizeModifier from 'popper-max-size-modifier'
2
- import {Modifier} from 'react-popper'
3
- import {PopoverModifiersProps} from './types'
4
-
5
- /*
6
-
7
- The order of popper phases:
8
-
9
- - beforeRead
10
- - read
11
- - afterRead
12
- - beforeMain
13
- - main
14
- - afterMain
15
- - beforeWrite
16
- - write
17
- - afterWrite
18
-
19
- */
20
-
21
- export function getPopoverModifiers(props: PopoverModifiersProps): Modifier<any, any>[] {
22
- const {
23
- allowedAutoPlacements,
24
- arrow,
25
- arrowElement,
26
- boundaryElement,
27
- constrainSize,
28
- distance,
29
- fallbackPlacements,
30
- margins,
31
- matchReferenceWidth,
32
- open,
33
- preventOverflow,
34
- skidding,
35
- tether,
36
- tetherOffset,
37
- } = props
38
-
39
- // NOTE: For later viewers:
40
- // Prior to adding this change, the popover would get `maxWidth: 0` the 2nd time it
41
- // was opened, when used with `constrainSize`. I was looking for a way to "reset"
42
- // the size of the popover as measured by Popper.js, and this seems to be a workaround.
43
- if (!open) {
44
- return []
45
- }
46
-
47
- const detectOverflowOptions = {
48
- altAxis: !constrainSize,
49
- boundary: boundaryElement || undefined,
50
- padding: 8,
51
- tether,
52
- tetherOffset,
53
- }
54
-
55
- return [
56
- constrainSize && {
57
- ...maxSizeModifier,
58
- options: detectOverflowOptions,
59
- },
60
- constrainSize && {
61
- name: 'applyMaxSize',
62
- enabled: true,
63
- phase: 'beforeWrite',
64
- requires: ['maxSize'],
65
- fn({state}: any) {
66
- const {width, height} = state.modifiersData.maxSize
67
-
68
- state.styles.popper = {
69
- ...state.styles.popper,
70
- maxWidth: `${width}px`,
71
- maxHeight: `${height}px`,
72
- }
73
- },
74
- },
75
- arrow && {
76
- name: 'arrow',
77
- options: {
78
- element: arrowElement,
79
- padding: 4,
80
- },
81
- },
82
- (constrainSize || preventOverflow) && {
83
- name: 'preventOverflow',
84
- options: detectOverflowOptions,
85
- },
86
- {
87
- name: 'offset',
88
- options: {
89
- offset: [skidding, distance],
90
- },
91
- },
92
- margins && {
93
- name: 'margins',
94
- enabled: true,
95
- phase: 'beforeRead',
96
- fn: ({state}: any) => {
97
- const {rects} = state
98
-
99
- if (rects.reference) {
100
- rects.reference.x += margins[3]
101
- rects.reference.y += margins[1]
102
- rects.reference.width -= margins[1] + margins[3]
103
- rects.reference.height -= margins[0] + margins[2]
104
- }
105
- },
106
- },
107
- {
108
- name: 'flip',
109
- options: {
110
- allowedAutoPlacements,
111
- boundary: boundaryElement || undefined,
112
- fallbackPlacements,
113
- padding: 8,
114
- },
115
- },
116
- matchReferenceWidth && {
117
- name: 'matchWidth',
118
- enabled: true,
119
- phase: 'beforeWrite',
120
- requires: ['computeStyles'],
121
- fn({state}: any) {
122
- const {width} = state.rects.reference
123
-
124
- state.styles.popper.width = `${width}px`
125
- },
126
- effect: ({state}: any) => {
127
- const refElement = state.elements.reference
128
-
129
- if (refElement instanceof HTMLElement) {
130
- state.elements.popper.style.width = `${
131
- refElement.offsetWidth - (margins ? margins[1] + margins[3] : 0)
132
- }px`
133
- }
134
- },
135
- },
136
- ].filter(Boolean) as Modifier<any, any>[]
137
- }
@@ -1 +0,0 @@
1
- export * from './usePopoverModifiers'
@@ -1,18 +0,0 @@
1
- import {Placement} from '../../../types'
2
-
3
- export interface PopoverModifiersProps {
4
- allowedAutoPlacements?: Placement[]
5
- arrow?: boolean
6
- arrowElement?: HTMLElement | null
7
- boundaryElement?: HTMLElement | null
8
- constrainSize?: boolean
9
- distance: number
10
- fallbackPlacements?: Placement[]
11
- margins?: [number, number, number, number]
12
- matchReferenceWidth?: boolean
13
- open: boolean
14
- preventOverflow?: boolean
15
- skidding: number
16
- tether?: boolean
17
- tetherOffset?: number | ((...args: any[]) => number)
18
- }
@@ -1,58 +0,0 @@
1
- import {useMemo} from 'react'
2
- import {Modifier} from 'react-popper'
3
-
4
- import {getPopoverModifiers} from './getPopoverModifiers'
5
- import {PopoverModifiersProps} from './types'
6
-
7
- export function usePopoverModifiers(props: PopoverModifiersProps): Modifier<any, any>[] {
8
- const {
9
- allowedAutoPlacements,
10
- arrow,
11
- arrowElement,
12
- boundaryElement,
13
- constrainSize,
14
- distance,
15
- fallbackPlacements,
16
- margins,
17
- matchReferenceWidth,
18
- open,
19
- preventOverflow,
20
- skidding,
21
- tether,
22
- tetherOffset,
23
- } = props
24
-
25
- return useMemo(() => {
26
- return getPopoverModifiers({
27
- allowedAutoPlacements,
28
- arrow,
29
- arrowElement,
30
- boundaryElement,
31
- constrainSize,
32
- distance,
33
- fallbackPlacements,
34
- margins,
35
- matchReferenceWidth,
36
- open,
37
- preventOverflow,
38
- skidding,
39
- tether,
40
- tetherOffset,
41
- })
42
- }, [
43
- allowedAutoPlacements,
44
- arrow,
45
- arrowElement,
46
- boundaryElement,
47
- constrainSize,
48
- distance,
49
- fallbackPlacements,
50
- margins,
51
- matchReferenceWidth,
52
- open,
53
- preventOverflow,
54
- skidding,
55
- tether,
56
- tetherOffset,
57
- ])
58
- }