@sanity/ui 2.14.4-canary.2 → 2.14.4
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/dist/_chunks-cjs/_visual-editing.js +64 -84
- package/dist/_chunks-cjs/_visual-editing.js.map +1 -1
- package/dist/_chunks-es/_visual-editing.mjs +64 -84
- package/dist/_chunks-es/_visual-editing.mjs.map +1 -1
- package/dist/_legacy/_visual-editing.esm.js +64 -84
- package/dist/_legacy/_visual-editing.esm.js.map +1 -1
- package/dist/index.d.mts +6 -26
- package/dist/index.d.ts +6 -26
- package/dist/index.esm.js +187 -182
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +185 -180
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +187 -182
- package/dist/index.mjs.map +1 -1
- package/package.json +41 -27
- package/src/core/components/toast/styles.ts +53 -57
- package/src/core/components/toast/toast.tsx +34 -127
- package/src/core/components/toast/toastProvider.tsx +142 -49
- package/src/core/constants.ts +29 -42
- package/src/core/primitives/grid/grid.tsx +0 -1
- package/src/core/primitives/popover/popoverCard.tsx +8 -20
- package/src/core/primitives/tooltip/tooltip.tsx +61 -24
- package/src/core/primitives/tooltip/tooltipCard.tsx +6 -7
- package/src/core/utils/layer/layer.tsx +1 -1
- package/src/core/components/toast/toastLayer.tsx +0 -50
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/ui",
|
|
3
|
-
"version": "2.14.4
|
|
3
|
+
"version": "2.14.4",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"sanity",
|
|
6
6
|
"ui",
|
|
@@ -61,6 +61,32 @@
|
|
|
61
61
|
"_visual-editing.js",
|
|
62
62
|
"theme.js"
|
|
63
63
|
],
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "run-s clean pkg:build pkg:check figma:pkg:build",
|
|
66
|
+
"clean": "rimraf .workshop dist",
|
|
67
|
+
"commit": "cz",
|
|
68
|
+
"cypress:dev": "run-p dev cypress:open",
|
|
69
|
+
"cypress:open": "cypress open",
|
|
70
|
+
"cypress:run": "cypress run",
|
|
71
|
+
"dev": "run-p storybook:dev workshop:dev",
|
|
72
|
+
"figma:pkg:build": "cd figma && pnpm build",
|
|
73
|
+
"format": "prettier --write --cache --ignore-unknown .",
|
|
74
|
+
"lint": "eslint . --ext .cjs,.js,.jsx,.mjs,.ts,.tsx",
|
|
75
|
+
"pkg:build": "pkg build --strict",
|
|
76
|
+
"pkg:check": "pkg --strict",
|
|
77
|
+
"prepare": "husky install",
|
|
78
|
+
"prepack": "pnpm build",
|
|
79
|
+
"release": "semantic-release",
|
|
80
|
+
"storybook:build": "storybook build",
|
|
81
|
+
"storybook:dev": "storybook dev -p 6006",
|
|
82
|
+
"test": "jest",
|
|
83
|
+
"test:browser": "start-server-and-test 'run-s workshop:build workshop:start' http://localhost:1337 'run-s cypress:run'",
|
|
84
|
+
"ts:check": "tsc",
|
|
85
|
+
"watch": "pkg watch --strict",
|
|
86
|
+
"workshop:build": "workshop build",
|
|
87
|
+
"workshop:dev": "workshop dev",
|
|
88
|
+
"workshop:start": "http-server -a localhost -c-0 -p 1337 -s -P http://localhost:1337/index.html? dist"
|
|
89
|
+
},
|
|
64
90
|
"commitlint": {
|
|
65
91
|
"extends": [
|
|
66
92
|
"@commitlint/config-conventional"
|
|
@@ -92,7 +118,7 @@
|
|
|
92
118
|
"@floating-ui/react-dom": "^2.1.2",
|
|
93
119
|
"@juggle/resize-observer": "^3.4.0",
|
|
94
120
|
"@sanity/color": "^3.0.6",
|
|
95
|
-
"@sanity/icons": "^3.
|
|
121
|
+
"@sanity/icons": "^3.7.0",
|
|
96
122
|
"csstype": "^3.1.3",
|
|
97
123
|
"framer-motion": "^12.4.7",
|
|
98
124
|
"react-compiler-runtime": "19.0.0-beta-e1e972c-20250221",
|
|
@@ -182,37 +208,25 @@
|
|
|
182
208
|
"react-is": "^18 || >=19.0.0-0",
|
|
183
209
|
"styled-components": "^5.2 || ^6"
|
|
184
210
|
},
|
|
211
|
+
"packageManager": "pnpm@9.15.6",
|
|
185
212
|
"engines": {
|
|
186
213
|
"node": ">=14.0.0"
|
|
187
214
|
},
|
|
188
215
|
"publishConfig": {
|
|
189
216
|
"access": "public"
|
|
190
217
|
},
|
|
218
|
+
"pnpm": {
|
|
219
|
+
"overrides": {
|
|
220
|
+
"@types/react": "$@types/react",
|
|
221
|
+
"@types/react-dom": "$@types/react-dom",
|
|
222
|
+
"@types/react-is": "$@types/react-is",
|
|
223
|
+
"conventional-changelog-conventionalcommits": ">= 8.0.0",
|
|
224
|
+
"react": "$react",
|
|
225
|
+
"react-dom": "$react-dom",
|
|
226
|
+
"react-is": "$react-is"
|
|
227
|
+
}
|
|
228
|
+
},
|
|
191
229
|
"esm.sh": {
|
|
192
230
|
"bundle": false
|
|
193
|
-
},
|
|
194
|
-
"scripts": {
|
|
195
|
-
"build": "run-s clean pkg:build pkg:check figma:pkg:build",
|
|
196
|
-
"clean": "rimraf .workshop dist",
|
|
197
|
-
"commit": "cz",
|
|
198
|
-
"cypress:dev": "run-p dev cypress:open",
|
|
199
|
-
"cypress:open": "cypress open",
|
|
200
|
-
"cypress:run": "cypress run",
|
|
201
|
-
"dev": "run-p storybook:dev workshop:dev",
|
|
202
|
-
"figma:pkg:build": "cd figma && pnpm build",
|
|
203
|
-
"format": "prettier --write --cache --ignore-unknown .",
|
|
204
|
-
"lint": "eslint . --ext .cjs,.js,.jsx,.mjs,.ts,.tsx",
|
|
205
|
-
"pkg:build": "pkg build --strict",
|
|
206
|
-
"pkg:check": "pkg --strict",
|
|
207
|
-
"release": "semantic-release",
|
|
208
|
-
"storybook:build": "storybook build",
|
|
209
|
-
"storybook:dev": "storybook dev -p 6006",
|
|
210
|
-
"test": "jest",
|
|
211
|
-
"test:browser": "start-server-and-test 'run-s workshop:build workshop:start' http://localhost:1337 'run-s cypress:run'",
|
|
212
|
-
"ts:check": "tsc",
|
|
213
|
-
"watch": "pkg watch --strict",
|
|
214
|
-
"workshop:build": "workshop build",
|
|
215
|
-
"workshop:dev": "workshop dev",
|
|
216
|
-
"workshop:start": "http-server -a localhost -c-0 -p 1337 -s -P http://localhost:1337/index.html? dist"
|
|
217
231
|
}
|
|
218
|
-
}
|
|
232
|
+
}
|
|
@@ -1,68 +1,64 @@
|
|
|
1
|
-
import {styled} from 'styled-components'
|
|
1
|
+
import {styled, keyframes, css} from 'styled-components'
|
|
2
2
|
import {ThemeColorStateToneKey, getTheme_v2} from '../../../theme'
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
const LOADING_BAR_HEIGHT = 2
|
|
7
|
-
|
|
8
|
-
export const STATUS_CARD_TONE = {
|
|
9
|
-
error: 'critical',
|
|
10
|
-
warning: 'caution',
|
|
11
|
-
success: 'positive',
|
|
12
|
-
info: 'neutral',
|
|
13
|
-
} satisfies {[key: string]: ThemeColorStateToneKey}
|
|
14
|
-
|
|
15
|
-
export const BUTTON_TONE = {
|
|
16
|
-
error: 'critical',
|
|
17
|
-
warning: 'caution',
|
|
18
|
-
success: 'positive',
|
|
19
|
-
info: 'neutral',
|
|
20
|
-
} satisfies {[key: string]: ButtonTone}
|
|
3
|
+
import {POPOVER_MOTION_CONTENT_OPACITY_PROPERTY} from '../../constants'
|
|
4
|
+
import {Flex} from '../../primitives'
|
|
5
|
+
import {ThemeProps} from '../../styles'
|
|
21
6
|
|
|
22
7
|
export const TextBox = styled(Flex)`
|
|
23
8
|
overflow-x: auto;
|
|
24
9
|
`
|
|
25
10
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
will-change: opacity, transform;
|
|
33
|
-
|
|
34
|
-
&[data-has-duration] {
|
|
35
|
-
padding-bottom: calc(${LOADING_BAR_HEIGHT}px / 2);
|
|
11
|
+
const loadingAnimation = keyframes`
|
|
12
|
+
0% {
|
|
13
|
+
width: 0;
|
|
14
|
+
}
|
|
15
|
+
100% {
|
|
16
|
+
width: 100%;
|
|
36
17
|
}
|
|
37
18
|
`
|
|
38
19
|
|
|
39
|
-
|
|
40
|
-
display: flex;
|
|
41
|
-
position: absolute;
|
|
42
|
-
bottom: 0px;
|
|
43
|
-
top: 0px;
|
|
44
|
-
left: 0px;
|
|
45
|
-
right: 0px;
|
|
46
|
-
pointer-events: none;
|
|
47
|
-
z-index: -1;
|
|
48
|
-
overflow: hidden;
|
|
49
|
-
overflow: clip;
|
|
50
|
-
background: transparent;
|
|
51
|
-
align-items: flex-end;
|
|
52
|
-
will-change: opacity;
|
|
53
|
-
`
|
|
20
|
+
const LOADING_BAR_HEIGHT = 2
|
|
54
21
|
|
|
55
|
-
|
|
56
|
-
tone: ThemeColorStateToneKey
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
display: block;
|
|
60
|
-
height: 100%;
|
|
61
|
-
width: 100%;
|
|
62
|
-
transform-origin: 0% 50%;
|
|
63
|
-
background-color: ${(props) => {
|
|
64
|
-
const {color} = getTheme_v2(props.theme)
|
|
22
|
+
export function rootStyles(
|
|
23
|
+
props: {$duration?: number; tone: ThemeColorStateToneKey} & ThemeProps,
|
|
24
|
+
): ReturnType<typeof css> {
|
|
25
|
+
const {color} = getTheme_v2(props.theme)
|
|
65
26
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
27
|
+
const loadingBarColor = color.button.default[props.tone].enabled.bg
|
|
28
|
+
|
|
29
|
+
if (!props.$duration)
|
|
30
|
+
return css`
|
|
31
|
+
pointer-events: all;
|
|
32
|
+
& > * {
|
|
33
|
+
opacity: var(${POPOVER_MOTION_CONTENT_OPACITY_PROPERTY}, 1);
|
|
34
|
+
will-change: opacity;
|
|
35
|
+
}
|
|
36
|
+
`
|
|
37
|
+
|
|
38
|
+
return css`
|
|
39
|
+
pointer-events: all;
|
|
40
|
+
width: 100%;
|
|
41
|
+
position: relative;
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
overflow: clip;
|
|
44
|
+
padding-bottom: ${LOADING_BAR_HEIGHT}px;
|
|
45
|
+
&::before {
|
|
46
|
+
content: '';
|
|
47
|
+
position: absolute;
|
|
48
|
+
bottom: 0px;
|
|
49
|
+
height: ${LOADING_BAR_HEIGHT}px;
|
|
50
|
+
background: ${loadingBarColor};
|
|
51
|
+
animation-name: ${loadingAnimation};
|
|
52
|
+
animation-duration: ${props.$duration}ms;
|
|
53
|
+
animation-fill-mode: both;
|
|
54
|
+
animation-timing-function: linear;
|
|
55
|
+
opacity: var(${POPOVER_MOTION_CONTENT_OPACITY_PROPERTY}, 1);
|
|
56
|
+
will-change: width;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
& > * {
|
|
60
|
+
opacity: var(${POPOVER_MOTION_CONTENT_OPACITY_PROPERTY}, 1);
|
|
61
|
+
will-change: opacity;
|
|
62
|
+
}
|
|
63
|
+
`
|
|
64
|
+
}
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import {CloseIcon} from '@sanity/icons'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import {ThemeColorStateToneKey} from '@sanity/ui/theme'
|
|
3
|
+
import {styled} from 'styled-components'
|
|
4
4
|
import {Box, Button, Flex, Stack, Text, Card} from '../../primitives'
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
LoadingBarProgress,
|
|
9
|
-
BUTTON_TONE,
|
|
10
|
-
STATUS_CARD_TONE,
|
|
11
|
-
TextBox,
|
|
12
|
-
StyledToast,
|
|
13
|
-
} from './styles'
|
|
5
|
+
import {ThemeProps} from '../../styles'
|
|
6
|
+
import type {ButtonTone} from '../../types'
|
|
7
|
+
import {rootStyles, TextBox} from './styles'
|
|
14
8
|
|
|
15
9
|
/**
|
|
16
10
|
* @public
|
|
@@ -18,14 +12,27 @@ import {
|
|
|
18
12
|
export interface ToastProps {
|
|
19
13
|
closable?: boolean
|
|
20
14
|
description?: React.ReactNode
|
|
21
|
-
onClose
|
|
15
|
+
onClose?: () => void
|
|
22
16
|
radius?: number | number[]
|
|
23
17
|
title?: React.ReactNode
|
|
24
18
|
status?: 'error' | 'warning' | 'success' | 'info'
|
|
25
19
|
duration?: number
|
|
26
|
-
updatedAt?: number
|
|
27
20
|
}
|
|
28
21
|
|
|
22
|
+
const STATUS_CARD_TONE: {[key: string]: ThemeColorStateToneKey} = {
|
|
23
|
+
error: 'critical',
|
|
24
|
+
warning: 'caution',
|
|
25
|
+
success: 'positive',
|
|
26
|
+
info: 'neutral',
|
|
27
|
+
} as const
|
|
28
|
+
|
|
29
|
+
const BUTTON_TONE = {
|
|
30
|
+
error: 'critical',
|
|
31
|
+
warning: 'caution',
|
|
32
|
+
success: 'positive',
|
|
33
|
+
info: 'neutral',
|
|
34
|
+
} satisfies {[key: string]: ButtonTone}
|
|
35
|
+
|
|
29
36
|
const ROLES = {
|
|
30
37
|
error: 'alert',
|
|
31
38
|
warning: 'alert',
|
|
@@ -33,6 +40,10 @@ const ROLES = {
|
|
|
33
40
|
info: 'alert',
|
|
34
41
|
} as const
|
|
35
42
|
|
|
43
|
+
const StyledToast = styled(Card)<{$duration?: number; tone: ThemeColorStateToneKey} & ThemeProps>(
|
|
44
|
+
rootStyles,
|
|
45
|
+
)
|
|
46
|
+
|
|
36
47
|
/**
|
|
37
48
|
* The `Toast` component gives feedback to users when an action has taken place.
|
|
38
49
|
*
|
|
@@ -41,62 +52,25 @@ const ROLES = {
|
|
|
41
52
|
* @public
|
|
42
53
|
*/
|
|
43
54
|
export function Toast(
|
|
44
|
-
props: ToastProps &
|
|
45
|
-
Omit<
|
|
46
|
-
React.HTMLProps<HTMLDivElement>,
|
|
47
|
-
| 'as'
|
|
48
|
-
| 'height'
|
|
49
|
-
| 'ref'
|
|
50
|
-
| 'title'
|
|
51
|
-
| 'onAnimationStart'
|
|
52
|
-
| 'onDragStart'
|
|
53
|
-
| 'onDragEnd'
|
|
54
|
-
| 'onDrag'
|
|
55
|
-
>,
|
|
55
|
+
props: ToastProps & Omit<React.HTMLProps<HTMLDivElement>, 'as' | 'height' | 'ref' | 'title'>,
|
|
56
56
|
): React.JSX.Element {
|
|
57
|
-
const {
|
|
58
|
-
closable,
|
|
59
|
-
description,
|
|
60
|
-
duration,
|
|
61
|
-
onClose,
|
|
62
|
-
radius = 3,
|
|
63
|
-
title,
|
|
64
|
-
status,
|
|
65
|
-
updatedAt,
|
|
66
|
-
...restProps
|
|
67
|
-
} = props
|
|
57
|
+
const {closable, description, duration, onClose, radius = 3, title, status, ...restProps} = props
|
|
68
58
|
const cardTone = status ? STATUS_CARD_TONE[status] : 'default'
|
|
69
59
|
const buttonTone = status ? BUTTON_TONE[status] : 'default'
|
|
70
60
|
const role = status ? ROLES[status] : 'status'
|
|
71
61
|
|
|
72
|
-
const prefersReducedMotion = usePrefersReducedMotion()
|
|
73
|
-
|
|
74
|
-
// eslint-disable-next-line prefer-const
|
|
75
|
-
let visualDuration = prefersReducedMotion ? 0 : 0.26
|
|
76
|
-
// visualDuration = 5
|
|
77
|
-
const transition = visualDuration ? {type: 'spring', visualDuration, bounce: 0.25} : {duration: 0}
|
|
78
|
-
|
|
79
|
-
const hasDuration = duration && isFinite(duration)
|
|
80
|
-
|
|
81
62
|
return (
|
|
82
|
-
<
|
|
63
|
+
<StyledToast
|
|
83
64
|
data-ui="Toast"
|
|
84
65
|
role={role}
|
|
85
66
|
{...restProps}
|
|
86
|
-
|
|
87
|
-
custom={visualDuration satisfies number}
|
|
67
|
+
marginTop={3}
|
|
88
68
|
radius={radius}
|
|
89
69
|
shadow={2}
|
|
90
70
|
tone={cardTone}
|
|
91
|
-
|
|
92
|
-
layout="position"
|
|
93
|
-
variants={container}
|
|
94
|
-
initial={['hidden', 'initial'] satisfies ContainerVariants[]}
|
|
95
|
-
animate={['visible', 'slideIn'] satisfies ContainerVariants[]}
|
|
96
|
-
exit={['hidden', 'slideOut'] satisfies ContainerVariants[]}
|
|
97
|
-
transition={transition}
|
|
71
|
+
$duration={duration}
|
|
98
72
|
>
|
|
99
|
-
<
|
|
73
|
+
<Flex align="flex-start">
|
|
100
74
|
<TextBox flex={1} padding={3}>
|
|
101
75
|
<Stack space={3}>
|
|
102
76
|
{title && (
|
|
@@ -105,9 +79,9 @@ export function Toast(
|
|
|
105
79
|
</Text>
|
|
106
80
|
)}
|
|
107
81
|
{description && (
|
|
108
|
-
<
|
|
82
|
+
<Text muted size={1}>
|
|
109
83
|
{description}
|
|
110
|
-
</
|
|
84
|
+
</Text>
|
|
111
85
|
)}
|
|
112
86
|
</Stack>
|
|
113
87
|
</TextBox>
|
|
@@ -125,76 +99,9 @@ export function Toast(
|
|
|
125
99
|
/>
|
|
126
100
|
</Box>
|
|
127
101
|
)}
|
|
128
|
-
</
|
|
129
|
-
|
|
130
|
-
<MotionLoadiongBar variants={content} transition={transition}>
|
|
131
|
-
<Card
|
|
132
|
-
tone={cardTone}
|
|
133
|
-
radius={radius}
|
|
134
|
-
style={{
|
|
135
|
-
position: 'absolute',
|
|
136
|
-
right: -2,
|
|
137
|
-
left: -2,
|
|
138
|
-
top: 0,
|
|
139
|
-
bottom: 2,
|
|
140
|
-
zIndex: 1,
|
|
141
|
-
}}
|
|
142
|
-
/>
|
|
143
|
-
<MotionLoadiongBarProgress
|
|
144
|
-
key={`${duration}-${updatedAt}`}
|
|
145
|
-
tone={cardTone}
|
|
146
|
-
initial={{scaleX: 0}}
|
|
147
|
-
animate={{scaleX: 1}}
|
|
148
|
-
transition={{delay: visualDuration, duration: duration / 1_000, ease: 'linear'}}
|
|
149
|
-
onAnimationComplete={onClose}
|
|
150
|
-
/>
|
|
151
|
-
</MotionLoadiongBar>
|
|
152
|
-
)}
|
|
153
|
-
</MotionToast>
|
|
102
|
+
</Flex>
|
|
103
|
+
</StyledToast>
|
|
154
104
|
)
|
|
155
105
|
}
|
|
156
106
|
|
|
157
107
|
Toast.displayName = 'Toast'
|
|
158
|
-
|
|
159
|
-
const container = {
|
|
160
|
-
initial: {y: 32, scale: 0.5, zIndex: 1},
|
|
161
|
-
hidden: {opacity: 0},
|
|
162
|
-
visible: (visualDuration: number) => {
|
|
163
|
-
if (!visualDuration) return {opacity: 1}
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
opacity: 1,
|
|
167
|
-
transition: {
|
|
168
|
-
when: 'beforeChildren',
|
|
169
|
-
staggerChildren: visualDuration / 3,
|
|
170
|
-
duration: visualDuration / 3,
|
|
171
|
-
},
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
slideIn: {
|
|
175
|
-
y: 0,
|
|
176
|
-
scale: 1,
|
|
177
|
-
},
|
|
178
|
-
slideOut: {
|
|
179
|
-
zIndex: 0,
|
|
180
|
-
scale: 0.75,
|
|
181
|
-
},
|
|
182
|
-
} satisfies Variants
|
|
183
|
-
type ContainerVariants = keyof typeof container
|
|
184
|
-
|
|
185
|
-
const content = {
|
|
186
|
-
hidden: {
|
|
187
|
-
opacity: 0,
|
|
188
|
-
},
|
|
189
|
-
visible: {
|
|
190
|
-
opacity: 1,
|
|
191
|
-
},
|
|
192
|
-
} satisfies Variants
|
|
193
|
-
|
|
194
|
-
const MotionToast = motion.create(StyledToast)
|
|
195
|
-
const MotionFlex = motion.create(Flex)
|
|
196
|
-
|
|
197
|
-
const MotionText = motion.create(Text)
|
|
198
|
-
|
|
199
|
-
const MotionLoadiongBar = motion.create(LoadingBar)
|
|
200
|
-
const MotionLoadiongBarProgress = motion.create(LoadingBarProgress)
|
|
@@ -1,102 +1,195 @@
|
|
|
1
|
-
import {AnimatePresence} from 'framer-motion'
|
|
2
|
-
import {
|
|
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'
|
|
3
5
|
import {useMounted} from '../../hooks/useMounted'
|
|
4
|
-
import {
|
|
6
|
+
import {usePrefersReducedMotion} from '../../hooks/usePrefersReducedMotion'
|
|
7
|
+
import {Box} from '../../primitives'
|
|
8
|
+
import {Layer} from '../../utils'
|
|
5
9
|
import {Toast} from './toast'
|
|
6
10
|
import {ToastContext} from './toastContext'
|
|
7
|
-
import {ToastLayer, type ToastLayerProps} from './toastLayer'
|
|
8
11
|
import {generateToastId} from './toastState'
|
|
9
12
|
import {ToastContextValue, ToastParams} from './types'
|
|
10
13
|
|
|
11
14
|
type ToastState = {
|
|
12
15
|
dismiss: () => void
|
|
13
16
|
id: string
|
|
14
|
-
updatedAt: number
|
|
15
17
|
params: ToastParams
|
|
16
18
|
}[]
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* @public
|
|
20
22
|
*/
|
|
21
|
-
export interface ToastProviderProps
|
|
23
|
+
export interface ToastProviderProps {
|
|
22
24
|
children?: React.ReactNode
|
|
25
|
+
padding?: number | number[]
|
|
26
|
+
paddingX?: number | number[]
|
|
27
|
+
paddingY?: number | number[]
|
|
23
28
|
zOffset?: number | number[]
|
|
24
29
|
}
|
|
25
30
|
|
|
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
|
+
|
|
26
49
|
/**
|
|
27
50
|
* @public
|
|
28
51
|
*/
|
|
29
52
|
export function ToastProvider(props: ToastProviderProps): React.JSX.Element {
|
|
30
|
-
const {children, padding, paddingX, paddingY,
|
|
31
|
-
const [state,
|
|
53
|
+
const {children, padding = 4, paddingX, paddingY, zOffset} = props
|
|
54
|
+
const [state, _setState] = useState<ToastState>([])
|
|
55
|
+
const toastsRef = useRef<{[key: string]: {timeoutId: NodeJS.Timeout}}>({})
|
|
32
56
|
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
|
+
)
|
|
33
88
|
|
|
34
89
|
const value: ToastContextValue = useMemo(() => {
|
|
35
90
|
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
|
+
|
|
36
94
|
const id = params.id || generateToastId()
|
|
37
95
|
const duration = params.duration || 5000
|
|
38
96
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const dismiss = () => {
|
|
42
|
-
startTransition(() => {
|
|
43
|
-
setState((prevState): ToastState => {
|
|
44
|
-
const idx = prevState.findIndex((t) => t.id === id)
|
|
97
|
+
const dismiss = () => {
|
|
98
|
+
const timeoutId = toastsRef.current[id]?.timeoutId
|
|
45
99
|
|
|
46
|
-
|
|
47
|
-
|
|
100
|
+
setState((prevState): ToastState => {
|
|
101
|
+
const idx = prevState.findIndex((t) => t.id === id)
|
|
48
102
|
|
|
49
|
-
|
|
103
|
+
if (idx > -1) {
|
|
104
|
+
const toasts = prevState.slice(0)
|
|
50
105
|
|
|
51
|
-
|
|
52
|
-
}
|
|
106
|
+
toasts.splice(idx, 1)
|
|
53
107
|
|
|
54
|
-
|
|
55
|
-
})
|
|
56
|
-
})
|
|
108
|
+
return toasts
|
|
57
109
|
}
|
|
58
110
|
|
|
59
111
|
return prevState
|
|
60
|
-
.filter((t) => t.id !== id)
|
|
61
|
-
.concat([
|
|
62
|
-
{
|
|
63
|
-
dismiss,
|
|
64
|
-
id,
|
|
65
|
-
updatedAt: Date.now(),
|
|
66
|
-
params: {...params, duration},
|
|
67
|
-
},
|
|
68
|
-
])
|
|
69
112
|
})
|
|
113
|
+
|
|
114
|
+
if (timeoutId !== undefined) {
|
|
115
|
+
clearTimeout(timeoutId)
|
|
116
|
+
delete toastsRef.current[id]
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
setState((prevState): ToastState => {
|
|
121
|
+
return prevState
|
|
122
|
+
.filter((t) => t.id !== id)
|
|
123
|
+
.concat([
|
|
124
|
+
{
|
|
125
|
+
dismiss,
|
|
126
|
+
id,
|
|
127
|
+
params: {...params, duration},
|
|
128
|
+
},
|
|
129
|
+
])
|
|
70
130
|
})
|
|
71
131
|
|
|
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
|
+
|
|
72
139
|
return id
|
|
73
140
|
}
|
|
74
141
|
|
|
75
142
|
return {version: 0.0, push}
|
|
76
143
|
}, [])
|
|
77
144
|
|
|
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
|
+
|
|
78
157
|
return (
|
|
79
158
|
<ToastContext.Provider value={value}>
|
|
80
159
|
{children}
|
|
81
160
|
{mounted && (
|
|
82
|
-
<
|
|
83
|
-
<
|
|
84
|
-
<
|
|
85
|
-
{
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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>
|
|
100
193
|
)}
|
|
101
194
|
</ToastContext.Provider>
|
|
102
195
|
)
|