@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,17 +1,30 @@
1
- import {cloneElement, forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'
2
- import {usePopper} from 'react-popper'
3
- import styled, {css} from 'styled-components'
4
- import {EMPTY_RECORD} from '../../constants'
5
- import {useArrayProp, useForwardedRef} from '../../hooks'
1
+ import {
2
+ Middleware,
3
+ arrow,
4
+ autoUpdate,
5
+ flip,
6
+ offset,
7
+ shift,
8
+ useFloating,
9
+ size as sizeMiddleware,
10
+ RootBoundary,
11
+ } from '@floating-ui/react-dom'
12
+ import {
13
+ cloneElement,
14
+ forwardRef,
15
+ memo,
16
+ useCallback,
17
+ useEffect,
18
+ useMemo,
19
+ useRef,
20
+ useState,
21
+ } from 'react'
22
+ import {useForwardedRef, useArrayProp, useElementSize} from '../../hooks'
6
23
  import {ThemeColorSchemeKey, useTheme} from '../../theme'
7
- import {CardTone, Placement, PopoverMargins} from '../../types'
8
- import {Layer, LayerProps, Portal, useBoundaryElement, usePortal} from '../../utils'
9
- import {Card} from '../card'
10
- import {ResponsiveWidthStyleProps} from '../container'
11
- import {responsiveContainerWidthStyle} from '../container/styles'
24
+ import {BoxOverflow, CardTone, Placement, PopoverMargins} from '../../types'
25
+ import {LayerProps, LayerProvider, Portal, useBoundaryElement} from '../../utils'
12
26
  import {ResponsiveRadiusProps, ResponsiveShadowProps, ResponsiveWidthProps} from '../types'
13
- import {PopoverArrow} from './arrow'
14
- import {usePopoverModifiers} from './modifiers'
27
+ import {PopoverCard} from './popoverCard'
15
28
 
16
29
  /**
17
30
  * @public
@@ -25,6 +38,7 @@ export interface PopoverProps
25
38
  * @beta
26
39
  */
27
40
  __unstable_margins?: PopoverMargins
41
+ /** @deprecated Use `fallbackPlacements` instead. */
28
42
  allowedAutoPlacements?: Placement[]
29
43
  arrow?: boolean
30
44
  boundaryElement?: HTMLElement | null
@@ -34,6 +48,7 @@ export interface PopoverProps
34
48
  disabled?: boolean
35
49
  fallbackPlacements?: Placement[]
36
50
  open?: boolean
51
+ overflow?: BoxOverflow
37
52
  padding?: number | number[]
38
53
  placement?: Placement
39
54
  portal?: boolean | string
@@ -41,217 +56,244 @@ export interface PopoverProps
41
56
  referenceElement?: HTMLElement | null
42
57
  matchReferenceWidth?: boolean
43
58
  scheme?: ThemeColorSchemeKey
59
+ /** @deprecated No longer supported. */
44
60
  tether?: boolean
61
+ /** @deprecated No longer supported. */
45
62
  tetherOffset?: number | ((...args: any[]) => number)
46
63
  tone?: CardTone
47
64
  }
48
65
 
49
- const Root = styled(Layer)<{$preventOverflow?: boolean}>(
50
- ({$preventOverflow}) => css`
51
- pointer-events: none;
52
- display: flex;
53
- flex-direction: column;
54
- max-width: calc(100% - 16px);
55
-
56
- & > * {
57
- min-height: 0;
58
- }
66
+ function size(scope: {
67
+ boundaryElement: HTMLElement | null
68
+ constrainSize: boolean
69
+ matchReferenceWidth: boolean | undefined
70
+ setAvailableWidth: (v: number) => void
71
+ setAvailableHeight: (v: number) => void
72
+ setReferenceWidth: (v: number) => void
73
+ }) {
74
+ return sizeMiddleware({
75
+ apply(args) {
76
+ if (scope.constrainSize) {
77
+ scope.setAvailableWidth(args.availableWidth)
78
+ scope.setAvailableHeight(args.availableHeight)
79
+ }
59
80
 
60
- /* Hide the popover when the reference element is out of bounds */
61
- ${$preventOverflow &&
62
- css`
63
- &[data-popper-reference-hidden='true'] {
64
- display: none;
81
+ if (scope.matchReferenceWidth) {
82
+ scope.setReferenceWidth(args.rects.reference.width)
65
83
  }
66
- `}
67
- `
68
- )
84
+ },
85
+ boundary: scope.boundaryElement || undefined,
86
+ })
87
+ }
69
88
 
70
- const PopoverCard = styled(Card)<
71
- ResponsiveWidthStyleProps & {
72
- $constrainSize?: boolean
73
- $preventOverflow?: boolean
74
- $width: (number | 'auto')[]
75
- }
76
- >(
77
- ({$constrainSize}) => css`
78
- flex: 1;
79
- max-height: ${$constrainSize && '100%'};
80
- pointer-events: all;
89
+ /** @public */
90
+ export const Popover = memo(
91
+ forwardRef(function Popover(
92
+ props: PopoverProps &
93
+ Omit<React.HTMLProps<HTMLDivElement>, 'as' | 'children' | 'content' | 'width'>,
94
+ ref: React.ForwardedRef<HTMLDivElement>
95
+ ): React.ReactElement {
96
+ const theme = useTheme()
97
+ const boundaryElementContext = useBoundaryElement()
81
98
 
82
- && {
83
- display: flex;
84
- }
99
+ const {
100
+ __unstable_margins: margins,
101
+ allowedAutoPlacements,
102
+ arrow: arrowProp = true,
103
+ boundaryElement = boundaryElementContext.element,
104
+ children: childProp,
105
+ constrainSize = false,
106
+ content,
107
+ disabled,
108
+ fallbackPlacements,
109
+ matchReferenceWidth: matchReferenceWidthProp,
110
+ open,
111
+ overflow = props.constrainSize ? 'auto' : undefined,
112
+ padding: paddingProp,
113
+ placement: placementProp = 'bottom',
114
+ portal,
115
+ preventOverflow = true,
116
+ radius: radiusProp = 3,
117
+ referenceElement,
118
+ scheme,
119
+ shadow: shadowProp = 3,
120
+ tether,
121
+ tetherOffset,
122
+ tone = 'inherit',
123
+ width: widthProp = undefined,
124
+ zOffset: zOffsetProp = theme.sanity.layer?.popover.zOffset,
125
+ ...restProps
126
+ } = props
127
+ const boundarySize = useElementSize(boundaryElement)?.border
128
+ const padding = useArrayProp(paddingProp)
129
+ const radius = useArrayProp(radiusProp)
130
+ const shadow = useArrayProp(shadowProp)
131
+ const width = useArrayProp(widthProp)
132
+ const zOffset = useArrayProp(zOffsetProp)
133
+ const [availableWidth, setAvailableWidth] = useState<number | undefined>(undefined)
134
+ const [availableHeight, setAvailableHeight] = useState<number | undefined>(undefined)
135
+ const [referenceWidth, setReferenceWidth] = useState<number | undefined>(undefined)
136
+ const forwardedRef = useForwardedRef(ref)
137
+ const arrowRef = useRef<HTMLDivElement | null>(null)
138
+ const rootBoundary: RootBoundary = 'viewport'
85
139
 
86
- flex-direction: column;
140
+ const middleware = useMemo(() => {
141
+ const ret: Middleware[] = []
87
142
 
88
- & > * {
89
- min-height: 0;
90
- }
91
-
92
- ${responsiveContainerWidthStyle}
93
- `
94
- )
143
+ // Flip the floating element when leaving the boundary box
144
+ if (constrainSize || preventOverflow) {
145
+ ret.push(
146
+ flip({
147
+ boundary: boundaryElement || undefined,
148
+ fallbackPlacements,
149
+ padding: 8,
150
+ rootBoundary,
151
+ })
152
+ )
153
+ }
95
154
 
96
- /**
97
- * @public
98
- */
99
- export const Popover = forwardRef(function Popover(
100
- props: PopoverProps &
101
- Omit<React.HTMLProps<HTMLDivElement>, 'as' | 'children' | 'content' | 'width'>,
102
- ref: React.ForwardedRef<HTMLDivElement>
103
- ) {
104
- const boundaryElementContext = useBoundaryElement()
105
- const theme = useTheme()
106
- const {
107
- __unstable_margins: margins,
108
- allowedAutoPlacements,
109
- arrow = true,
110
- boundaryElement: boundaryElementProp = boundaryElementContext.element,
111
- children: child,
112
- content,
113
- constrainSize,
114
- disabled,
115
- fallbackPlacements,
116
- open = false,
117
- padding,
118
- placement = 'bottom',
119
- portal: portalProp = false,
120
- preventOverflow,
121
- radius = 3,
122
- referenceElement: referenceElementProp,
123
- matchReferenceWidth,
124
- shadow = 3,
125
- scheme,
126
- style = EMPTY_RECORD,
127
- tether,
128
- tetherOffset,
129
- tone = 'inherit',
130
- width: widthProp = 0,
131
- zOffset = theme.sanity.layer?.popover.zOffset,
132
- ...restProps
133
- } = props
134
- const forwardedRef = useForwardedRef(ref)
135
- const portal = usePortal()
136
- const [referenceElement, setReferenceElement] = useState<HTMLElement | null>(null)
137
- const [popperElement, setPopperElement] = useState<HTMLElement | null>(null)
138
- const [arrowElement, setArrowElement] = useState<HTMLElement | null>(null)
139
- const popperReferenceElement = referenceElementProp || referenceElement
140
- const width = useArrayProp(matchReferenceWidth ? 'auto' : widthProp)
155
+ // Track sizes
156
+ ret.push(
157
+ size({
158
+ boundaryElement,
159
+ constrainSize,
160
+ matchReferenceWidth: matchReferenceWidthProp,
161
+ setAvailableHeight,
162
+ setAvailableWidth,
163
+ setReferenceWidth,
164
+ })
165
+ )
141
166
 
142
- const modifiers = usePopoverModifiers({
143
- allowedAutoPlacements,
144
- arrow,
145
- arrowElement,
146
- boundaryElement: boundaryElementProp || portal.boundaryElement,
147
- constrainSize,
148
- distance: arrow ? 4 : 0,
149
- fallbackPlacements,
150
- margins,
151
- matchReferenceWidth,
152
- open,
153
- preventOverflow,
154
- skidding: 0,
155
- tether,
156
- tetherOffset,
157
- })
167
+ // Define distance between reference and floating element
168
+ ret.push(offset({mainAxis: arrowProp ? 4 : 0}))
158
169
 
159
- const popper = usePopper(popperReferenceElement, popperElement, {
160
- placement,
161
- modifiers,
162
- })
170
+ // Shift the popover so its sits with the boundary eleement
171
+ if (preventOverflow) {
172
+ ret.push(
173
+ shift({
174
+ boundary: boundaryElement || undefined,
175
+ rootBoundary,
176
+ padding: 8,
177
+ })
178
+ )
179
+ }
163
180
 
164
- const {attributes, forceUpdate, styles} = popper
181
+ // Place arrow
182
+ if (arrowProp) {
183
+ ret.push(arrow({element: arrowRef, padding: 4}))
184
+ }
165
185
 
166
- const setRef = useCallback(
167
- (el: HTMLElement | null) => {
168
- const childRef = (child as any).ref
186
+ return ret
187
+ }, [
188
+ arrowProp,
189
+ boundaryElement,
190
+ constrainSize,
191
+ fallbackPlacements,
192
+ matchReferenceWidthProp,
193
+ preventOverflow,
194
+ ])
169
195
 
170
- setReferenceElement(el)
196
+ const {x, y, placement, reference, floating, middlewareData, strategy} = useFloating({
197
+ middleware,
198
+ placement: placementProp,
199
+ whileElementsMounted: autoUpdate,
200
+ })
171
201
 
172
- if (typeof childRef === 'function') {
173
- childRef(el)
174
- } else if (childRef) {
175
- childRef.current = el
176
- }
177
- },
178
- [child]
179
- )
202
+ const arrowX = middlewareData.arrow?.x
203
+ const arrowY = middlewareData.arrow?.y
180
204
 
181
- const setRootRef = useCallback(
182
- (el: HTMLDivElement | null) => {
183
- setPopperElement(el)
184
- forwardedRef.current = el
185
- },
186
- [forwardedRef]
187
- )
205
+ const setArrow = useCallback((arrowEl: HTMLDivElement | null) => {
206
+ arrowRef.current = arrowEl
207
+ }, [])
188
208
 
189
- const popoverStyle = useMemo(() => ({...style, ...styles.popper}), [style, styles])
209
+ const setFloating = useCallback(
210
+ (node: HTMLDivElement | null) => {
211
+ forwardedRef.current = node
212
+ floating(node)
213
+ },
214
+ [floating, forwardedRef]
215
+ )
190
216
 
191
- const updateTimeoutRef = useRef<NodeJS.Timeout | null>(null)
217
+ const setReference = useCallback(
218
+ (node: HTMLElement | null) => {
219
+ reference(node)
192
220
 
193
- useEffect(() => {
194
- if (updateTimeoutRef.current) {
195
- clearTimeout(updateTimeoutRef.current)
196
- updateTimeoutRef.current = null
197
- }
221
+ const childRef = (childProp as any)?.ref
198
222
 
199
- updateTimeoutRef.current = setTimeout(() => {
200
- if (forceUpdate) {
201
- try {
202
- forceUpdate()
203
- } catch (_) {
204
- // ignore caught error
223
+ if (typeof childRef === 'function') {
224
+ childRef(node)
225
+ } else if (childRef) {
226
+ childRef.current = node
205
227
  }
206
- }
207
- }, 0)
208
- }, [content, forceUpdate, open, popperReferenceElement])
228
+ },
229
+ [childProp, reference]
230
+ )
231
+
232
+ const child = useMemo(() => {
233
+ if (!childProp || referenceElement) return null
209
234
 
210
- if (disabled) {
211
- return child || <></>
212
- }
235
+ return cloneElement(childProp, {ref: setReference})
236
+ }, [childProp, referenceElement, setReference])
213
237
 
214
- const node = (
215
- <Root
216
- data-ui="Popover"
217
- {...restProps}
218
- $preventOverflow={preventOverflow}
219
- ref={setRootRef}
220
- style={popoverStyle}
221
- zOffset={zOffset}
222
- {...attributes.popper}
223
- >
224
- <PopoverCard
225
- $constrainSize={constrainSize}
226
- $width={width}
227
- data-ui="PopoverCard"
228
- padding={padding}
229
- radius={radius}
230
- scheme={scheme}
231
- shadow={shadow}
232
- tone={tone}
233
- >
234
- {arrow && <PopoverArrow ref={setArrowElement} style={styles.arrow} />}
235
- {content}
236
- </PopoverCard>
237
- </Root>
238
- )
238
+ useEffect(() => {
239
+ if (referenceElement) reference(referenceElement)
240
+ }, [reference, referenceElement])
241
+
242
+ if (disabled) {
243
+ return childProp || <></>
244
+ }
239
245
 
240
- return (
241
- <>
242
- {child && !referenceElementProp ? cloneElement(child, {ref: setRef}) : child || <></>}
246
+ const popover = (
247
+ <LayerProvider zOffset={zOffset}>
248
+ <PopoverCard
249
+ {...restProps}
250
+ __unstable_margins={margins}
251
+ arrow={arrowProp}
252
+ arrowRef={setArrow}
253
+ arrowX={arrowX}
254
+ arrowY={arrowY}
255
+ availableWidth={constrainSize ? availableWidth : undefined}
256
+ availableHeight={constrainSize ? availableHeight : undefined}
257
+ boundaryWidth={boundarySize?.width}
258
+ overflow={overflow}
259
+ padding={padding}
260
+ placement={placement}
261
+ radius={radius}
262
+ ref={setFloating}
263
+ referenceWidth={matchReferenceWidthProp ? referenceWidth : undefined}
264
+ scheme={scheme}
265
+ shadow={shadow}
266
+ strategy={strategy}
267
+ tone={tone}
268
+ x={x}
269
+ y={y}
270
+ width={width}
271
+ >
272
+ {content}
273
+ </PopoverCard>
274
+ </LayerProvider>
275
+ )
243
276
 
244
- {open && (
245
- <>
246
- {portalProp && (
247
- <Portal __unstable_name={typeof portalProp === 'string' ? portalProp : undefined}>
248
- {node}
249
- </Portal>
250
- )}
277
+ return (
278
+ <>
279
+ {/* the popover */}
280
+ {open && (
281
+ <>
282
+ {portal ? (
283
+ <Portal __unstable_name={typeof portal === 'string' ? portal : undefined}>
284
+ {popover}
285
+ </Portal>
286
+ ) : (
287
+ popover
288
+ )}
289
+ </>
290
+ )}
291
+
292
+ {/* the referred element */}
293
+ {child}
294
+ </>
295
+ )
296
+ })
297
+ )
251
298
 
252
- {!portalProp && node}
253
- </>
254
- )}
255
- </>
256
- )
257
- })
299
+ Popover.displayName = 'Popover'
@@ -17,11 +17,11 @@ const Root = styled.div`
17
17
  transform-origin: 13.5px 13.5px;
18
18
  }
19
19
 
20
- [data-popper-placement^='top'] > div > & {
20
+ [data-placement^='top'] > & {
21
21
  bottom: -27px;
22
22
  }
23
23
 
24
- [data-popper-placement^='right'] > div > & {
24
+ [data-placement^='right'] > & {
25
25
  left: -27px;
26
26
 
27
27
  & > svg {
@@ -29,7 +29,7 @@ const Root = styled.div`
29
29
  }
30
30
  }
31
31
 
32
- [data-popper-placement^='left'] > div > & {
32
+ [data-placement^='left'] > & {
33
33
  right: -27px;
34
34
 
35
35
  & > svg {
@@ -37,7 +37,7 @@ const Root = styled.div`
37
37
  }
38
38
  }
39
39
 
40
- [data-popper-placement^='bottom'] > div > & {
40
+ [data-placement^='bottom'] > & {
41
41
  top: -27px;
42
42
 
43
43
  & > svg {
@@ -59,7 +59,7 @@ export const PopoverArrow = forwardRef(function PopoverArrow(
59
59
  ref: React.ForwardedRef<HTMLDivElement>
60
60
  ) {
61
61
  return (
62
- <Root data-ui="PopoverArrow" {...props} ref={ref}>
62
+ <Root data-ui="Popover__arrow" {...props} ref={ref}>
63
63
  <svg
64
64
  width="27"
65
65
  height="11"
@@ -0,0 +1,160 @@
1
+ import {Strategy} from '@floating-ui/react-dom'
2
+ import React, {CSSProperties, forwardRef, memo, useMemo} from 'react'
3
+ import styled, {CSSObject} from 'styled-components'
4
+ import {FLOATING_STATIC_SIDES} from '../../constants'
5
+ import {ThemeColorSchemeKey} from '../../theme'
6
+ import {BoxOverflow, CardTone, Placement, PopoverMargins} from '../../types'
7
+ import {useLayer} from '../../utils'
8
+ import {Box} from '../box'
9
+ import {Card} from '../card'
10
+ import {PopoverArrow} from './popoverArrow'
11
+
12
+ function popoverCardStyle(props: {$boundaryWidth?: number}): CSSObject {
13
+ const {$boundaryWidth} = props
14
+
15
+ return {
16
+ maxWidth: $boundaryWidth ? `${$boundaryWidth - 16}px` : 'calc(100% - 16px)',
17
+ }
18
+ }
19
+
20
+ const Root = memo(styled(Card)(popoverCardStyle))
21
+
22
+ /**
23
+ * @internal
24
+ */
25
+ export const PopoverCard = memo(
26
+ forwardRef(function PopoverCard(
27
+ props: {
28
+ /**
29
+ * @beta
30
+ */
31
+ __unstable_margins?: PopoverMargins
32
+ arrow: boolean
33
+ arrowRef: React.Ref<HTMLDivElement>
34
+ arrowX?: number
35
+ arrowY?: number
36
+ availableHeight?: number
37
+ availableWidth?: number
38
+ boundaryWidth?: number
39
+ overflow?: BoxOverflow
40
+ padding?: number | number[]
41
+ placement?: Placement
42
+ radius?: number | number[]
43
+ referenceWidth?: number
44
+ scheme?: ThemeColorSchemeKey
45
+ shadow?: number | number[]
46
+ strategy: Strategy
47
+ tone: CardTone
48
+ width?: number | 'auto' | (number | 'auto')[]
49
+ x: number | null
50
+ y: number | null
51
+ } & Omit<React.HTMLProps<HTMLDivElement>, 'as' | 'height' | 'width'>,
52
+ ref: React.ForwardedRef<HTMLDivElement>
53
+ ) {
54
+ const {
55
+ __unstable_margins: marginsProp,
56
+ arrow,
57
+ arrowRef,
58
+ arrowX,
59
+ arrowY,
60
+ availableHeight,
61
+ availableWidth,
62
+ boundaryWidth,
63
+ children,
64
+ padding,
65
+ placement,
66
+ overflow,
67
+ radius,
68
+ referenceWidth: referenceWidthProp,
69
+ scheme,
70
+ shadow,
71
+ strategy,
72
+ tone,
73
+ width,
74
+ x: xProp,
75
+ y: yProp,
76
+ ...restProps
77
+ } = props
78
+
79
+ const {zIndex} = useLayer()
80
+
81
+ // top right bottom left
82
+ const margins: PopoverMargins = useMemo(() => marginsProp || [0, 0, 0, 0], [marginsProp])
83
+
84
+ // translate according to margins
85
+ const referenceWidth = referenceWidthProp
86
+ ? referenceWidthProp - margins[1] - margins[3]
87
+ : undefined
88
+ const x = (xProp ?? 0) + margins[3]
89
+ const y = (yProp ?? 0) + margins[0]
90
+
91
+ const maxWidth = availableWidth
92
+ ? Math.min(availableWidth - 8, referenceWidth || Infinity)
93
+ : undefined
94
+
95
+ const rootStyle: CSSProperties = useMemo(
96
+ () => ({
97
+ position: strategy,
98
+ top: y,
99
+ left: x,
100
+ zIndex,
101
+ width: referenceWidth,
102
+ maxWidth: referenceWidth ? undefined : maxWidth,
103
+ }),
104
+ [referenceWidth, maxWidth, strategy, x, y, zIndex]
105
+ )
106
+
107
+ const wrapperStyle: CSSProperties = useMemo(
108
+ () => ({
109
+ minHeight: 25,
110
+ maxHeight: availableHeight ? availableHeight - 8 : undefined,
111
+ }),
112
+ [availableHeight]
113
+ )
114
+
115
+ const staticSide = placement && FLOATING_STATIC_SIDES[placement.split('-')[0]]
116
+
117
+ const arrowStyle: CSSProperties = useMemo(() => {
118
+ const style: CSSProperties = {
119
+ left: arrowX !== null ? arrowX : undefined,
120
+ top: arrowY !== null ? arrowY : undefined,
121
+ right: undefined,
122
+ bottom: undefined,
123
+ }
124
+
125
+ if (staticSide) style[staticSide] = -27
126
+
127
+ return style
128
+ }, [arrowX, arrowY, staticSide])
129
+
130
+ return (
131
+ <Root
132
+ {...restProps}
133
+ $boundaryWidth={boundaryWidth}
134
+ data-placement={placement}
135
+ data-ui="Popover"
136
+ radius={radius}
137
+ ref={ref}
138
+ scheme={scheme}
139
+ shadow={shadow}
140
+ style={rootStyle}
141
+ tone={tone}
142
+ $width={width}
143
+ >
144
+ <Box
145
+ data-ui="Popover__wrapper"
146
+ overflow={overflow}
147
+ padding={padding}
148
+ sizing="border"
149
+ style={wrapperStyle}
150
+ >
151
+ {children}
152
+ </Box>
153
+
154
+ {arrow && <PopoverArrow ref={arrowRef} style={arrowStyle} />}
155
+ </Root>
156
+ )
157
+ })
158
+ )
159
+
160
+ PopoverCard.displayName = 'PopoverCard'