@sanity/ui 1.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/ui",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "sideEffects": false,
5
5
  "types": "./dist/index.d.ts",
6
6
  "source": "./src/index.ts",
@@ -49,7 +49,7 @@
49
49
  "workshop:start": "http-server -a localhost -c-0 -p 1337 -s -P http://localhost:1337/index.html? dist"
50
50
  },
51
51
  "dependencies": {
52
- "@floating-ui/react-dom": "1.1.1",
52
+ "@floating-ui/react-dom": "2.0.0",
53
53
  "@sanity/color": "^2.2.5",
54
54
  "@sanity/icons": "^2.3.1",
55
55
  "csstype": "^3.1.2",
@@ -57,10 +57,10 @@
57
57
  "react-refractor": "^2.1.7"
58
58
  },
59
59
  "devDependencies": {
60
- "@babel/core": "^7.22.1",
61
- "@babel/preset-env": "^7.22.4",
62
- "@babel/preset-react": "^7.22.3",
63
- "@babel/preset-typescript": "^7.21.5",
60
+ "@babel/core": "^7.22.5",
61
+ "@babel/preset-env": "^7.22.5",
62
+ "@babel/preset-react": "^7.22.5",
63
+ "@babel/preset-typescript": "^7.22.5",
64
64
  "@commitlint/cli": "^17.6.5",
65
65
  "@commitlint/config-conventional": "^17.6.5",
66
66
  "@juggle/resize-observer": "^3.4.0",
@@ -72,7 +72,7 @@
72
72
  "@testing-library/react": "^14.0.0",
73
73
  "@types/jest": "^29.5.2",
74
74
  "@types/jest-axe": "^3.5.5",
75
- "@types/node": "^18.16.16",
75
+ "@types/node": "^20.2.5",
76
76
  "@types/react": "^18.2.9",
77
77
  "@types/react-dom": "^18.2.4",
78
78
  "@types/react-is": "^18.2.0",
@@ -52,6 +52,7 @@ export default function AlignedStory() {
52
52
  constrainSize={constrainSize}
53
53
  content={content}
54
54
  open={open}
55
+ overflow="auto"
55
56
  padding={3}
56
57
  portal={portal}
57
58
  placement={placement}
@@ -0,0 +1,86 @@
1
+ import {useSelect} from '@sanity/ui-workshop'
2
+ import {useCallback, useEffect, useRef, useState} from 'react'
3
+ import {BoundaryElementProvider} from '../../../utils'
4
+ import {Box} from '../../box'
5
+ import {Card} from '../../card'
6
+ import {Flex} from '../../flex'
7
+ import {Stack} from '../../stack'
8
+ import {Text} from '../../text'
9
+ import {Popover, PopoverProps, PopoverUpdateCallback} from '../popover'
10
+
11
+ const SIDE_PANEL_WIDTH = {
12
+ sm: 300,
13
+ md: 400,
14
+ lg: 500,
15
+ xl: 600,
16
+ }
17
+
18
+ export default function SidePanelStory() {
19
+ const sidePanelWidth = useSelect('Side panel width', SIDE_PANEL_WIDTH, SIDE_PANEL_WIDTH.md)
20
+ const [sidePanel, setSidePanel] = useState<HTMLDivElement | null>(null)
21
+ const updateRef = useRef<PopoverUpdateCallback>()
22
+
23
+ useEffect(() => updateRef.current?.(), [sidePanelWidth])
24
+
25
+ return (
26
+ <Flex height="fill">
27
+ <Card flex={1}>
28
+ <Box padding={4}>
29
+ <Text>
30
+ This story shows that popovers respect their boundary when used in a side panel.
31
+ </Text>
32
+ </Box>
33
+ </Card>
34
+ <BoundaryElementProvider element={sidePanel}>
35
+ <Card borderLeft flex="none" ref={setSidePanel} style={{width: sidePanelWidth}} width={0}>
36
+ <Stack padding={4} space={5}>
37
+ <Text muted size={1}>
38
+ Click the <code>reference</code> text below to toggle the popover.
39
+ </Text>
40
+
41
+ <Card border padding={3}>
42
+ <Text size={1}>
43
+ Some editor <InlineObject updateRef={updateRef} />
44
+ </Text>
45
+ </Card>
46
+ </Stack>
47
+ </Card>
48
+ </BoundaryElementProvider>
49
+ </Flex>
50
+ )
51
+ }
52
+
53
+ function InlineObject(props: {updateRef?: PopoverProps['updateRef']}) {
54
+ const {updateRef} = props
55
+ const [open, setOpen] = useState(false)
56
+
57
+ const handleClick = useCallback(() => {
58
+ setOpen((prev) => !prev)
59
+ }, [])
60
+
61
+ return (
62
+ <Popover
63
+ content={
64
+ <Box padding={3}>
65
+ <Text size={1}>
66
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum
67
+ has been the industry's standard dummy text ever since the 1500s, when an unknown
68
+ printer took a galley of type and scrambled it to make a type specimen book. It has
69
+ survived not only five centuries, but also the leap into electronic typesetting,
70
+ remaining essentially unchanged. It was popularised in the 1960s with the release of
71
+ Letraset sheets containing Lorem Ipsum passages, and more recently with desktop
72
+ publishing software like Aldus PageMaker including versions of Lorem Ipsum.
73
+ </Text>
74
+ </Box>
75
+ }
76
+ constrainSize
77
+ open={open}
78
+ overflow="auto"
79
+ portal
80
+ width={0}
81
+ updateRef={updateRef}
82
+ >
83
+ <code onClick={handleClick}>reference</code>
84
+ </Popover>
85
+ )
86
+ }
@@ -1,5 +1,5 @@
1
- import {useBoolean, useSelect, useText} from '@sanity/ui-workshop'
2
- import {ReactElement, useState} from 'react'
1
+ import {useAction, useBoolean, useSelect, useText} from '@sanity/ui-workshop'
2
+ import {ReactElement, useRef, useState} from 'react'
3
3
  import {
4
4
  WORKSHOP_CONTAINER_WIDTH_OPTIONS,
5
5
  WORKSHOP_PLACEMENT_OPTIONS,
@@ -10,10 +10,13 @@ import {Button} from '../../button'
10
10
  import {Card} from '../../card'
11
11
  import {Text} from '../../text'
12
12
  import {Popover} from '../popover'
13
+ import {PopoverUpdateCallback} from '../types'
13
14
 
14
15
  export default function TestStory(): ReactElement {
15
16
  const [portalElement, setPortalElement] = useState<HTMLDivElement | null>(null)
16
17
  const [boundaryElement, setBoundaryElement] = useState<HTMLDivElement | null>(null)
18
+
19
+ const mount = useBoolean('Mount', true)
17
20
  const arrow = useBoolean('Arrow', true)
18
21
  const constrainSize = useBoolean('Constrain size', true)
19
22
  const matchReferenceWidth = useBoolean('Match reference width', false)
@@ -26,6 +29,10 @@ export default function TestStory(): ReactElement {
26
29
 
27
30
  const text = useText('Text', 'Test')
28
31
 
32
+ const updateRef = useRef<PopoverUpdateCallback>()
33
+
34
+ const button = <Button text="reference" style={{width: referenceWide ? 300 : undefined}} />
35
+
29
36
  return (
30
37
  <Card
31
38
  ref={setBoundaryElement}
@@ -56,22 +63,27 @@ export default function TestStory(): ReactElement {
56
63
  <div style={{padding: '150vh'}}>
57
64
  <PortalProvider element={portalElement}>
58
65
  <BoundaryElementProvider element={boundaryElement}>
59
- <Popover
60
- arrow={arrow}
61
- content={<Text size={1}>{text}</Text>}
62
- constrainSize={constrainSize}
63
- fallbackPlacements={['left', 'bottom', 'right', 'top']}
64
- matchReferenceWidth={matchReferenceWidth}
65
- open={open}
66
- padding={3}
67
- placement={placement}
68
- portal
69
- preventOverflow={preventOverflow}
70
- radius={radius}
71
- width={width}
72
- >
73
- <Button text="reference" style={{width: referenceWide ? 300 : undefined}} />
74
- </Popover>
66
+ {!mount && button}
67
+ {mount && (
68
+ <Popover
69
+ arrow={arrow}
70
+ content={<Text>{text}</Text>}
71
+ constrainSize={constrainSize}
72
+ fallbackPlacements={['left', 'bottom', 'right', 'top']}
73
+ matchReferenceWidth={matchReferenceWidth}
74
+ open={open}
75
+ overflow="hidden"
76
+ padding={3}
77
+ placement={placement}
78
+ portal
79
+ preventOverflow={preventOverflow}
80
+ radius={radius}
81
+ updateRef={updateRef}
82
+ width={width}
83
+ >
84
+ {button}
85
+ </Popover>
86
+ )}
75
87
  </BoundaryElementProvider>
76
88
  </PortalProvider>
77
89
  </div>
@@ -40,5 +40,10 @@ export default defineScope({
40
40
  title: 'Open on mount',
41
41
  component: lazy(() => import('./OpenOnMountStory')),
42
42
  },
43
+ {
44
+ name: 'side-panel',
45
+ title: 'Side panel',
46
+ component: lazy(() => import('./SidePanelStory')),
47
+ },
43
48
  ],
44
49
  })
@@ -0,0 +1,32 @@
1
+ import {Theme} from '../../theme'
2
+ import {DEFAULT_POPOVER_PADDING} from './constants'
3
+ import {PopoverWidth} from './types'
4
+
5
+ export function calcCurrentWidth(params: {
6
+ mediaIndex: number
7
+ theme: Theme
8
+ width: PopoverWidth[]
9
+ }): number | undefined {
10
+ const {mediaIndex, theme, width} = params
11
+
12
+ const w = width[mediaIndex]
13
+ const currentWidth: PopoverWidth | undefined = w === undefined ? width[width.length - 1] : w
14
+
15
+ return typeof currentWidth === 'number' ? theme.sanity.container[currentWidth] : undefined
16
+ }
17
+
18
+ export function calcMaxWidth(params: {
19
+ boundaryWidth: number | undefined
20
+ currentWidth: number | undefined
21
+ }): number | undefined {
22
+ const {boundaryWidth, currentWidth} = params
23
+
24
+ if (currentWidth === undefined && boundaryWidth === undefined) {
25
+ return undefined
26
+ }
27
+
28
+ return Math.min(
29
+ currentWidth ?? Infinity,
30
+ (boundaryWidth || Infinity) - DEFAULT_POPOVER_PADDING * 2
31
+ )
32
+ }
@@ -1 +1,2 @@
1
1
  export * from './popover'
2
+ export * from './types'
@@ -8,51 +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'
36
+ import {PopoverUpdateCallback, PopoverWidth} from './types'
26
37
 
27
- /**
28
- * @public
29
- */
38
+ /** @public */
30
39
  export interface PopoverProps
31
40
  extends Omit<LayerProps, 'as'>,
32
41
  ResponsiveRadiusProps,
33
- ResponsiveShadowProps,
34
- ResponsiveWidthProps {
35
- /**
36
- * @beta
37
- */
42
+ ResponsiveShadowProps {
43
+ /** @beta */
38
44
  __unstable_margins?: PopoverMargins
39
45
  arrow?: boolean
46
+ /** @deprecated Use `floatingBoundary` and/or `referenceBoundary` instead */
40
47
  boundaryElement?: HTMLElement | null
41
48
  children?: React.ReactElement
42
49
  constrainSize?: boolean
43
50
  content?: React.ReactNode
44
51
  disabled?: boolean
45
52
  fallbackPlacements?: Placement[]
53
+ floatingBoundary?: HTMLElement | null
54
+ matchReferenceWidth?: boolean
46
55
  open?: boolean
47
56
  overflow?: BoxOverflow
48
57
  padding?: number | number[]
49
58
  placement?: Placement
50
59
  portal?: boolean | string
51
60
  preventOverflow?: boolean
61
+ referenceBoundary?: HTMLElement | null
52
62
  referenceElement?: HTMLElement | null
53
- matchReferenceWidth?: boolean
54
63
  scheme?: ThemeColorSchemeKey
55
64
  tone?: CardTone
65
+ /** @beta */
66
+ updateRef?:
67
+ | MutableRefObject<PopoverUpdateCallback | undefined>
68
+ | RefCallback<PopoverUpdateCallback | undefined>
69
+ width?: PopoverWidth | PopoverWidth[]
56
70
  }
57
71
 
58
72
  /** @public */
@@ -74,7 +88,8 @@ export const Popover = memo(
74
88
  content,
75
89
  disabled,
76
90
  fallbackPlacements,
77
- matchReferenceWidth: matchReferenceWidthProp,
91
+ matchReferenceWidth,
92
+ floatingBoundary = props.boundaryElement ?? boundaryElementContext.element,
78
93
  open,
79
94
  overflow = 'hidden',
80
95
  padding: paddingProp,
@@ -82,24 +97,73 @@ export const Popover = memo(
82
97
  portal,
83
98
  preventOverflow = true,
84
99
  radius: radiusProp = 3,
100
+ referenceBoundary = props.boundaryElement ?? boundaryElementContext.element,
85
101
  referenceElement,
86
102
  scheme,
87
103
  shadow: shadowProp = 3,
88
104
  tone = 'inherit',
89
105
  width: widthProp = 'auto',
90
106
  zOffset: zOffsetProp = theme.sanity.layer?.popover.zOffset,
107
+ updateRef,
91
108
  ...restProps
92
109
  } = props
93
110
  const boundarySize = useElementSize(boundaryElement)?.border
94
111
  const padding = useArrayProp(paddingProp)
95
112
  const radius = useArrayProp(radiusProp)
96
113
  const shadow = useArrayProp(shadowProp)
97
- const width = useArrayProp(widthProp)
114
+ const widthArrayProp = useArrayProp(widthProp)
98
115
  const zOffset = useArrayProp(zOffsetProp)
99
116
  const forwardedRef = useForwardedRef(ref)
100
117
  const arrowRef = useRef<HTMLDivElement | null>(null)
101
118
  const rootBoundary: RootBoundary = 'viewport'
102
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
+
103
167
  const middleware = useMemo(() => {
104
168
  const ret: Middleware[] = []
105
169
 
@@ -107,7 +171,7 @@ export const Popover = memo(
107
171
  if (constrainSize || preventOverflow) {
108
172
  ret.push(
109
173
  flip({
110
- boundary: boundaryElement || undefined,
174
+ boundary: floatingBoundary || undefined,
111
175
  fallbackPlacements,
112
176
  padding: DEFAULT_POPOVER_PADDING,
113
177
  rootBoundary,
@@ -123,23 +187,36 @@ export const Popover = memo(
123
187
  )
124
188
 
125
189
  // Track sizes
126
- if (constrainSize || matchReferenceWidthProp) {
190
+ if (constrainSize || matchReferenceWidth) {
127
191
  ret.push(
128
192
  size({
129
193
  apply({availableWidth, availableHeight, elements, referenceWidth}) {
130
- 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) {
131
202
  elements.floating.style.width = `${referenceWidth}px`
203
+ } else if (_currentWidth !== undefined) {
204
+ elements.floating.style.width = `${_currentWidth}px`
132
205
  }
133
206
 
134
207
  if (constrainSize) {
135
- elements.floating.style.maxWidth = `${availableWidth}px`
208
+ elements.floating.style.maxWidth = `${Math.min(
209
+ availableWidth,
210
+ _maxWidth ?? Infinity
211
+ )}px`
212
+
136
213
  elements.floating.style.maxHeight = `${availableHeight}px`
137
214
  }
138
215
  },
139
- boundaryElement,
216
+ boundaryElement: floatingBoundary || undefined,
140
217
  constrainSize,
141
218
  margins,
142
- matchReferenceWidth: matchReferenceWidthProp,
219
+ matchReferenceWidth,
143
220
  padding: DEFAULT_POPOVER_PADDING,
144
221
  })
145
222
  )
@@ -149,7 +226,7 @@ export const Popover = memo(
149
226
  if (preventOverflow) {
150
227
  ret.push(
151
228
  shift({
152
- boundary: boundaryElement || undefined,
229
+ boundary: floatingBoundary || undefined,
153
230
  rootBoundary,
154
231
  padding: DEFAULT_POPOVER_PADDING,
155
232
  })
@@ -168,7 +245,7 @@ export const Popover = memo(
168
245
 
169
246
  ret.push(
170
247
  hide({
171
- boundary: boundaryElement || undefined,
248
+ boundary: referenceBoundary || undefined,
172
249
  padding: DEFAULT_POPOVER_PADDING,
173
250
  strategy: 'referenceHidden',
174
251
  })
@@ -177,32 +254,20 @@ export const Popover = memo(
177
254
  return ret
178
255
  }, [
179
256
  arrowProp,
180
- boundaryElement,
181
257
  constrainSize,
182
258
  fallbackPlacements,
259
+ floatingBoundary,
183
260
  margins,
184
- matchReferenceWidthProp,
261
+ matchReferenceWidth,
185
262
  preventOverflow,
263
+ referenceBoundary,
186
264
  ])
187
265
 
188
- const floatingProps: UseFloatingProps = useMemo(
189
- () => ({
190
- middleware,
191
- placement: placementProp,
192
- whileElementsMounted: autoUpdate,
193
- }),
194
- [middleware, placementProp]
195
- )
196
-
197
- const {
198
- x,
199
- y,
200
- placement,
201
- reference: referenceRef,
202
- floating: floatingRef,
203
- middlewareData,
204
- strategy,
205
- } = useFloating(floatingProps)
266
+ const {x, y, middlewareData, placement, refs, strategy, update} = useFloating({
267
+ middleware,
268
+ placement: placementProp,
269
+ whileElementsMounted: autoUpdate,
270
+ })
206
271
 
207
272
  const referenceHidden = middlewareData.hide?.referenceHidden
208
273
 
@@ -216,14 +281,14 @@ export const Popover = memo(
216
281
  const setFloating = useCallback(
217
282
  (node: HTMLDivElement | null) => {
218
283
  forwardedRef.current = node
219
- floatingRef(node)
284
+ refs.setFloating(node)
220
285
  },
221
- [floatingRef, forwardedRef]
286
+ [forwardedRef, refs]
222
287
  )
223
288
 
224
289
  const setReference = useCallback(
225
290
  (node: HTMLElement | null) => {
226
- referenceRef(node)
291
+ refs.setReference(node)
227
292
 
228
293
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
229
294
  const childRef = (childProp as any)?.ref
@@ -234,7 +299,7 @@ export const Popover = memo(
234
299
  childRef.current = node
235
300
  }
236
301
  },
237
- [childProp, referenceRef]
302
+ [childProp, refs]
238
303
  )
239
304
 
240
305
  const child = useMemo(() => {
@@ -244,8 +309,18 @@ export const Popover = memo(
244
309
  }, [childProp, referenceElement, setReference])
245
310
 
246
311
  useEffect(() => {
247
- referenceRef(referenceElement || null)
248
- }, [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])
320
+
321
+ useEffect(() => {
322
+ refs.setReference(referenceElement || null)
323
+ }, [referenceElement, refs])
249
324
 
250
325
  if (disabled) {
251
326
  return childProp || <></>
@@ -260,7 +335,6 @@ export const Popover = memo(
260
335
  arrowRef={setArrow}
261
336
  arrowX={arrowX}
262
337
  arrowY={arrowY}
263
- boundaryWidth={preventOverflow ? boundarySize?.width : undefined}
264
338
  hidden={referenceHidden}
265
339
  overflow={overflow}
266
340
  padding={padding}
@@ -271,9 +345,9 @@ export const Popover = memo(
271
345
  shadow={shadow}
272
346
  strategy={strategy}
273
347
  tone={tone}
348
+ width={matchReferenceWidth ? referenceWidthRef.current : width}
274
349
  x={x}
275
350
  y={y}
276
- width={width}
277
351
  >
278
352
  {content}
279
353
  </PopoverCard>