@sanity/ui 2.14.2 → 2.14.4-canary.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.
@@ -1,102 +1,42 @@
1
- import {AnimatePresence, motion, type Variants} from 'framer-motion'
2
- import {useMemo, useRef, useState, startTransition, useEffect} from 'react'
3
- import {styled} from 'styled-components'
4
- import {POPOVER_MOTION_CONTENT_OPACITY_PROPERTY} from '../../constants'
1
+ import {AnimatePresence} from 'framer-motion'
2
+ import {useMemo, useState} from 'react'
5
3
  import {useMounted} from '../../hooks/useMounted'
6
- import {usePrefersReducedMotion} from '../../hooks/usePrefersReducedMotion'
7
- import {Box} from '../../primitives'
8
- import {Layer} from '../../utils'
4
+ import {LayerProvider} from '../../utils'
9
5
  import {Toast} from './toast'
10
6
  import {ToastContext} from './toastContext'
7
+ import {ToastLayer, type ToastLayerProps} from './toastLayer'
11
8
  import {generateToastId} from './toastState'
12
9
  import {ToastContextValue, ToastParams} from './types'
13
10
 
14
11
  type ToastState = {
15
12
  dismiss: () => void
16
13
  id: string
14
+ updatedAt: number
17
15
  params: ToastParams
18
16
  }[]
19
17
 
20
18
  /**
21
19
  * @public
22
20
  */
23
- export interface ToastProviderProps {
21
+ export interface ToastProviderProps extends Omit<ToastLayerProps, 'children'> {
24
22
  children?: React.ReactNode
25
- padding?: number | number[]
26
- paddingX?: number | number[]
27
- paddingY?: number | number[]
28
23
  zOffset?: number | number[]
29
24
  }
30
25
 
31
- const StyledToastProvider = styled(Layer)`
32
- position: fixed;
33
- top: 0;
34
- left: 0;
35
- right: 0;
36
- bottom: 0;
37
- pointer-events: none;
38
- `
39
-
40
- const ToastContainer = styled.div`
41
- box-sizing: border-box;
42
- position: absolute;
43
- right: 0;
44
- bottom: 0;
45
- max-width: 420px;
46
- width: 100%;
47
- `
48
-
49
26
  /**
50
27
  * @public
51
28
  */
52
29
  export function ToastProvider(props: ToastProviderProps): React.JSX.Element {
53
- const {children, padding = 4, paddingX, paddingY, zOffset} = props
54
- const [state, _setState] = useState<ToastState>([])
55
- const toastsRef = useRef<{[key: string]: {timeoutId: NodeJS.Timeout}}>({})
30
+ const {children, padding, paddingX, paddingY, gap, zOffset = 1} = props
31
+ const [state, setState] = useState<ToastState>([])
56
32
  const mounted = useMounted()
57
- const prefersReducedMotion = usePrefersReducedMotion()
58
- const variants = useMemo<Variants>(
59
- () => ({
60
- /**
61
- * These variants makes use of special timing, by using a negative opacity as a starting position,
62
- * as well as double opacity as the end position.
63
- * The purpose of this is to make the tooltip/popover container appear before the content, and when exiting
64
- * we want the content to disappear faster than the container.
65
- */
66
- initial: {
67
- opacity: 0,
68
- [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY]: -1,
69
- y: 32,
70
- scale: 0.25,
71
- willChange: 'transform',
72
- },
73
- animate: {
74
- opacity: 2,
75
- [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY]: 1,
76
- y: 0,
77
- scale: 1,
78
- },
79
- exit: {
80
- opacity: 0,
81
- [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY]: -1,
82
- scale: 0.5,
83
- },
84
- transition: {duration: prefersReducedMotion ? 0 : 0.2},
85
- }),
86
- [prefersReducedMotion],
87
- )
88
33
 
89
34
  const value: ToastContextValue = useMemo(() => {
90
35
  const push = (params: ToastParams) => {
91
- // Wrap setState in startTransition to allow React to give input state updates higher priority
92
- const setState: typeof _setState = (state) => startTransition(() => _setState(state))
93
-
94
36
  const id = params.id || generateToastId()
95
37
  const duration = params.duration || 5000
96
38
 
97
39
  const dismiss = () => {
98
- const timeoutId = toastsRef.current[id]?.timeoutId
99
-
100
40
  setState((prevState): ToastState => {
101
41
  const idx = prevState.findIndex((t) => t.id === id)
102
42
 
@@ -110,11 +50,6 @@ export function ToastProvider(props: ToastProviderProps): React.JSX.Element {
110
50
 
111
51
  return prevState
112
52
  })
113
-
114
- if (timeoutId !== undefined) {
115
- clearTimeout(timeoutId)
116
- delete toastsRef.current[id]
117
- }
118
53
  }
119
54
 
120
55
  setState((prevState): ToastState => {
@@ -124,72 +59,40 @@ export function ToastProvider(props: ToastProviderProps): React.JSX.Element {
124
59
  {
125
60
  dismiss,
126
61
  id,
62
+ updatedAt: Date.now(),
127
63
  params: {...params, duration},
128
64
  },
129
65
  ])
130
66
  })
131
67
 
132
- if (toastsRef.current[id]) {
133
- clearTimeout(toastsRef.current[id].timeoutId)
134
- delete toastsRef.current[id]
135
- }
136
-
137
- toastsRef.current[id] = {timeoutId: setTimeout(dismiss, duration)}
138
-
139
68
  return id
140
69
  }
141
70
 
142
71
  return {version: 0.0, push}
143
72
  }, [])
144
73
 
145
- // clear timeouts on unmount
146
- useEffect(
147
- () => () => {
148
- for (const {timeoutId} of Object.values(toastsRef.current)) {
149
- clearTimeout(timeoutId)
150
- }
151
-
152
- toastsRef.current = {}
153
- },
154
- [],
155
- )
156
-
157
74
  return (
158
75
  <ToastContext.Provider value={value}>
159
76
  {children}
160
77
  {mounted && (
161
- <StyledToastProvider data-ui="ToastProvider" zOffset={zOffset}>
162
- <ToastContainer>
163
- <Box padding={padding} paddingX={paddingX} paddingY={paddingY}>
164
- <AnimatePresence initial={false}>
165
- {state.map(({dismiss, id, params}) => (
166
- <motion.div
167
- key={id}
168
- layout="position"
169
- initial="initial"
170
- animate="animate"
171
- exit="exit"
172
- variants={variants}
173
- transition={
174
- prefersReducedMotion
175
- ? {duration: 0}
176
- : {type: 'spring', damping: 30, stiffness: 400}
177
- }
178
- >
179
- <Toast
180
- closable={params.closable}
181
- description={params.description}
182
- onClose={dismiss}
183
- status={params.status}
184
- title={params.title}
185
- duration={params.duration}
186
- />
187
- </motion.div>
188
- ))}
189
- </AnimatePresence>
190
- </Box>
191
- </ToastContainer>
192
- </StyledToastProvider>
78
+ <LayerProvider zOffset={zOffset}>
79
+ <ToastLayer padding={padding} paddingX={paddingX} paddingY={paddingY} gap={gap}>
80
+ <AnimatePresence initial={false} mode="popLayout">
81
+ {state.map(({dismiss, id, params, updatedAt}) => (
82
+ <Toast
83
+ key={id}
84
+ closable={params.closable}
85
+ description={params.description}
86
+ onClose={dismiss}
87
+ status={params.status}
88
+ title={params.title}
89
+ duration={params.duration}
90
+ updatedAt={updatedAt}
91
+ />
92
+ ))}
93
+ </AnimatePresence>
94
+ </ToastLayer>
95
+ </LayerProvider>
193
96
  )}
194
97
  </ToastContext.Provider>
195
98
  )
@@ -10,47 +10,60 @@ export const EMPTY_ARRAY: never[] = []
10
10
  */
11
11
  export const EMPTY_RECORD: Record<string, never> = {}
12
12
 
13
- /**
14
- * @internal
15
- */
16
- export const POPOVER_MOTION_CONTENT_OPACITY_PROPERTY = '--motion-content-opacity' as string
13
+ const POPOVER_MOTION_DURATION = 0.3
17
14
 
18
15
  /**
19
16
  * Shared `framer-motion` variants used by `Popover` and `Tooltip` components.
20
17
  * @internal
21
18
  */
22
19
  export const POPOVER_MOTION_PROPS: {
23
- animate: Variant
24
- initial: Variant
25
- exit: Variant
20
+ outerVariants: {
21
+ initial: Variant
22
+ hidden: Variant
23
+ visible: Variant
24
+ scaleIn: Variant
25
+ scaleOut: Variant
26
+ }
27
+ innerVariants: {
28
+ hidden: Variant
29
+ visible: Variant
30
+ }
26
31
  transition: Transition
27
32
  } = {
28
- /**
29
- * These variants makes use of special timing, by using a negative opacity as a starting position,
30
- * as well as double opacity as the end position.
31
- * The purpose of this is to make the tooltip/popover container appear before the content, and when exiting
32
- * we want the content to disappear faster than the container.
33
- */
34
- initial: {
35
- opacity: 0.5,
36
- // the nagative opacity here, as well as the double opacity further down, are to make the content appear after the backgdrop, and when exiting the content should disappear first.
37
- [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY as string]: -1,
38
- scale: 0.97,
39
- willChange: 'transform',
40
- },
41
- animate: {
42
- opacity: 2,
43
- [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY as string]: 1,
44
- scale: 1,
33
+ outerVariants: {
34
+ initial: {
35
+ scale: 0.97,
36
+ willChange: 'transform',
37
+ },
38
+ hidden: {
39
+ opacity: 0,
40
+ },
41
+ visible: {
42
+ opacity: 1,
43
+ transition: {
44
+ when: 'beforeChildren',
45
+ duration: POPOVER_MOTION_DURATION / 3,
46
+ },
47
+ },
48
+ scaleIn: {
49
+ scale: 1,
50
+ },
51
+ scaleOut: {
52
+ scale: 0.97,
53
+ },
45
54
  },
46
- exit: {
47
- opacity: 0,
48
- [POPOVER_MOTION_CONTENT_OPACITY_PROPERTY as string]: -1,
49
- scale: 0.97,
55
+ innerVariants: {
56
+ hidden: {
57
+ opacity: 0,
58
+ },
59
+ visible: {
60
+ opacity: 1,
61
+ },
50
62
  },
51
63
  transition: {
52
- duration: 0.4,
53
64
  type: 'spring',
65
+ visualDuration: POPOVER_MOTION_DURATION,
66
+ bounce: 0.25,
54
67
  },
55
68
  }
56
69
 
@@ -15,13 +15,14 @@ export function useDelayedState<S>(
15
15
  setState(nextState)
16
16
  }
17
17
 
18
+ if (!delay) return action()
19
+
18
20
  // A new state change has been initiated, cancel the previous one.
19
21
  if (delayedAction.current) {
20
22
  clearTimeout(delayedAction.current)
21
23
  delayedAction.current = undefined
22
24
  }
23
25
 
24
- if (!delay) return action()
25
26
  delayedAction.current = setTimeout(action, delay)
26
27
  }, [])
27
28
 
@@ -10,6 +10,7 @@ import {ResponsiveGridProps} from '../types'
10
10
  */
11
11
  export interface GridProps extends Omit<BoxProps, 'display'>, ResponsiveGridProps {}
12
12
 
13
+ // @TODO this might be how ToastLayer has to be setup
13
14
  const StyledGrid = styled(Box)<ResponsiveGridStyleProps>(responsiveGridStyle)
14
15
 
15
16
  /**
@@ -3,7 +3,7 @@ import {ThemeColorSchemeKey} from '@sanity/ui/theme'
3
3
  import {type MotionProps, motion} from 'framer-motion'
4
4
  import React, {CSSProperties, forwardRef, memo, useMemo} from 'react'
5
5
  import {styled} from 'styled-components'
6
- import {POPOVER_MOTION_CONTENT_OPACITY_PROPERTY, POPOVER_MOTION_PROPS} from '../../constants'
6
+ import {POPOVER_MOTION_PROPS} from '../../constants'
7
7
  import {BoxOverflow, CardTone, Placement, PopoverMargins, Radius} from '../../types'
8
8
  import {Arrow, useLayer} from '../../utils'
9
9
  import {Card, CardProps} from '../card'
@@ -22,10 +22,11 @@ const MotionCard = styled(motion.create(Card))`
22
22
  flex-direction: column;
23
23
  width: max-content;
24
24
  min-width: min-content;
25
- & > * {
26
- opacity: var(${POPOVER_MOTION_CONTENT_OPACITY_PROPERTY}, 1);
27
- will-change: opacity;
28
- }
25
+ will-change: transform;
26
+ `
27
+
28
+ const MotionFlex = styled(motion.create(Flex))`
29
+ will-change: opacity;
29
30
  `
30
31
 
31
32
  /**
@@ -131,13 +132,24 @@ export const PopoverCard = memo(
131
132
  sizing="border"
132
133
  style={rootStyle}
133
134
  tone={tone}
134
- {...(animate ? POPOVER_MOTION_PROPS : {})}
135
+ variants={POPOVER_MOTION_PROPS.outerVariants}
136
+ transition={POPOVER_MOTION_PROPS.transition}
137
+ initial={animate ? ['hidden', 'initial'] : undefined}
138
+ animate={animate ? ['visible', 'scaleIn'] : undefined}
139
+ exit={animate ? ['hidden', 'scaleOut'] : undefined}
135
140
  >
136
- <Flex data-ui="Popover__wrapper" direction="column" flex={1} overflow={overflow}>
141
+ <MotionFlex
142
+ data-ui="Popover__wrapper"
143
+ direction="column"
144
+ flex={1}
145
+ overflow={overflow}
146
+ variants={POPOVER_MOTION_PROPS.innerVariants}
147
+ transition={POPOVER_MOTION_PROPS.transition}
148
+ >
137
149
  <Flex direction="column" flex={1} padding={padding}>
138
150
  {children}
139
151
  </Flex>
140
- </Flex>
152
+ </MotionFlex>
141
153
 
142
154
  {arrow && (
143
155
  <Arrow
@@ -219,49 +219,6 @@ export const Tooltip = forwardRef(function Tooltip(
219
219
  [isInsideGroup, delayGroupContext, openDelay, tooltipId, closeDelay, setIsOpen],
220
220
  )
221
221
 
222
- const handleBlur = useCallback(
223
- (e: FocusEvent) => {
224
- handleIsOpenChange(false)
225
- childProp?.props?.onBlur?.(e)
226
- },
227
- [childProp?.props, handleIsOpenChange],
228
- )
229
- const handleClick = useCallback(
230
- (e: MouseEvent) => {
231
- handleIsOpenChange(false, true)
232
- childProp?.props.onClick?.(e)
233
- },
234
- [childProp?.props, handleIsOpenChange],
235
- )
236
- const handleContextMenu = useCallback(
237
- (e: MouseEvent) => {
238
- handleIsOpenChange(false, true)
239
- childProp?.props.onContextMenu?.(e)
240
- },
241
- [childProp?.props, handleIsOpenChange],
242
- )
243
- const handleFocus = useCallback(
244
- (e: FocusEvent) => {
245
- handleIsOpenChange(true)
246
- childProp?.props?.onFocus?.(e)
247
- },
248
- [childProp?.props, handleIsOpenChange],
249
- )
250
- const handleMouseEnter = useCallback(
251
- (e: MouseEvent) => {
252
- handleIsOpenChange(true)
253
- childProp?.props?.onMouseEnter?.(e)
254
- },
255
- [childProp?.props, handleIsOpenChange],
256
- )
257
- const handleMouseLeave = useCallback(
258
- (e: MouseEvent) => {
259
- handleIsOpenChange(false)
260
- childProp?.props?.onMouseLeave?.(e)
261
- },
262
- [childProp?.props, handleIsOpenChange],
263
- )
264
-
265
222
  // Handle closing the tooltip when the mouse leaves the referenceElement
266
223
  useCloseOnMouseLeave({handleIsOpenChange, referenceElement, showTooltip})
267
224
 
@@ -292,6 +249,28 @@ export const Tooltip = forwardRef(function Tooltip(
292
249
  }
293
250
  }, [handleIsOpenChange, showTooltip])
294
251
 
252
+ /* eslint-disable padding-line-between-statements */
253
+ // Handle delays and grouping
254
+ const handleChildEvent = useEffectEvent((open: boolean, immediate?: boolean) =>
255
+ handleIsOpenChange(open, immediate),
256
+ )
257
+ useEffect(() => {
258
+ if (!referenceElement) return
259
+
260
+ const controller = new AbortController()
261
+ const {signal} = controller
262
+
263
+ referenceElement.addEventListener('blur', () => handleChildEvent(false), {signal})
264
+ referenceElement.addEventListener('focus', () => handleChildEvent(true), {signal})
265
+ referenceElement.addEventListener('mouseenter', () => handleChildEvent(true), {signal})
266
+ referenceElement.addEventListener('mouseleave', () => handleChildEvent(false), {signal})
267
+ referenceElement.addEventListener('click', () => handleChildEvent(false, true), {signal})
268
+ referenceElement.addEventListener('contextmenu', () => handleChildEvent(false, true), {signal})
269
+
270
+ return () => controller.abort()
271
+ }, [referenceElement])
272
+ /* eslint-enable padding-line-between-statements */
273
+
295
274
  // // Set the max width of the tooltip based on boundaries and portals
296
275
  useLayoutEffect(() => {
297
276
  // Get the maximum tooltip width (sans tooltip padding)
@@ -324,24 +303,8 @@ export const Tooltip = forwardRef(function Tooltip(
324
303
  const child = useMemo(() => {
325
304
  if (!childProp) return null
326
305
 
327
- return cloneElement(childProp, {
328
- onBlur: handleBlur,
329
- onFocus: handleFocus,
330
- onMouseEnter: handleMouseEnter,
331
- onMouseLeave: handleMouseLeave,
332
- onClick: handleClick,
333
- onContextMenu: handleContextMenu,
334
- ref: setReferenceElement,
335
- })
336
- }, [
337
- childProp,
338
- handleBlur,
339
- handleClick,
340
- handleContextMenu,
341
- handleFocus,
342
- handleMouseEnter,
343
- handleMouseLeave,
344
- ])
306
+ return cloneElement(childProp, {ref: setReferenceElement})
307
+ }, [childProp])
345
308
 
346
309
  // If there's a child then we need to set the reference element to the cloned child ref
347
310
  // and if child changes we make sure to update or remove the reference element.
@@ -2,7 +2,7 @@ import {ThemeColorSchemeKey} from '@sanity/ui/theme'
2
2
  import {type MotionProps, motion} from 'framer-motion'
3
3
  import React, {CSSProperties, forwardRef, memo, useMemo} from 'react'
4
4
  import {styled} from 'styled-components'
5
- import {POPOVER_MOTION_CONTENT_OPACITY_PROPERTY, POPOVER_MOTION_PROPS} from '../../constants'
5
+ import {POPOVER_MOTION_PROPS} from '../../constants'
6
6
  import {Placement, Radius} from '../../types'
7
7
  import {Arrow} from '../../utils'
8
8
  import {Card, CardProps} from '../card'
@@ -13,10 +13,7 @@ import {
13
13
  } from './constants'
14
14
 
15
15
  const MotionCard = styled(motion.create(Card))`
16
- & > * {
17
- opacity: var(${POPOVER_MOTION_CONTENT_OPACITY_PROPERTY}, 1);
18
- will-change: opacity;
19
- }
16
+ will-change: transform;
20
17
  `
21
18
 
22
19
  /**
@@ -89,7 +86,11 @@ export const TooltipCard = memo(
89
86
  scheme={scheme}
90
87
  shadow={shadow}
91
88
  style={rootStyle}
92
- {...(animate ? POPOVER_MOTION_PROPS : {})}
89
+ variants={POPOVER_MOTION_PROPS.outerVariants}
90
+ transition={POPOVER_MOTION_PROPS.transition}
91
+ initial={animate ? ['hidden', 'initial'] : undefined}
92
+ animate={animate ? ['visible', 'scaleIn'] : undefined}
93
+ exit={animate ? ['hidden', 'scaleOut'] : undefined}
93
94
  >
94
95
  {children}
95
96
 
@@ -66,8 +66,8 @@ const LayerChildren = forwardRef(function LayerChildren(
66
66
 
67
67
  return (
68
68
  <StyledLayer
69
- {...restProps}
70
69
  data-ui="Layer"
70
+ {...restProps}
71
71
  onFocus={handleFocus}
72
72
  ref={ref}
73
73
  style={{...style, zIndex}}