@charcoal-ui/react 6.1.0-beta.0 → 6.1.0-beta.2

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.
Files changed (67) hide show
  1. package/dist/components/Notification/NotificationItem.cjs +30 -0
  2. package/dist/components/Notification/NotificationItem.cjs.map +1 -0
  3. package/dist/components/Notification/NotificationItem.d.ts +15 -0
  4. package/dist/components/Notification/NotificationItem.d.ts.map +1 -0
  5. package/dist/components/Notification/NotificationItem.js +30 -0
  6. package/dist/components/Notification/NotificationItem.js.map +1 -0
  7. package/dist/components/Notification/NotificationRegion.cjs +50 -0
  8. package/dist/components/Notification/NotificationRegion.cjs.map +1 -0
  9. package/dist/components/Notification/NotificationRegion.d.ts +14 -0
  10. package/dist/components/Notification/NotificationRegion.d.ts.map +1 -0
  11. package/dist/components/Notification/NotificationRegion.js +50 -0
  12. package/dist/components/Notification/NotificationRegion.js.map +1 -0
  13. package/dist/components/Notification/Snackbar/index.d.ts +29 -0
  14. package/dist/components/Notification/Snackbar/index.d.ts.map +1 -0
  15. package/dist/components/Notification/Snackbar/index2.cjs +79 -0
  16. package/dist/components/Notification/Snackbar/index2.cjs.map +1 -0
  17. package/dist/components/Notification/Snackbar/index2.js +78 -0
  18. package/dist/components/Notification/Snackbar/index2.js.map +1 -0
  19. package/dist/components/Notification/Toast/index.d.ts +28 -0
  20. package/dist/components/Notification/Toast/index.d.ts.map +1 -0
  21. package/dist/components/Notification/Toast/index2.cjs +86 -0
  22. package/dist/components/Notification/Toast/index2.cjs.map +1 -0
  23. package/dist/components/Notification/Toast/index2.js +85 -0
  24. package/dist/components/Notification/Toast/index2.js.map +1 -0
  25. package/dist/components/Notification/types.d.ts +27 -0
  26. package/dist/components/Notification/types.d.ts.map +1 -0
  27. package/dist/components/Notification/useNotificationQueue.cjs +155 -0
  28. package/dist/components/Notification/useNotificationQueue.cjs.map +1 -0
  29. package/dist/components/Notification/useNotificationQueue.d.ts +16 -0
  30. package/dist/components/Notification/useNotificationQueue.d.ts.map +1 -0
  31. package/dist/components/Notification/useNotificationQueue.js +155 -0
  32. package/dist/components/Notification/useNotificationQueue.js.map +1 -0
  33. package/dist/components/Pagination/index2.js +60 -52
  34. package/dist/components/Pagination/index2.js.map +1 -1
  35. package/dist/index.cjs +5 -2
  36. package/dist/index.css +822 -757
  37. package/dist/index.d.ts +3 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +3 -2
  40. package/dist/layered.css +822 -757
  41. package/dist/layered.css.map +1 -1
  42. package/package.json +5 -5
  43. package/src/__tests__/css-output.test.ts +39 -6
  44. package/src/components/Notification/NotificationItem.tsx +51 -0
  45. package/src/components/Notification/NotificationRegion.tsx +76 -0
  46. package/src/components/{Snackbar → Notification/Snackbar}/__snapshots__/index.css.snap +1 -42
  47. package/src/components/{Snackbar → Notification/Snackbar}/index.css +1 -43
  48. package/src/components/{Snackbar → Notification/Snackbar}/index.story.tsx +70 -34
  49. package/src/components/{Snackbar → Notification/Snackbar}/index.test.tsx +99 -18
  50. package/src/components/Notification/Snackbar/index.tsx +136 -0
  51. package/src/components/Notification/Toast/__snapshots__/index.css.snap +62 -0
  52. package/src/components/Notification/Toast/index.css +61 -0
  53. package/src/components/Notification/Toast/index.story.tsx +177 -0
  54. package/src/components/Notification/Toast/index.test.tsx +263 -0
  55. package/src/components/Notification/Toast/index.tsx +86 -0
  56. package/src/components/Notification/__snapshots__/layout.css.snap +47 -0
  57. package/src/components/Notification/layout.css +46 -0
  58. package/src/components/Notification/types.ts +28 -0
  59. package/src/components/Notification/useNotificationQueue.ts +235 -0
  60. package/src/index.ts +17 -5
  61. package/dist/components/Snackbar/index.d.ts +0 -52
  62. package/dist/components/Snackbar/index.d.ts.map +0 -1
  63. package/dist/components/Snackbar/index2.cjs +0 -268
  64. package/dist/components/Snackbar/index2.cjs.map +0 -1
  65. package/dist/components/Snackbar/index2.js +0 -263
  66. package/dist/components/Snackbar/index2.js.map +0 -1
  67. package/src/components/Snackbar/index.tsx +0 -455
@@ -1,455 +0,0 @@
1
- import './index.css'
2
-
3
- import {
4
- forwardRef,
5
- useCallback,
6
- useEffect,
7
- useImperativeHandle,
8
- useRef,
9
- } from 'react'
10
- import type { ElementType, ReactNode, RefObject } from 'react'
11
- import { Overlay } from 'react-aria/Overlay'
12
- import { useToast, useToastRegion } from 'react-aria/useToast'
13
- import {
14
- useToastState,
15
- type QueuedToast,
16
- type ToastState,
17
- } from 'react-stately/useToastState'
18
- import { useClassNames } from '../../_lib/useClassNames'
19
- import Button, { type ButtonProps } from '../Button'
20
-
21
- const DEFAULT_DURATION_MS = 5000
22
- const DEFAULT_Z_INDEX = 20
23
- const ENTER_ANIMATION_DURATION_MS = 300
24
- const EXIT_ANIMATION_DURATION_MS = 300
25
-
26
- type SnackbarPosition = 'top' | 'bottom'
27
-
28
- type SnackbarButtonOption<T extends ElementType = 'button'> = Omit<
29
- ButtonProps<T>,
30
- 'component' | 'className' | 'size'
31
- > & {
32
- /**
33
- * ボタンのルート要素として使用するコンポーネント。ページ遷移を伴う場合は `Link` を指定する
34
- * @default 'button'
35
- * @example 'Link'
36
- */
37
- component?: T
38
- } & ('button' extends T ? unknown : { component: T })
39
-
40
- export type SnackbarShowOptions<T extends ElementType = 'button'> = {
41
- /**
42
- * Snackbar を表示する時間(ミリ秒)。負の値は 0、数値でない値は 5000 として扱う
43
- * @default 5000
44
- */
45
- duration?: number
46
- /**
47
- * Snackbar の右側に表示するボタン
48
- */
49
- button?: SnackbarButtonOption<T>
50
- }
51
-
52
- export type SnackbarHandler = {
53
- show: <T extends ElementType = 'button'>(
54
- message: ReactNode,
55
- options?: SnackbarShowOptions<T>,
56
- ) => void
57
- }
58
-
59
- export type SnackbarProps = {
60
- /**
61
- * Snackbar の表示位置。ボタン付きの場合は `bottom` に固定される
62
- * @default 'bottom'
63
- */
64
- position?: SnackbarPosition
65
- /**
66
- * 画面端からの距離(ピクセル)
67
- * @default 16
68
- */
69
- offset?: number
70
- /**
71
- * 暗い背景色
72
- * @default false
73
- */
74
- dim?: boolean
75
- zIndex?: number
76
- portalContainer?: HTMLElement
77
- className?: string
78
- }
79
-
80
- type SnackbarContent = {
81
- message: ReactNode
82
- button?: SnackbarButtonContent
83
- }
84
-
85
- type QueuedSnackbar = SnackbarContent & {
86
- duration: number
87
- }
88
-
89
- type SnackbarButtonContent = Omit<SnackbarButtonOption, 'component'> & {
90
- component?: ElementType
91
- }
92
-
93
- const Snackbar = forwardRef<SnackbarHandler, SnackbarProps>(function Snackbar(
94
- {
95
- position = 'bottom',
96
- offset = 16,
97
- dim = false,
98
- zIndex = DEFAULT_Z_INDEX,
99
- portalContainer,
100
- className,
101
- },
102
- ref,
103
- ) {
104
- 'use memo'
105
-
106
- const snackbarRef = useRef<HTMLDivElement>(null)
107
- const queueRef = useRef<QueuedSnackbar[]>([])
108
- const isShowingRef = useRef(false)
109
- const hoverRef = useRef({
110
- active: false,
111
- pending: undefined as (() => void) | undefined,
112
- })
113
- // wrapUpdate を固定したまま、後から定義する playNext の最新版を呼ぶ
114
- const playNextRef = useRef<(() => void) | undefined>(undefined)
115
-
116
- // wrapUpdate の参照が変わると useToastState が ToastQueue を作り直すため useCallback で固定する
117
- const wrapToastUpdate = useCallback(function wrapToastUpdate(
118
- update: () => void,
119
- action: 'add' | 'remove' | 'clear',
120
- ) {
121
- if (action !== 'remove') {
122
- update()
123
- return
124
- }
125
-
126
- function finish() {
127
- update()
128
- playNextRef.current?.()
129
- }
130
-
131
- if (hoverRef.current.active) {
132
- hoverRef.current.pending = () => wrapToastUpdate(update, 'remove')
133
- return
134
- }
135
-
136
- if (snackbarRef.current === null) {
137
- finish()
138
- return
139
- }
140
- const snackbar: HTMLDivElement = snackbarRef.current
141
-
142
- // allow-discreteが Newly Available で使えないので、要素の削除をアニメーション完了まで待つ
143
- snackbar.dataset.exiting = 'true'
144
- let completed = false
145
- let fallbackTimer = 0 // complete 内で clearTimeout(setTimeout(...)) すると新規タイマーを即キャンセルしてしまう
146
- function handleAnimationEnd(event: AnimationEvent) {
147
- if (
148
- event.target === snackbar &&
149
- event.animationName === 'charcoal-snackbar-exit'
150
- ) {
151
- complete()
152
- }
153
- }
154
- function complete() {
155
- if (completed) return
156
- completed = true
157
- snackbar.removeEventListener('animationend', handleAnimationEnd)
158
- window.clearTimeout(fallbackTimer)
159
- finish()
160
- }
161
- snackbar.addEventListener('animationend', handleAnimationEnd)
162
- fallbackTimer = window.setTimeout(
163
- complete,
164
- EXIT_ANIMATION_DURATION_MS + 100,
165
- )
166
- if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
167
- complete()
168
- }
169
- }, [])
170
-
171
- const state = useToastState<SnackbarContent>({
172
- maxVisibleToasts: 1,
173
- wrapUpdate: wrapToastUpdate,
174
- })
175
-
176
- useEffect(() => {
177
- return () => {
178
- queueRef.current = []
179
- isShowingRef.current = false
180
- }
181
- }, [])
182
-
183
- function playNext() {
184
- const next = queueRef.current.shift()
185
- if (next === undefined) {
186
- isShowingRef.current = false
187
- return
188
- }
189
-
190
- const { duration, ...content } = next
191
- isShowingRef.current = true
192
- const enterMs = window.matchMedia?.('(prefers-reduced-motion: reduce)')
193
- .matches
194
- ? 0
195
- : ENTER_ANIMATION_DURATION_MS
196
- state.add(content, {
197
- // react-stately は 0 を「タイマーなし」と扱うため、最小の正数を渡す
198
- timeout: Math.max(1, duration + enterMs),
199
- })
200
- }
201
- playNextRef.current = playNext
202
-
203
- function show<T extends ElementType = 'button'>(
204
- message: ReactNode,
205
- options: SnackbarShowOptions<T> = {},
206
- ) {
207
- const { duration: durationOption = DEFAULT_DURATION_MS, button } = options
208
- const duration =
209
- typeof durationOption === 'number' && Number.isFinite(durationOption)
210
- ? Math.max(0, durationOption)
211
- : DEFAULT_DURATION_MS
212
-
213
- queueRef.current.push({
214
- message,
215
- button: button as SnackbarButtonContent | undefined,
216
- duration,
217
- })
218
- if (!isShowingRef.current) {
219
- playNext()
220
- }
221
- }
222
-
223
- useImperativeHandle(ref, () => ({ show }))
224
-
225
- return (
226
- <SnackbarRegion
227
- state={state}
228
- position={position}
229
- offset={offset}
230
- dim={dim}
231
- zIndex={zIndex}
232
- portalContainer={portalContainer}
233
- className={className}
234
- snackbarRef={snackbarRef}
235
- onHoverStart={() => {
236
- hoverRef.current.active = true
237
- }}
238
- onHoverEnd={() => {
239
- hoverRef.current.active = false
240
- const pending = hoverRef.current.pending
241
- hoverRef.current.pending = undefined
242
- pending?.()
243
- }}
244
- onActionClose={() => {
245
- hoverRef.current.active = false
246
- hoverRef.current.pending = undefined
247
- }}
248
- />
249
- )
250
- })
251
-
252
- export default Snackbar
253
-
254
- export function useSnackbar(props: SnackbarProps = {}) {
255
- 'use memo'
256
-
257
- const { position, offset, dim, zIndex, portalContainer, className } = props
258
- const snackbarHandlerRef = useRef<SnackbarHandler>(null)
259
- const element = (
260
- <Snackbar
261
- ref={snackbarHandlerRef}
262
- position={position}
263
- offset={offset}
264
- dim={dim}
265
- zIndex={zIndex}
266
- portalContainer={portalContainer}
267
- className={className}
268
- />
269
- )
270
- function show<T extends ElementType = 'button'>(
271
- message: ReactNode,
272
- options?: SnackbarShowOptions<T>,
273
- ) {
274
- snackbarHandlerRef.current?.show(message, options)
275
- }
276
- return [element, show] as const
277
- }
278
-
279
- function SnackbarRegion({
280
- state,
281
- position,
282
- offset,
283
- dim,
284
- zIndex,
285
- portalContainer,
286
- className,
287
- snackbarRef,
288
- onHoverStart,
289
- onHoverEnd,
290
- onActionClose,
291
- }: {
292
- state: ToastState<SnackbarContent>
293
- position: SnackbarPosition
294
- offset: number
295
- dim: boolean
296
- zIndex: number
297
- portalContainer?: HTMLElement
298
- className?: string
299
- snackbarRef: RefObject<HTMLDivElement>
300
- onHoverStart: () => void
301
- onHoverEnd: () => void
302
- onActionClose: () => void
303
- }) {
304
- const regionRef = useRef<HTMLDivElement>(null)
305
- const pausedByFocusRef = useRef(false)
306
- const timerState: ToastState<SnackbarContent> = {
307
- ...state,
308
- pauseAll() {
309
- if (regionRef.current?.contains(document.activeElement)) {
310
- pausedByFocusRef.current = true
311
- state.pauseAll()
312
- }
313
- },
314
- resumeAll() {
315
- if (pausedByFocusRef.current) {
316
- pausedByFocusRef.current = false
317
- state.resumeAll()
318
- }
319
- },
320
- }
321
- const { regionProps } = useToastRegion({}, timerState, regionRef)
322
- const classNames = useClassNames('charcoal-snackbar-region', className)
323
-
324
- if (state.visibleToasts.length === 0) {
325
- return null
326
- }
327
-
328
- // ボタン付きの Snackbar は常に下部に表示される
329
- const hasButton = state.visibleToasts.some(
330
- (toast) => toast.content.button !== undefined,
331
- )
332
- const effectivePosition = hasButton ? 'bottom' : position
333
-
334
- return (
335
- <Overlay disableFocusManagement portalContainer={portalContainer}>
336
- <div
337
- {...regionProps}
338
- ref={regionRef}
339
- className={classNames}
340
- data-position={effectivePosition}
341
- style={{
342
- zIndex,
343
- '--charcoal-snackbar-offset': `${offset}px`,
344
- }}
345
- >
346
- {state.visibleToasts.map((toast) => (
347
- <SnackbarItem
348
- key={toast.key}
349
- toast={toast}
350
- state={state}
351
- dim={dim}
352
- snackbarRef={snackbarRef}
353
- onHoverStart={onHoverStart}
354
- onHoverEnd={onHoverEnd}
355
- onActionClose={() => {
356
- onActionClose()
357
- state.close(toast.key)
358
- }}
359
- />
360
- ))}
361
- </div>
362
- </Overlay>
363
- )
364
- }
365
-
366
- function SnackbarItem({
367
- toast,
368
- state,
369
- dim,
370
- snackbarRef,
371
- onHoverStart,
372
- onHoverEnd,
373
- onActionClose,
374
- }: {
375
- toast: QueuedToast<SnackbarContent>
376
- state: ToastState<SnackbarContent>
377
- dim: boolean
378
- snackbarRef: RefObject<HTMLDivElement>
379
- onHoverStart: () => void
380
- onHoverEnd: () => void
381
- onActionClose: () => void
382
- }) {
383
- const { toastProps, contentProps, titleProps } = useToast(
384
- { toast },
385
- state,
386
- snackbarRef,
387
- )
388
- const { message, button } = toast.content
389
-
390
- return (
391
- <div
392
- {...toastProps}
393
- ref={snackbarRef}
394
- className="charcoal-snackbar"
395
- data-dim={dim}
396
- data-with-button={button !== undefined}
397
- onPointerEnter={onHoverStart}
398
- onPointerLeave={onHoverEnd}
399
- >
400
- <div
401
- {...contentProps}
402
- role="status"
403
- className="charcoal-snackbar-content"
404
- >
405
- <div {...titleProps} className="charcoal-snackbar-label">
406
- {message}
407
- </div>
408
- </div>
409
- {button !== undefined && (
410
- <SnackbarAction button={button} dim={dim} onClose={onActionClose} />
411
- )}
412
- </div>
413
- )
414
- }
415
-
416
- function SnackbarAction({
417
- button,
418
- dim,
419
- onClose,
420
- }: {
421
- button: SnackbarButtonContent
422
- dim: boolean
423
- onClose: () => void
424
- }) {
425
- const { onClick, variant, children: buttonChildren, ...buttonProps } = button
426
-
427
- function handleButtonClick(
428
- event: Parameters<NonNullable<ButtonProps<'button'>['onClick']>>[0],
429
- ) {
430
- onClick?.(event)
431
- onClose()
432
- }
433
-
434
- return (
435
- <PolymorphicButton
436
- {...buttonProps}
437
- size="S"
438
- variant={variant ?? (dim ? 'Navigation' : undefined)}
439
- onClick={handleButtonClick}
440
- >
441
- {buttonChildren}
442
- </PolymorphicButton>
443
- )
444
- }
445
-
446
- function PolymorphicButton({
447
- component,
448
- ...props
449
- }: Omit<ButtonProps, 'component'> & { component?: ElementType }) {
450
- if (component === undefined || component === 'button') {
451
- return <Button {...props} />
452
- }
453
-
454
- return <Button {...props} component={component} />
455
- }