@sanity/ui 1.4.0 → 1.6.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,52 +8,65 @@ import {
8
8
  offset,
9
9
  shift,
10
10
  useFloating,
11
- UseFloatingProps,
12
11
  } from '@floating-ui/react-dom'
13
- import {cloneElement, forwardRef, memo, useCallback, useEffect, useMemo, useRef} from 'react'
14
- import {useForwardedRef, useArrayProp, useElementSize} from '../../hooks'
12
+ import {
13
+ MutableRefObject,
14
+ RefCallback,
15
+ cloneElement,
16
+ forwardRef,
17
+ memo,
18
+ useCallback,
19
+ useEffect,
20
+ useMemo,
21
+ useRef,
22
+ } from 'react'
23
+ import {useForwardedRef, useArrayProp, useElementSize, useMediaIndex} from '../../hooks'
15
24
  import {ThemeColorSchemeKey, useTheme} from '../../theme'
16
25
  import {BoxOverflow, CardTone, Placement, PopoverMargins} from '../../types'
17
26
  import {LayerProps, LayerProvider, Portal, useBoundaryElement} from '../../utils'
18
- import {ResponsiveRadiusProps, ResponsiveShadowProps, ResponsiveWidthProps} from '../types'
27
+ import {ResponsiveRadiusProps, ResponsiveShadowProps} from '../types'
19
28
  import {
20
29
  DEFAULT_POPOVER_DISTANCE,
21
30
  DEFAULT_POPOVER_MARGINS,
22
31
  DEFAULT_POPOVER_PADDING,
23
32
  } from './constants'
24
33
  import {size} from './floating-ui/size'
34
+ import {calcCurrentWidth, calcMaxWidth} from './helpers'
25
35
  import {PopoverCard} from './popoverCard'
26
- import {useOnForcedUpdate} from './useOnForcedUpdate'
36
+ import {PopoverUpdateCallback, PopoverWidth} from './types'
27
37
 
28
- /**
29
- * @public
30
- */
38
+ /** @public */
31
39
  export interface PopoverProps
32
40
  extends Omit<LayerProps, 'as'>,
33
41
  ResponsiveRadiusProps,
34
- ResponsiveShadowProps,
35
- ResponsiveWidthProps {
36
- /**
37
- * @beta
38
- */
42
+ ResponsiveShadowProps {
43
+ /** @beta */
39
44
  __unstable_margins?: PopoverMargins
40
45
  arrow?: boolean
46
+ /** @deprecated Use `floatingBoundary` and/or `referenceBoundary` instead */
41
47
  boundaryElement?: HTMLElement | null
42
48
  children?: React.ReactElement
43
49
  constrainSize?: boolean
44
50
  content?: React.ReactNode
45
51
  disabled?: boolean
46
52
  fallbackPlacements?: Placement[]
53
+ floatingBoundary?: HTMLElement | null
54
+ matchReferenceWidth?: boolean
47
55
  open?: boolean
48
56
  overflow?: BoxOverflow
49
57
  padding?: number | number[]
50
58
  placement?: Placement
51
59
  portal?: boolean | string
52
60
  preventOverflow?: boolean
61
+ referenceBoundary?: HTMLElement | null
53
62
  referenceElement?: HTMLElement | null
54
- matchReferenceWidth?: boolean
55
63
  scheme?: ThemeColorSchemeKey
56
64
  tone?: CardTone
65
+ /** @beta */
66
+ updateRef?:
67
+ | MutableRefObject<PopoverUpdateCallback | undefined>
68
+ | RefCallback<PopoverUpdateCallback | undefined>
69
+ width?: PopoverWidth | PopoverWidth[]
57
70
  }
58
71
 
59
72
  /** @public */
@@ -75,7 +88,8 @@ export const Popover = memo(
75
88
  content,
76
89
  disabled,
77
90
  fallbackPlacements,
78
- matchReferenceWidth: matchReferenceWidthProp,
91
+ matchReferenceWidth,
92
+ floatingBoundary = props.boundaryElement ?? boundaryElementContext.element,
79
93
  open,
80
94
  overflow = 'hidden',
81
95
  padding: paddingProp,
@@ -83,24 +97,73 @@ export const Popover = memo(
83
97
  portal,
84
98
  preventOverflow = true,
85
99
  radius: radiusProp = 3,
100
+ referenceBoundary = props.boundaryElement ?? boundaryElementContext.element,
86
101
  referenceElement,
87
102
  scheme,
88
103
  shadow: shadowProp = 3,
89
104
  tone = 'inherit',
90
105
  width: widthProp = 'auto',
91
106
  zOffset: zOffsetProp = theme.sanity.layer?.popover.zOffset,
107
+ updateRef,
92
108
  ...restProps
93
109
  } = props
94
110
  const boundarySize = useElementSize(boundaryElement)?.border
95
111
  const padding = useArrayProp(paddingProp)
96
112
  const radius = useArrayProp(radiusProp)
97
113
  const shadow = useArrayProp(shadowProp)
98
- const width = useArrayProp(widthProp)
114
+ const widthArrayProp = useArrayProp(widthProp)
99
115
  const zOffset = useArrayProp(zOffsetProp)
100
116
  const forwardedRef = useForwardedRef(ref)
101
117
  const arrowRef = useRef<HTMLDivElement | null>(null)
102
118
  const rootBoundary: RootBoundary = 'viewport'
103
119
 
120
+ const mediaIndex = useMediaIndex()
121
+ const boundaryWidth = constrainSize || preventOverflow ? boundarySize?.width : undefined
122
+
123
+ // Update width when
124
+ // - media index changes
125
+ // - `width` property changes
126
+ const width = calcCurrentWidth({mediaIndex, theme, width: widthArrayProp})
127
+ const widthRef = useRef(width)
128
+
129
+ useEffect(() => {
130
+ widthRef.current = width
131
+ }, [width])
132
+
133
+ // Update max width when
134
+ // - boundary width changes
135
+ // - `width` property changes
136
+ const maxWidth = calcMaxWidth({boundaryWidth, currentWidth: width})
137
+ const maxWidthRef = useRef(maxWidth)
138
+
139
+ useEffect(() => {
140
+ maxWidthRef.current = maxWidth
141
+ }, [maxWidth])
142
+
143
+ // Keep track of reference element width (see `size` middleware below)
144
+ const referenceWidthRef = useRef<number>()
145
+
146
+ // Force apply width & max width to floating element
147
+ useEffect(() => {
148
+ const floatingElement = forwardedRef.current
149
+
150
+ if (!open || !floatingElement) return
151
+
152
+ const referenceWidth = referenceWidthRef.current
153
+
154
+ if (matchReferenceWidth) {
155
+ if (referenceWidth !== undefined) {
156
+ floatingElement.style.width = `${referenceWidth}px`
157
+ }
158
+ } else if (width !== undefined) {
159
+ floatingElement.style.width = `${width}px`
160
+ }
161
+
162
+ if (typeof maxWidth === 'number') {
163
+ floatingElement.style.maxWidth = `${maxWidth}px`
164
+ }
165
+ }, [width, forwardedRef, matchReferenceWidth, maxWidth, open])
166
+
104
167
  const middleware = useMemo(() => {
105
168
  const ret: Middleware[] = []
106
169
 
@@ -108,7 +171,7 @@ export const Popover = memo(
108
171
  if (constrainSize || preventOverflow) {
109
172
  ret.push(
110
173
  flip({
111
- boundary: boundaryElement || undefined,
174
+ boundary: floatingBoundary || undefined,
112
175
  fallbackPlacements,
113
176
  padding: DEFAULT_POPOVER_PADDING,
114
177
  rootBoundary,
@@ -124,23 +187,36 @@ export const Popover = memo(
124
187
  )
125
188
 
126
189
  // Track sizes
127
- if (constrainSize || matchReferenceWidthProp) {
190
+ if (constrainSize || matchReferenceWidth) {
128
191
  ret.push(
129
192
  size({
130
193
  apply({availableWidth, availableHeight, elements, referenceWidth}) {
131
- if (matchReferenceWidthProp) {
194
+ // not fresh, so use refs
195
+
196
+ referenceWidthRef.current = referenceWidth
197
+
198
+ const _currentWidth = widthRef.current
199
+ const _maxWidth = maxWidthRef.current
200
+
201
+ if (matchReferenceWidth) {
132
202
  elements.floating.style.width = `${referenceWidth}px`
203
+ } else if (_currentWidth !== undefined) {
204
+ elements.floating.style.width = `${_currentWidth}px`
133
205
  }
134
206
 
135
207
  if (constrainSize) {
136
- elements.floating.style.maxWidth = `${availableWidth}px`
208
+ elements.floating.style.maxWidth = `${Math.min(
209
+ availableWidth,
210
+ _maxWidth ?? Infinity
211
+ )}px`
212
+
137
213
  elements.floating.style.maxHeight = `${availableHeight}px`
138
214
  }
139
215
  },
140
- boundaryElement,
216
+ boundaryElement: floatingBoundary || undefined,
141
217
  constrainSize,
142
218
  margins,
143
- matchReferenceWidth: matchReferenceWidthProp,
219
+ matchReferenceWidth,
144
220
  padding: DEFAULT_POPOVER_PADDING,
145
221
  })
146
222
  )
@@ -150,7 +226,7 @@ export const Popover = memo(
150
226
  if (preventOverflow) {
151
227
  ret.push(
152
228
  shift({
153
- boundary: boundaryElement || undefined,
229
+ boundary: floatingBoundary || undefined,
154
230
  rootBoundary,
155
231
  padding: DEFAULT_POPOVER_PADDING,
156
232
  })
@@ -169,7 +245,7 @@ export const Popover = memo(
169
245
 
170
246
  ret.push(
171
247
  hide({
172
- boundary: boundaryElement || undefined,
248
+ boundary: referenceBoundary || undefined,
173
249
  padding: DEFAULT_POPOVER_PADDING,
174
250
  strategy: 'referenceHidden',
175
251
  })
@@ -178,37 +254,20 @@ export const Popover = memo(
178
254
  return ret
179
255
  }, [
180
256
  arrowProp,
181
- boundaryElement,
182
257
  constrainSize,
183
258
  fallbackPlacements,
259
+ floatingBoundary,
184
260
  margins,
185
- matchReferenceWidthProp,
261
+ matchReferenceWidth,
186
262
  preventOverflow,
263
+ referenceBoundary,
187
264
  ])
188
265
 
189
- const floatingProps: UseFloatingProps = useMemo(
190
- () => ({
191
- middleware,
192
- placement: placementProp,
193
- whileElementsMounted: autoUpdate,
194
- }),
195
- [middleware, placementProp]
196
- )
197
-
198
- const {
199
- x,
200
- y,
201
- placement,
202
- reference: referenceRef,
203
- floating: floatingRef,
204
- update: floatingUpdate,
205
- middlewareData,
206
- strategy,
207
- } = useFloating(floatingProps)
208
-
209
- const handleForcedUpdate = useCallback(() => {
210
- floatingUpdate()
211
- }, [floatingUpdate])
266
+ const {x, y, middlewareData, placement, refs, strategy, update} = useFloating({
267
+ middleware,
268
+ placement: placementProp,
269
+ whileElementsMounted: autoUpdate,
270
+ })
212
271
 
213
272
  const referenceHidden = middlewareData.hide?.referenceHidden
214
273
 
@@ -222,14 +281,14 @@ export const Popover = memo(
222
281
  const setFloating = useCallback(
223
282
  (node: HTMLDivElement | null) => {
224
283
  forwardedRef.current = node
225
- floatingRef(node)
284
+ refs.setFloating(node)
226
285
  },
227
- [floatingRef, forwardedRef]
286
+ [forwardedRef, refs]
228
287
  )
229
288
 
230
289
  const setReference = useCallback(
231
290
  (node: HTMLElement | null) => {
232
- referenceRef(node)
291
+ refs.setReference(node)
233
292
 
234
293
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
235
294
  const childRef = (childProp as any)?.ref
@@ -240,7 +299,7 @@ export const Popover = memo(
240
299
  childRef.current = node
241
300
  }
242
301
  },
243
- [childProp, referenceRef]
302
+ [childProp, refs]
244
303
  )
245
304
 
246
305
  const child = useMemo(() => {
@@ -250,10 +309,18 @@ export const Popover = memo(
250
309
  }, [childProp, referenceElement, setReference])
251
310
 
252
311
  useEffect(() => {
253
- referenceRef(referenceElement || null)
254
- }, [referenceRef, referenceElement])
312
+ if (updateRef) {
313
+ if (typeof updateRef === 'function') {
314
+ updateRef(update)
315
+ } else if (updateRef) {
316
+ updateRef.current = update
317
+ }
318
+ }
319
+ }, [update, updateRef])
255
320
 
256
- useOnForcedUpdate(handleForcedUpdate)
321
+ useEffect(() => {
322
+ refs.setReference(referenceElement || null)
323
+ }, [referenceElement, refs])
257
324
 
258
325
  if (disabled) {
259
326
  return childProp || <></>
@@ -268,7 +335,6 @@ export const Popover = memo(
268
335
  arrowRef={setArrow}
269
336
  arrowX={arrowX}
270
337
  arrowY={arrowY}
271
- boundaryWidth={preventOverflow ? boundarySize?.width : undefined}
272
338
  hidden={referenceHidden}
273
339
  overflow={overflow}
274
340
  padding={padding}
@@ -279,9 +345,9 @@ export const Popover = memo(
279
345
  shadow={shadow}
280
346
  strategy={strategy}
281
347
  tone={tone}
348
+ width={matchReferenceWidth ? referenceWidthRef.current : width}
282
349
  x={x}
283
350
  y={y}
284
- width={width}
285
351
  >
286
352
  {content}
287
353
  </PopoverCard>
@@ -1,45 +1,23 @@
1
1
  import {Strategy} from '@floating-ui/react-dom'
2
2
  import React, {CSSProperties, forwardRef, memo, useMemo} from 'react'
3
- import styled, {CSSObject} from 'styled-components'
3
+ import styled from 'styled-components'
4
4
  import {FLOATING_STATIC_SIDES} from '../../constants'
5
- import {ThemeProps} from '../../styles'
6
5
  import {ThemeColorSchemeKey} from '../../theme'
7
6
  import {BoxOverflow, CardTone, Placement, PopoverMargins} from '../../types'
8
7
  import {useLayer} from '../../utils'
9
8
  import {Card} from '../card'
10
- import {Container} from '../container'
11
- import {
12
- DEFAULT_POPOVER_ARROW_WIDTH,
13
- DEFAULT_POPOVER_MARGINS,
14
- DEFAULT_POPOVER_PADDING,
15
- } from './constants'
9
+ import {Flex} from '../flex'
10
+ import {DEFAULT_POPOVER_ARROW_WIDTH, DEFAULT_POPOVER_MARGINS} from './constants'
16
11
  import {PopoverArrow} from './popoverArrow'
17
12
 
18
- function popoverCardStyle(props: {$boundaryWidth?: number} & ThemeProps): CSSObject {
19
- const {$boundaryWidth} = props
20
-
21
- return {
22
- '&:not([hidden])': {
23
- display: 'flex',
24
- },
25
-
26
- flexDirection: 'column',
27
-
28
- width: 'max-content',
29
- minWidth: 'min-content',
30
- maxWidth:
31
- typeof $boundaryWidth === 'number'
32
- ? `${$boundaryWidth - DEFAULT_POPOVER_PADDING * 2}px`
33
- : undefined,
34
- }
35
- }
36
-
37
- const Root = memo(styled(Card)(popoverCardStyle))
38
-
39
- const PopoverContainer = memo(styled(Container)`
40
- max-height: inherit;
41
- max-width: inherit;
42
- `)
13
+ const Root = styled(Card)({
14
+ '&:not([hidden])': {
15
+ display: 'flex',
16
+ },
17
+ flexDirection: 'column',
18
+ width: 'max-content',
19
+ minWidth: 'min-content',
20
+ })
43
21
 
44
22
  /**
45
23
  * @internal
@@ -53,7 +31,6 @@ export const PopoverCard = memo(
53
31
  arrowRef: React.Ref<HTMLDivElement>
54
32
  arrowX?: number
55
33
  arrowY?: number
56
- boundaryWidth?: number
57
34
  overflow?: BoxOverflow
58
35
  padding?: number | number[]
59
36
  placement?: Placement
@@ -62,7 +39,7 @@ export const PopoverCard = memo(
62
39
  shadow?: number | number[]
63
40
  strategy: Strategy
64
41
  tone: CardTone
65
- width?: number | 'auto' | (number | 'auto')[]
42
+ width: number | undefined
66
43
  x: number | null
67
44
  y: number | null
68
45
  } & Omit<React.HTMLProps<HTMLDivElement>, 'as' | 'height' | 'width'>,
@@ -74,7 +51,6 @@ export const PopoverCard = memo(
74
51
  arrowRef,
75
52
  arrowX,
76
53
  arrowY,
77
- boundaryWidth,
78
54
  children,
79
55
  padding,
80
56
  placement,
@@ -85,7 +61,7 @@ export const PopoverCard = memo(
85
61
  strategy,
86
62
  style,
87
63
  tone,
88
- width = 'auto',
64
+ width,
89
65
  x: xProp,
90
66
  y: yProp,
91
67
  ...restProps
@@ -108,10 +84,11 @@ export const PopoverCard = memo(
108
84
  position: strategy,
109
85
  top: y,
110
86
  left: x,
87
+ width,
111
88
  zIndex,
112
89
  ...style,
113
90
  }),
114
- [strategy, style, x, y, zIndex]
91
+ [strategy, style, width, x, y, zIndex]
115
92
  )
116
93
 
117
94
  const staticSide = placement && FLOATING_STATIC_SIDES[placement.split('-')[0]]
@@ -131,10 +108,9 @@ export const PopoverCard = memo(
131
108
 
132
109
  return (
133
110
  <Root
111
+ data-ui="Popover"
134
112
  {...restProps}
135
- $boundaryWidth={boundaryWidth}
136
113
  data-placement={placement}
137
- data-ui="Popover"
138
114
  radius={radius}
139
115
  ref={ref}
140
116
  scheme={scheme}
@@ -143,16 +119,11 @@ export const PopoverCard = memo(
143
119
  style={rootStyle}
144
120
  tone={tone}
145
121
  >
146
- <PopoverContainer
147
- data-ui="Popover__wrapper"
148
- flex={1}
149
- overflow={overflow}
150
- padding={padding}
151
- sizing="border"
152
- width={width}
153
- >
154
- {children}
155
- </PopoverContainer>
122
+ <Flex data-ui="Popover__wrapper" direction="column" flex={1} overflow={overflow}>
123
+ <Flex direction="column" flex={1} padding={padding}>
124
+ {children}
125
+ </Flex>
126
+ </Flex>
156
127
 
157
128
  {arrow && <PopoverArrow ref={arrowRef} style={arrowStyle} />}
158
129
  </Root>
@@ -1,8 +1,6 @@
1
- /**
2
- * @public
3
- */
4
- export interface PopoverContextValue {
5
- version: number
6
- forceUpdate: () => void
7
- lastUpdateId: string | null
8
- }
1
+ /** @beta */
2
+ export type PopoverUpdateCallback = () => void
3
+
4
+ /** @public */
5
+ // TODO: remove `auto` from PopoverWidth
6
+ export type PopoverWidth = number | 'auto'
@@ -110,7 +110,7 @@ export const Tooltip = forwardRef(function Tooltip(
110
110
  return ret
111
111
  }, [boundaryElement, fallbackPlacements])
112
112
 
113
- const {x, y, placement, reference, floating, middlewareData, update, strategy} = useFloating({
113
+ const {x, y, placement, middlewareData, refs, update, strategy} = useFloating({
114
114
  middleware,
115
115
  placement: placementProp,
116
116
  whileElementsMounted: autoUpdate,
@@ -186,7 +186,7 @@ export const Tooltip = forwardRef(function Tooltip(
186
186
  }, [content])
187
187
 
188
188
  // Update reference
189
- useEffect(() => reference(referenceElement), [reference, referenceElement])
189
+ useEffect(() => refs.setReference(referenceElement), [referenceElement, refs])
190
190
 
191
191
  const setArrow = useCallback(
192
192
  (arrowEl: HTMLDivElement | null) => {
@@ -199,9 +199,9 @@ export const Tooltip = forwardRef(function Tooltip(
199
199
  const setFloating = useCallback(
200
200
  (node: HTMLDivElement | null) => {
201
201
  forwardedRef.current = node
202
- floating(node)
202
+ refs.setFloating(node)
203
203
  },
204
- [floating, forwardedRef]
204
+ [forwardedRef, refs]
205
205
  )
206
206
 
207
207
  const childRef: ForwardedRef<HTMLElement | null> = (childProp as any)?.ref
@@ -1,103 +0,0 @@
1
- import {useBoolean} from '@sanity/ui-workshop'
2
- import {useCallback, useMemo, useState} from 'react'
3
- import {BoundaryElementProvider} from '../../../utils'
4
- import {Button} from '../../button'
5
- import {Card} from '../../card'
6
- import {Flex} from '../../flex'
7
- import {Text} from '../../text'
8
- import {Popover, PopoverProps} from '../popover'
9
- import {PopoverProvider} from '../popoverProvider'
10
- import {usePopover} from '../usePopover'
11
-
12
- function PopoverComponent({
13
- referenceElement,
14
- boundaryElement,
15
- }: {
16
- referenceElement: PopoverProps['referenceElement']
17
- boundaryElement: PopoverProps['boundaryElement']
18
- }) {
19
- return (
20
- <Popover
21
- open
22
- content={
23
- <Card padding={5} radius={3}>
24
- <Text weight="semibold" muted>
25
- I should follow my reference element <br />
26
- when the parent container is resized.
27
- </Text>
28
- </Card>
29
- }
30
- referenceElement={referenceElement}
31
- boundaryElement={boundaryElement}
32
- />
33
- )
34
- }
35
-
36
- export default function PositionStory() {
37
- return (
38
- <PopoverProvider>
39
- <InnerStory />
40
- </PopoverProvider>
41
- )
42
- }
43
-
44
- function InnerStory() {
45
- const [size, setSize] = useState(0.5)
46
- const [referenceElement, setReferenceElement] = useState<PopoverProps['referenceElement']>(null)
47
- const [boundaryElement, setBoundaryElement] = useState<PopoverProps['boundaryElement']>(null)
48
- const updatePositionOnClick = useBoolean('Force popover update on size change', false, 'Props')
49
- const {forceUpdate} = usePopover()
50
-
51
- // Random number between 0.2 and 5
52
- const handleSetSize = useCallback(() => {
53
- const s = Math.random() * (0.5 - 0.2) + 0.2
54
-
55
- setSize(s)
56
-
57
- if (updatePositionOnClick) {
58
- forceUpdate()
59
- }
60
- }, [updatePositionOnClick])
61
-
62
- const element = useMemo(() => {
63
- return (
64
- <Card
65
- as="span"
66
- border
67
- radius={2}
68
- style={{display: 'inline', padding: '0.4, 1'}}
69
- ref={setReferenceElement}
70
- tone="primary"
71
- >
72
- nunc
73
- </Card>
74
- )
75
- }, [])
76
-
77
- return (
78
- <>
79
- <PopoverComponent referenceElement={referenceElement} boundaryElement={boundaryElement} />
80
-
81
- <BoundaryElementProvider element={boundaryElement as HTMLDivElement}>
82
- <Flex height="fill">
83
- <Card borderRight flex={size} tone="transparent">
84
- <Flex align="center" justify="center" height="fill">
85
- <Button text="Change size" onClick={handleSetSize} />
86
- </Flex>
87
- </Card>
88
-
89
- <Card flex={1} ref={setBoundaryElement}>
90
- <Flex height="fill" padding={5}>
91
- <Text>
92
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ultricies, nisl ut
93
- aliquet ultricies, nunc sapien ultricies tortor, nec tincidunt elit risus ac nunc.
94
- Nulla facilisi. Sed aliquam, {element} eget aliquam ultricies, diam diam aliquam
95
- augue, eget ultricies nisl nunc eget nunc. Nulla facilisi. Sed aliquam,
96
- </Text>
97
- </Flex>
98
- </Card>
99
- </Flex>
100
- </BoundaryElementProvider>
101
- </>
102
- )
103
- }