@charcoal-ui/react 6.1.0-beta.0 → 6.1.0-beta.1
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/components/Notification/NotificationItem.cjs +30 -0
- package/dist/components/Notification/NotificationItem.cjs.map +1 -0
- package/dist/components/Notification/NotificationItem.d.ts +15 -0
- package/dist/components/Notification/NotificationItem.d.ts.map +1 -0
- package/dist/components/Notification/NotificationItem.js +30 -0
- package/dist/components/Notification/NotificationItem.js.map +1 -0
- package/dist/components/Notification/NotificationRegion.cjs +50 -0
- package/dist/components/Notification/NotificationRegion.cjs.map +1 -0
- package/dist/components/Notification/NotificationRegion.d.ts +14 -0
- package/dist/components/Notification/NotificationRegion.d.ts.map +1 -0
- package/dist/components/Notification/NotificationRegion.js +50 -0
- package/dist/components/Notification/NotificationRegion.js.map +1 -0
- package/dist/components/Notification/Snackbar/index.d.ts +45 -0
- package/dist/components/Notification/Snackbar/index.d.ts.map +1 -0
- package/dist/components/Notification/Snackbar/index2.cjs +99 -0
- package/dist/components/Notification/Snackbar/index2.cjs.map +1 -0
- package/dist/components/Notification/Snackbar/index2.js +94 -0
- package/dist/components/Notification/Snackbar/index2.js.map +1 -0
- package/dist/components/Notification/Toast/index.d.ts +28 -0
- package/dist/components/Notification/Toast/index.d.ts.map +1 -0
- package/dist/components/Notification/Toast/index2.cjs +86 -0
- package/dist/components/Notification/Toast/index2.cjs.map +1 -0
- package/dist/components/Notification/Toast/index2.js +81 -0
- package/dist/components/Notification/Toast/index2.js.map +1 -0
- package/dist/components/Notification/types.d.ts +27 -0
- package/dist/components/Notification/types.d.ts.map +1 -0
- package/dist/components/Notification/useNotificationQueue.cjs +155 -0
- package/dist/components/Notification/useNotificationQueue.cjs.map +1 -0
- package/dist/components/Notification/useNotificationQueue.d.ts +16 -0
- package/dist/components/Notification/useNotificationQueue.d.ts.map +1 -0
- package/dist/components/Notification/useNotificationQueue.js +155 -0
- package/dist/components/Notification/useNotificationQueue.js.map +1 -0
- package/dist/index.cjs +4 -1
- package/dist/index.css +1315 -1250
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/layered.css +1315 -1250
- package/dist/layered.css.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/css-output.test.ts +39 -6
- package/src/components/Notification/NotificationItem.tsx +51 -0
- package/src/components/Notification/NotificationRegion.tsx +76 -0
- package/src/components/{Snackbar → Notification/Snackbar}/__snapshots__/index.css.snap +1 -42
- package/src/components/{Snackbar → Notification/Snackbar}/index.css +1 -43
- package/src/components/{Snackbar → Notification/Snackbar}/index.story.tsx +52 -23
- package/src/components/{Snackbar → Notification/Snackbar}/index.test.tsx +61 -11
- package/src/components/Notification/Snackbar/index.tsx +131 -0
- package/src/components/Notification/Toast/__snapshots__/index.css.snap +62 -0
- package/src/components/Notification/Toast/index.css +61 -0
- package/src/components/Notification/Toast/index.story.tsx +174 -0
- package/src/components/Notification/Toast/index.test.tsx +263 -0
- package/src/components/Notification/Toast/index.tsx +86 -0
- package/src/components/Notification/__snapshots__/layout.css.snap +47 -0
- package/src/components/Notification/layout.css +46 -0
- package/src/components/Notification/types.ts +28 -0
- package/src/components/Notification/useNotificationQueue.ts +235 -0
- package/src/index.ts +14 -2
- package/dist/components/Snackbar/index.d.ts +0 -52
- package/dist/components/Snackbar/index.d.ts.map +0 -1
- package/dist/components/Snackbar/index2.cjs +0 -268
- package/dist/components/Snackbar/index2.cjs.map +0 -1
- package/dist/components/Snackbar/index2.js +0 -263
- package/dist/components/Snackbar/index2.js.map +0 -1
- package/src/components/Snackbar/index.tsx +0 -455
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
.charcoal-snackbar-region {
|
|
2
2
|
--charcoal-snackbar-offset: 16px;
|
|
3
|
-
position: fixed;
|
|
4
|
-
left: 0;
|
|
5
|
-
right: 0;
|
|
6
|
-
display: grid;
|
|
7
|
-
justify-content: center;
|
|
8
|
-
pointer-events: none;
|
|
9
|
-
box-sizing: border-box;
|
|
10
3
|
}
|
|
11
4
|
|
|
12
5
|
.charcoal-snackbar-region[data-position='top'] {
|
|
@@ -25,17 +18,8 @@
|
|
|
25
18
|
}
|
|
26
19
|
|
|
27
20
|
.charcoal-snackbar {
|
|
28
|
-
box-sizing: border-box;
|
|
29
|
-
border-radius: var(--charcoal-radius-oval);
|
|
30
|
-
width: fit-content;
|
|
31
|
-
max-width: calc(100vw - var(--charcoal-space-46));
|
|
32
21
|
min-height: 44px;
|
|
33
22
|
padding: var(--charcoal-space-25) var(--charcoal-space-40);
|
|
34
|
-
display: flex;
|
|
35
|
-
align-items: center;
|
|
36
|
-
justify-content: center;
|
|
37
|
-
gap: var(--charcoal-space-20);
|
|
38
|
-
pointer-events: auto;
|
|
39
23
|
background-color: var(--charcoal-color-container-default);
|
|
40
24
|
color: var(--charcoal-color-text-default);
|
|
41
25
|
border: var(--charcoal-border-width-m) solid
|
|
@@ -47,7 +31,7 @@
|
|
|
47
31
|
animation: charcoal-snackbar-exit 0.3s ease-out both;
|
|
48
32
|
}
|
|
49
33
|
|
|
50
|
-
.charcoal-snackbar[data-with-
|
|
34
|
+
.charcoal-snackbar[data-with-action='true'] {
|
|
51
35
|
min-height: 56px;
|
|
52
36
|
padding: var(--charcoal-space-25) var(--charcoal-space-30)
|
|
53
37
|
var(--charcoal-space-25) var(--charcoal-space-40);
|
|
@@ -59,24 +43,6 @@
|
|
|
59
43
|
border-color: var(--charcoal-color-border-hud);
|
|
60
44
|
}
|
|
61
45
|
|
|
62
|
-
.charcoal-snackbar-content {
|
|
63
|
-
display: flex;
|
|
64
|
-
align-items: center;
|
|
65
|
-
min-width: 0;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.charcoal-snackbar-label {
|
|
69
|
-
min-width: 0;
|
|
70
|
-
overflow: hidden;
|
|
71
|
-
overflow-wrap: break-word;
|
|
72
|
-
display: -webkit-box;
|
|
73
|
-
-webkit-box-orient: vertical;
|
|
74
|
-
-webkit-line-clamp: 2;
|
|
75
|
-
font-size: var(--charcoal-text-font-size-caption-m);
|
|
76
|
-
font-weight: var(--charcoal-text-font-weight-bold);
|
|
77
|
-
line-height: var(--charcoal-text-line-height-caption-m);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
46
|
@keyframes charcoal-snackbar-enter {
|
|
81
47
|
|
|
82
48
|
from {
|
|
@@ -101,10 +67,3 @@ to {
|
|
|
101
67
|
}
|
|
102
68
|
}
|
|
103
69
|
|
|
104
|
-
@media (prefers-reduced-motion: reduce) {
|
|
105
|
-
|
|
106
|
-
.charcoal-snackbar, .charcoal-snackbar[data-exiting='true'] {
|
|
107
|
-
animation: none;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
.charcoal-snackbar-region {
|
|
2
2
|
--charcoal-snackbar-offset: 16px;
|
|
3
3
|
|
|
4
|
-
position: fixed;
|
|
5
|
-
left: 0;
|
|
6
|
-
right: 0;
|
|
7
|
-
display: grid;
|
|
8
|
-
justify-content: center;
|
|
9
|
-
pointer-events: none;
|
|
10
|
-
box-sizing: border-box;
|
|
11
|
-
|
|
12
4
|
&[data-position='top'] {
|
|
13
5
|
--charcoal-snackbar-enter-offset: calc(-1 * var(--charcoal-space-30));
|
|
14
6
|
|
|
@@ -28,17 +20,8 @@
|
|
|
28
20
|
}
|
|
29
21
|
|
|
30
22
|
.charcoal-snackbar {
|
|
31
|
-
box-sizing: border-box;
|
|
32
|
-
border-radius: var(--charcoal-radius-oval);
|
|
33
|
-
width: fit-content;
|
|
34
|
-
max-width: calc(100vw - var(--charcoal-space-46));
|
|
35
23
|
min-height: 44px;
|
|
36
24
|
padding: var(--charcoal-space-25) var(--charcoal-space-40);
|
|
37
|
-
display: flex;
|
|
38
|
-
align-items: center;
|
|
39
|
-
justify-content: center;
|
|
40
|
-
gap: var(--charcoal-space-20);
|
|
41
|
-
pointer-events: auto;
|
|
42
25
|
background-color: var(--charcoal-color-container-default);
|
|
43
26
|
color: var(--charcoal-color-text-default);
|
|
44
27
|
border: var(--charcoal-border-width-m) solid
|
|
@@ -49,7 +32,7 @@
|
|
|
49
32
|
animation: charcoal-snackbar-exit 0.3s ease-out both;
|
|
50
33
|
}
|
|
51
34
|
|
|
52
|
-
&[data-with-
|
|
35
|
+
&[data-with-action='true'] {
|
|
53
36
|
min-height: 56px;
|
|
54
37
|
padding: var(--charcoal-space-25) var(--charcoal-space-30)
|
|
55
38
|
var(--charcoal-space-25) var(--charcoal-space-40);
|
|
@@ -62,24 +45,6 @@
|
|
|
62
45
|
}
|
|
63
46
|
}
|
|
64
47
|
|
|
65
|
-
.charcoal-snackbar-content {
|
|
66
|
-
display: flex;
|
|
67
|
-
align-items: center;
|
|
68
|
-
min-width: 0;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.charcoal-snackbar-label {
|
|
72
|
-
min-width: 0;
|
|
73
|
-
overflow: hidden;
|
|
74
|
-
overflow-wrap: break-word;
|
|
75
|
-
display: -webkit-box;
|
|
76
|
-
-webkit-box-orient: vertical;
|
|
77
|
-
-webkit-line-clamp: 2;
|
|
78
|
-
font-size: var(--charcoal-text-font-size-caption-m);
|
|
79
|
-
font-weight: var(--charcoal-text-font-weight-bold);
|
|
80
|
-
line-height: var(--charcoal-text-line-height-caption-m);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
48
|
@keyframes charcoal-snackbar-enter {
|
|
84
49
|
from {
|
|
85
50
|
opacity: 0;
|
|
@@ -101,10 +66,3 @@
|
|
|
101
66
|
opacity: 0;
|
|
102
67
|
}
|
|
103
68
|
}
|
|
104
|
-
|
|
105
|
-
@media (prefers-reduced-motion: reduce) {
|
|
106
|
-
.charcoal-snackbar,
|
|
107
|
-
.charcoal-snackbar[data-exiting='true'] {
|
|
108
|
-
animation: none;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, type ReactNode } from 'react'
|
|
2
2
|
import { Meta, StoryObj } from '@storybook/react-vite'
|
|
3
|
-
import Button from '
|
|
3
|
+
import Button from '../../Button'
|
|
4
4
|
import unstable_Snackbar, {
|
|
5
5
|
useSnackbar as unstable_useSnackbar,
|
|
6
6
|
type SnackbarProps as unstable_SnackbarProps,
|
|
@@ -10,8 +10,7 @@ const defaultOpen = !!process.env.TEST
|
|
|
10
10
|
|
|
11
11
|
type SnackbarStoryArgs = unstable_SnackbarProps & {
|
|
12
12
|
message: ReactNode
|
|
13
|
-
|
|
14
|
-
duration?: number
|
|
13
|
+
actionChildren?: string
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
export default {
|
|
@@ -20,9 +19,12 @@ export default {
|
|
|
20
19
|
parameters: {
|
|
21
20
|
layout: 'centered',
|
|
22
21
|
tokenVersion: 'v2',
|
|
22
|
+
controls: {
|
|
23
|
+
sort: 'requiredFirst',
|
|
24
|
+
},
|
|
23
25
|
docs: {
|
|
24
26
|
description: {
|
|
25
|
-
component: `同時に表示できるスナックバーは1
|
|
27
|
+
component: `同時に表示できるスナックバーは1つのみです。表示中に別のスナックバーが発生した場合は、\`order\` に応じてキューに積むか、表示中のスナックバーを置き換えます。
|
|
26
28
|
|
|
27
29
|
別々の \`useSnackbar\` を呼び出した場合は互いに独立するため、同時表示数は1件に制限されません。`,
|
|
28
30
|
},
|
|
@@ -30,23 +32,55 @@ export default {
|
|
|
30
32
|
},
|
|
31
33
|
args: {
|
|
32
34
|
message: '保存しました',
|
|
33
|
-
|
|
35
|
+
order: 'queue',
|
|
34
36
|
},
|
|
35
37
|
argTypes: {
|
|
36
38
|
position: {
|
|
37
39
|
options: ['top', 'bottom'],
|
|
38
40
|
control: { type: 'inline-radio' },
|
|
41
|
+
table: { category: 'Hook' },
|
|
39
42
|
},
|
|
40
|
-
|
|
41
|
-
control: '
|
|
43
|
+
offset: {
|
|
44
|
+
control: { type: 'number', min: 0, step: 1 },
|
|
45
|
+
table: { category: 'Hook' },
|
|
42
46
|
},
|
|
43
47
|
duration: {
|
|
44
48
|
control: { type: 'number', min: 0, step: 500 },
|
|
45
49
|
description: '表示時間(ミリ秒)',
|
|
50
|
+
table: { category: 'Hook' },
|
|
51
|
+
},
|
|
52
|
+
order: {
|
|
53
|
+
options: ['queue', 'replace'],
|
|
54
|
+
control: { type: 'inline-radio' },
|
|
55
|
+
table: { category: 'Hook' },
|
|
56
|
+
},
|
|
57
|
+
dim: {
|
|
58
|
+
control: 'boolean',
|
|
59
|
+
table: { category: 'Hook' },
|
|
60
|
+
},
|
|
61
|
+
zIndex: {
|
|
62
|
+
control: { type: 'number', min: 0, step: 1 },
|
|
63
|
+
table: { category: 'Hook' },
|
|
64
|
+
},
|
|
65
|
+
className: {
|
|
66
|
+
control: 'text',
|
|
67
|
+
table: { category: 'Hook' },
|
|
68
|
+
},
|
|
69
|
+
portalContainer: {
|
|
70
|
+
control: false,
|
|
71
|
+
table: { category: 'Hook' },
|
|
72
|
+
},
|
|
73
|
+
css: {
|
|
74
|
+
table: { disable: true },
|
|
75
|
+
},
|
|
76
|
+
message: {
|
|
77
|
+
control: 'text',
|
|
78
|
+
table: { category: 'Show' },
|
|
46
79
|
},
|
|
47
|
-
|
|
48
|
-
name: '
|
|
80
|
+
actionChildren: {
|
|
81
|
+
name: 'action',
|
|
49
82
|
control: 'text',
|
|
83
|
+
table: { category: 'Show' },
|
|
50
84
|
},
|
|
51
85
|
},
|
|
52
86
|
render: (args) => <SnackbarDemo {...args} />,
|
|
@@ -54,15 +88,13 @@ export default {
|
|
|
54
88
|
|
|
55
89
|
function SnackbarDemo({
|
|
56
90
|
message,
|
|
57
|
-
|
|
58
|
-
duration,
|
|
91
|
+
actionChildren,
|
|
59
92
|
...props
|
|
60
93
|
}: SnackbarStoryArgs) {
|
|
61
94
|
const [snackbar, showSnackbar] = unstable_useSnackbar(props)
|
|
62
|
-
const
|
|
95
|
+
const hasAction = actionChildren !== undefined && actionChildren !== ''
|
|
63
96
|
const showOptions = {
|
|
64
|
-
|
|
65
|
-
...(hasButton ? { button: { children: buttonChildren } } : {}),
|
|
97
|
+
...(hasAction ? { action: <Button>{actionChildren}</Button> } : {}),
|
|
66
98
|
}
|
|
67
99
|
|
|
68
100
|
useEffect(() => {
|
|
@@ -70,10 +102,9 @@ function SnackbarDemo({
|
|
|
70
102
|
return
|
|
71
103
|
}
|
|
72
104
|
showSnackbar(message, {
|
|
73
|
-
|
|
74
|
-
...(hasButton ? { button: { children: buttonChildren } } : {}),
|
|
105
|
+
...(hasAction ? { action: <Button>{actionChildren}</Button> } : {}),
|
|
75
106
|
})
|
|
76
|
-
}, [
|
|
107
|
+
}, [actionChildren, hasAction, message, showSnackbar])
|
|
77
108
|
|
|
78
109
|
return (
|
|
79
110
|
<>
|
|
@@ -132,10 +163,10 @@ export const WithLineBreak: StoryObj<SnackbarStoryArgs> = {
|
|
|
132
163
|
},
|
|
133
164
|
}
|
|
134
165
|
|
|
135
|
-
export const
|
|
166
|
+
export const WithAction: StoryObj<SnackbarStoryArgs> = {
|
|
136
167
|
args: {
|
|
137
168
|
message: '保存しました',
|
|
138
|
-
|
|
169
|
+
actionChildren: '取り消す',
|
|
139
170
|
},
|
|
140
171
|
parameters: {
|
|
141
172
|
docs: {
|
|
@@ -152,9 +183,7 @@ export function Example() {
|
|
|
152
183
|
<Button
|
|
153
184
|
onClick={() =>
|
|
154
185
|
showSnackbar('保存しました', {
|
|
155
|
-
|
|
156
|
-
children: '取り消す',
|
|
157
|
-
},
|
|
186
|
+
action: <Button>取り消す</Button>,
|
|
158
187
|
})
|
|
159
188
|
}
|
|
160
189
|
>
|
|
@@ -179,6 +208,6 @@ export const Dim: StoryObj<SnackbarStoryArgs> = {
|
|
|
179
208
|
args: {
|
|
180
209
|
dim: true,
|
|
181
210
|
message: 'Dim の Snackbar',
|
|
182
|
-
|
|
211
|
+
actionChildren: '閉じる',
|
|
183
212
|
},
|
|
184
213
|
}
|
|
@@ -62,9 +62,9 @@ describe('Snackbar', () => {
|
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
it.each([0, -1])('clamps duration to zero: %s', (duration) => {
|
|
65
|
-
const { show } = renderSnackbar()
|
|
65
|
+
const { show } = renderSnackbar({ duration })
|
|
66
66
|
|
|
67
|
-
show('保存しました'
|
|
67
|
+
show('保存しました')
|
|
68
68
|
expect(screen.getByText('保存しました')).toBeInTheDocument()
|
|
69
69
|
|
|
70
70
|
act(() => {
|
|
@@ -84,9 +84,9 @@ describe('Snackbar', () => {
|
|
|
84
84
|
Number.NEGATIVE_INFINITY,
|
|
85
85
|
'invalid' as unknown as number,
|
|
86
86
|
])('falls back to the default duration: %s', (duration) => {
|
|
87
|
-
const { show } = renderSnackbar()
|
|
87
|
+
const { show } = renderSnackbar({ duration })
|
|
88
88
|
|
|
89
|
-
show('保存しました'
|
|
89
|
+
show('保存しました')
|
|
90
90
|
act(() => {
|
|
91
91
|
vi.advanceTimersByTime(4999)
|
|
92
92
|
})
|
|
@@ -120,6 +120,56 @@ describe('Snackbar', () => {
|
|
|
120
120
|
expect(screen.getByText('second')).toBeInTheDocument()
|
|
121
121
|
})
|
|
122
122
|
|
|
123
|
+
it('replaces the current snackbar immediately when requested', () => {
|
|
124
|
+
const onClose = vi.fn()
|
|
125
|
+
const { show } = renderSnackbar({ order: 'replace' })
|
|
126
|
+
|
|
127
|
+
show('first', { onClose })
|
|
128
|
+
show('second')
|
|
129
|
+
|
|
130
|
+
expect(screen.queryByText('first')).not.toBeInTheDocument()
|
|
131
|
+
expect(screen.getByText('second')).toBeInTheDocument()
|
|
132
|
+
expect(onClose).toHaveBeenCalledExactlyOnceWith('replaced')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('reports timeout when the snackbar closes after its duration', () => {
|
|
136
|
+
const onClose = vi.fn()
|
|
137
|
+
const { show } = renderSnackbar({ duration: 0 })
|
|
138
|
+
|
|
139
|
+
show('保存しました', { onClose })
|
|
140
|
+
act(() => {
|
|
141
|
+
vi.advanceTimersByTime(1)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
expect(onClose).toHaveBeenCalledExactlyOnceWith('timeout')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('reports action when the action is clicked', () => {
|
|
148
|
+
const onClose = vi.fn()
|
|
149
|
+
const { show } = renderSnackbar()
|
|
150
|
+
|
|
151
|
+
show('保存しました', {
|
|
152
|
+
action: <button type="button">取り消す</button>,
|
|
153
|
+
onClose,
|
|
154
|
+
})
|
|
155
|
+
fireEvent.click(screen.getByRole('button', { name: '取り消す' }))
|
|
156
|
+
|
|
157
|
+
expect(onClose).toHaveBeenCalledExactlyOnceWith('action')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('reports unmounted only for the currently displayed snackbar', () => {
|
|
161
|
+
const firstOnClose = vi.fn()
|
|
162
|
+
const secondOnClose = vi.fn()
|
|
163
|
+
const { show, unmount } = renderSnackbar()
|
|
164
|
+
|
|
165
|
+
show('first', { onClose: firstOnClose })
|
|
166
|
+
show('second', { onClose: secondOnClose })
|
|
167
|
+
unmount()
|
|
168
|
+
|
|
169
|
+
expect(firstOnClose).toHaveBeenCalledExactlyOnceWith('unmounted')
|
|
170
|
+
expect(secondOnClose).not.toHaveBeenCalled()
|
|
171
|
+
})
|
|
172
|
+
|
|
123
173
|
it('keeps the snackbar open while hovered after the timer ends, then closes on leave', () => {
|
|
124
174
|
const { show } = renderSnackbar()
|
|
125
175
|
|
|
@@ -163,11 +213,11 @@ describe('Snackbar', () => {
|
|
|
163
213
|
expect(snackbar).toHaveAttribute('data-exiting', 'true')
|
|
164
214
|
})
|
|
165
215
|
|
|
166
|
-
it('places a snackbar with
|
|
216
|
+
it('places a snackbar with an action at the bottom', () => {
|
|
167
217
|
const { show } = renderSnackbar({ position: 'top' })
|
|
168
218
|
|
|
169
219
|
show('保存しました', {
|
|
170
|
-
|
|
220
|
+
action: <button type="button">取り消す</button>,
|
|
171
221
|
})
|
|
172
222
|
|
|
173
223
|
expect(screen.getByRole('region')).toHaveAttribute(
|
|
@@ -176,7 +226,7 @@ describe('Snackbar', () => {
|
|
|
176
226
|
)
|
|
177
227
|
})
|
|
178
228
|
|
|
179
|
-
it('places a snackbar without
|
|
229
|
+
it('places a snackbar without an action at the top when requested', () => {
|
|
180
230
|
const { show } = renderSnackbar({ position: 'top' })
|
|
181
231
|
|
|
182
232
|
show('保存しました')
|
|
@@ -201,7 +251,7 @@ describe('Snackbar', () => {
|
|
|
201
251
|
const { show } = renderSnackbar()
|
|
202
252
|
|
|
203
253
|
show('保存しました', {
|
|
204
|
-
|
|
254
|
+
action: <button type="button">取り消す</button>,
|
|
205
255
|
})
|
|
206
256
|
|
|
207
257
|
fireEvent.keyDown(document, { key: 'F6' })
|
|
@@ -216,7 +266,7 @@ describe('Snackbar', () => {
|
|
|
216
266
|
trigger.focus()
|
|
217
267
|
|
|
218
268
|
show('保存しました', {
|
|
219
|
-
|
|
269
|
+
action: <button type="button">閉じる</button>,
|
|
220
270
|
})
|
|
221
271
|
|
|
222
272
|
fireEvent.keyDown(document, { key: 'F6' })
|
|
@@ -229,14 +279,14 @@ describe('Snackbar', () => {
|
|
|
229
279
|
trigger.remove()
|
|
230
280
|
})
|
|
231
281
|
|
|
232
|
-
it('closes on
|
|
282
|
+
it('closes on action click even while hovered', () => {
|
|
233
283
|
const { show } = renderSnackbar()
|
|
234
284
|
const trigger = document.createElement('button')
|
|
235
285
|
document.body.append(trigger)
|
|
236
286
|
trigger.focus()
|
|
237
287
|
|
|
238
288
|
show('保存しました', {
|
|
239
|
-
|
|
289
|
+
action: <button type="button">閉じる</button>,
|
|
240
290
|
})
|
|
241
291
|
|
|
242
292
|
const snackbar = screen
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import '../layout.css'
|
|
2
|
+
import './index.css'
|
|
3
|
+
|
|
4
|
+
import { forwardRef, useImperativeHandle, useRef } from 'react'
|
|
5
|
+
import type { ReactNode } from 'react'
|
|
6
|
+
import { NotificationItem } from '../NotificationItem'
|
|
7
|
+
import { NotificationRegion } from '../NotificationRegion'
|
|
8
|
+
import { useNotificationQueue } from '../useNotificationQueue'
|
|
9
|
+
import type { NotificationProps, Position } from '../types'
|
|
10
|
+
|
|
11
|
+
export type SnackbarCloseReason =
|
|
12
|
+
'timeout' | 'replaced' | 'action' | 'close' | 'unmounted'
|
|
13
|
+
|
|
14
|
+
export type ShowSnackbarOptions = {
|
|
15
|
+
/**
|
|
16
|
+
* Snackbar の右側に表示するアクション
|
|
17
|
+
*/
|
|
18
|
+
action?: ReactNode
|
|
19
|
+
/**
|
|
20
|
+
* Snackbar が閉じるときに呼び出される
|
|
21
|
+
*/
|
|
22
|
+
onClose?: (reason: SnackbarCloseReason) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type SnackbarHandler = {
|
|
26
|
+
show: (message: ReactNode, options?: ShowSnackbarOptions) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type SnackbarProps = Omit<NotificationProps, 'position'> & {
|
|
30
|
+
/**
|
|
31
|
+
* Snackbar の表示位置。ボタン付きの場合は `bottom` に固定される
|
|
32
|
+
* @default 'bottom'
|
|
33
|
+
*/
|
|
34
|
+
position?: Position
|
|
35
|
+
/**
|
|
36
|
+
* 暗い背景色
|
|
37
|
+
* @default false
|
|
38
|
+
*/
|
|
39
|
+
dim?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type SnackbarContent = {
|
|
43
|
+
message: ReactNode
|
|
44
|
+
action?: ReactNode
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const Snackbar = forwardRef<SnackbarHandler, SnackbarProps>(function Snackbar(
|
|
48
|
+
{ position = 'bottom', dim = false, duration, order, ...regionProps },
|
|
49
|
+
ref,
|
|
50
|
+
) {
|
|
51
|
+
'use memo'
|
|
52
|
+
|
|
53
|
+
const { state, itemRef, enqueue, close, onHoverStart, onHoverEnd } =
|
|
54
|
+
useNotificationQueue<SnackbarContent, SnackbarCloseReason>('snackbar', {
|
|
55
|
+
duration,
|
|
56
|
+
order,
|
|
57
|
+
timeoutReason: 'timeout',
|
|
58
|
+
unmountedReason: 'unmounted',
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
function show(message: ReactNode, options: ShowSnackbarOptions = {}) {
|
|
62
|
+
enqueue(
|
|
63
|
+
{
|
|
64
|
+
message,
|
|
65
|
+
action: options.action,
|
|
66
|
+
},
|
|
67
|
+
options.onClose,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
useImperativeHandle(ref, () => ({ show }))
|
|
72
|
+
|
|
73
|
+
// アクション付きの Snackbar は常に下部に表示される
|
|
74
|
+
const hasAction = state.visibleToasts.some(
|
|
75
|
+
(toast) => toast.content.action !== undefined,
|
|
76
|
+
)
|
|
77
|
+
const effectivePosition = hasAction ? 'bottom' : position
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<NotificationRegion
|
|
81
|
+
name="snackbar"
|
|
82
|
+
state={state}
|
|
83
|
+
position={effectivePosition}
|
|
84
|
+
{...regionProps}
|
|
85
|
+
>
|
|
86
|
+
{state.visibleToasts.map((toast) => (
|
|
87
|
+
<NotificationItem
|
|
88
|
+
key={toast.key}
|
|
89
|
+
name="snackbar"
|
|
90
|
+
toast={toast}
|
|
91
|
+
state={state}
|
|
92
|
+
itemRef={itemRef}
|
|
93
|
+
onHoverStart={onHoverStart}
|
|
94
|
+
onHoverEnd={onHoverEnd}
|
|
95
|
+
data-dim={dim}
|
|
96
|
+
data-with-action={toast.content.action !== undefined}
|
|
97
|
+
>
|
|
98
|
+
{toast.content.action !== undefined && (
|
|
99
|
+
<SnackbarAction
|
|
100
|
+
action={toast.content.action}
|
|
101
|
+
onClick={() => close(toast.key, 'action')}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
</NotificationItem>
|
|
105
|
+
))}
|
|
106
|
+
</NotificationRegion>
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
export default Snackbar
|
|
111
|
+
|
|
112
|
+
export function useSnackbar(props: SnackbarProps = {}) {
|
|
113
|
+
'use memo'
|
|
114
|
+
|
|
115
|
+
const snackbarHandlerRef = useRef<SnackbarHandler>(null)
|
|
116
|
+
const element = <Snackbar ref={snackbarHandlerRef} {...props} />
|
|
117
|
+
function show(message: ReactNode, options?: ShowSnackbarOptions) {
|
|
118
|
+
snackbarHandlerRef.current?.show(message, options)
|
|
119
|
+
}
|
|
120
|
+
return [element, show] as const
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function SnackbarAction({
|
|
124
|
+
action,
|
|
125
|
+
onClick,
|
|
126
|
+
}: {
|
|
127
|
+
action: ReactNode
|
|
128
|
+
onClick: () => void
|
|
129
|
+
}) {
|
|
130
|
+
return <div onClick={onClick}>{action}</div>
|
|
131
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
.charcoal-toast-region {
|
|
2
|
+
--charcoal-toast-offset: 16px;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.charcoal-toast-region[data-position='top'] {
|
|
6
|
+
--charcoal-toast-enter-offset: calc(-1 * var(--charcoal-space-30));
|
|
7
|
+
top: calc(
|
|
8
|
+
env(safe-area-inset-top, 0px) + max(var(--charcoal-toast-offset), 16px)
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.charcoal-toast-region[data-position='bottom'] {
|
|
13
|
+
--charcoal-toast-enter-offset: var(--charcoal-space-30);
|
|
14
|
+
bottom: calc(
|
|
15
|
+
env(safe-area-inset-bottom, 0px) + max(var(--charcoal-toast-offset), 16px)
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.charcoal-toast {
|
|
20
|
+
min-height: 40px;
|
|
21
|
+
padding: var(--charcoal-space-20) var(--charcoal-space-40);
|
|
22
|
+
color: var(--charcoal-color-text-on-positive-default);
|
|
23
|
+
border: var(--charcoal-border-width-m) solid var(--charcoal-color-border-hud);
|
|
24
|
+
animation: charcoal-toast-enter 0.3s ease-out both;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.charcoal-toast[data-exiting='true'] {
|
|
28
|
+
animation: charcoal-toast-exit 0.3s ease-out both;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.charcoal-toast[data-type='success'] {
|
|
32
|
+
background-color: var(--charcoal-color-container-positive-default);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.charcoal-toast[data-type='error'] {
|
|
36
|
+
background-color: var(--charcoal-color-container-negative-default);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@keyframes charcoal-toast-enter {
|
|
40
|
+
|
|
41
|
+
from {
|
|
42
|
+
opacity: 0;
|
|
43
|
+
transform: translateY(var(--charcoal-toast-enter-offset));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
to {
|
|
47
|
+
opacity: 1;
|
|
48
|
+
transform: translateY(0);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@keyframes charcoal-toast-exit {
|
|
53
|
+
|
|
54
|
+
from {
|
|
55
|
+
opacity: 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
to {
|
|
59
|
+
opacity: 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
.charcoal-toast-region {
|
|
2
|
+
--charcoal-toast-offset: 16px;
|
|
3
|
+
|
|
4
|
+
&[data-position='top'] {
|
|
5
|
+
--charcoal-toast-enter-offset: calc(-1 * var(--charcoal-space-30));
|
|
6
|
+
|
|
7
|
+
top: calc(
|
|
8
|
+
env(safe-area-inset-top, 0px) + max(var(--charcoal-toast-offset), 16px)
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
&[data-position='bottom'] {
|
|
13
|
+
--charcoal-toast-enter-offset: var(--charcoal-space-30);
|
|
14
|
+
|
|
15
|
+
bottom: calc(
|
|
16
|
+
env(safe-area-inset-bottom, 0px) + max(var(--charcoal-toast-offset), 16px)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.charcoal-toast {
|
|
22
|
+
min-height: 40px;
|
|
23
|
+
padding: var(--charcoal-space-20) var(--charcoal-space-40);
|
|
24
|
+
color: var(--charcoal-color-text-on-positive-default);
|
|
25
|
+
border: var(--charcoal-border-width-m) solid var(--charcoal-color-border-hud);
|
|
26
|
+
animation: charcoal-toast-enter 0.3s ease-out both;
|
|
27
|
+
|
|
28
|
+
&[data-exiting='true'] {
|
|
29
|
+
animation: charcoal-toast-exit 0.3s ease-out both;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&[data-type='success'] {
|
|
33
|
+
background-color: var(--charcoal-color-container-positive-default);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
&[data-type='error'] {
|
|
37
|
+
background-color: var(--charcoal-color-container-negative-default);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@keyframes charcoal-toast-enter {
|
|
42
|
+
from {
|
|
43
|
+
opacity: 0;
|
|
44
|
+
transform: translateY(var(--charcoal-toast-enter-offset));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
to {
|
|
48
|
+
opacity: 1;
|
|
49
|
+
transform: translateY(0);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@keyframes charcoal-toast-exit {
|
|
54
|
+
from {
|
|
55
|
+
opacity: 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
to {
|
|
59
|
+
opacity: 0;
|
|
60
|
+
}
|
|
61
|
+
}
|