|
| 1 | +'use client'; |
| 2 | +import { Check } from '@primeicons/react/check'; |
| 3 | +import { ExclamationTriangle } from '@primeicons/react/exclamation-triangle'; |
| 4 | +import { InfoCircle } from '@primeicons/react/info-circle'; |
| 5 | +import { Times } from '@primeicons/react/times'; |
| 6 | +import { usePortal } from '@primereact/headless/portal'; |
| 7 | +import { useToast } from '@primereact/headless/toast'; |
| 8 | +import { toast, useToaster } from '@primereact/headless/toaster'; |
| 9 | +import { ToastType } from 'primereact/toaster'; |
| 10 | +import * as React from 'react'; |
| 11 | +import { createPortal } from 'react-dom'; |
| 12 | + |
| 13 | +const variantIcons: Record<string, React.ReactNode> = { |
| 14 | + success: <Check />, |
| 15 | + danger: <Times />, |
| 16 | + warn: <ExclamationTriangle />, |
| 17 | + info: <InfoCircle /> |
| 18 | +}; |
| 19 | + |
| 20 | +function ToastItem({ toastData, toaster }: { toastData: ToastType; toaster: ReturnType<typeof useToaster> }) { |
| 21 | + const { rootProps, closeProps } = useToast({ toast: toastData, toaster }); |
| 22 | + |
| 23 | + return ( |
| 24 | + <div |
| 25 | + {...rootProps} |
| 26 | + className={[ |
| 27 | + // base |
| 28 | + 'w-full p-4 rounded-lg border border-surface-200 dark:border-surface-800 bg-surface-0 dark:bg-surface-900 text-surface-900 dark:text-surface-0', |
| 29 | + 'outline-none absolute touch-none shadow-sm [z-index:var(--toast-z-index)]', |
| 30 | + 'opacity-0 [transform:translateX(var(--offset-x))_translateY(calc(100%*var(--raise-factor)*-1))]', |
| 31 | + '[transition:transform_0.3s,opacity_0.3s,height_0.3s]', |
| 32 | + // focus |
| 33 | + 'focus-visible:outline-1 focus-visible:outline-primary focus-visible:outline-offset-2', |
| 34 | + // mounted |
| 35 | + 'data-[mounted]:opacity-100 data-[mounted]:[transform:translateY(0)]', |
| 36 | + // collapsed stack |
| 37 | + 'not-data-[expanded]:not-data-[front]:overflow-hidden not-data-[expanded]:not-data-[front]:[height:var(--front-toast-height)] not-data-[expanded]:not-data-[front]:[transform:translateX(var(--offset-x))_translateY(calc(var(--raise-factor)*var(--toast-index)*var(--gap)))_scale(calc(var(--toast-index)*-0.05+1))]', |
| 38 | + // expanded |
| 39 | + 'data-[mounted]:data-[expanded]:[height:var(--initial-height)] data-[mounted]:data-[expanded]:[transform:translateX(var(--offset-x))_translateY(var(--offset-y))]', |
| 40 | + // expanded gap pseudo |
| 41 | + 'data-[expanded]:after:content-[""] data-[expanded]:after:absolute data-[expanded]:after:left-0 data-[expanded]:after:[height:calc(var(--gap)+1px)] data-[expanded]:after:w-full data-[expanded]:after:bottom-full', |
| 42 | + // hidden |
| 43 | + 'not-data-[visible]:!opacity-0 not-data-[visible]:!pointer-events-none not-data-[visible]:!select-none', |
| 44 | + // removed front |
| 45 | + 'data-[removed]:data-[front]:not-data-[swipe-out]:opacity-0 data-[removed]:data-[front]:not-data-[swipe-out]:[transform:translateX(var(--offset-x))_translateY(calc(var(--raise-factor)*-100%))]', |
| 46 | + // removed non-front expanded |
| 47 | + 'data-[removed]:not-data-[front]:not-data-[swipe-out]:data-[expanded]:opacity-0 data-[removed]:not-data-[front]:not-data-[swipe-out]:data-[expanded]:[transform:translateX(var(--offset-x))_translateY(calc(var(--raise-factor)*var(--offset-y)*0.4))]', |
| 48 | + // removed non-front collapsed |
| 49 | + 'data-[removed]:not-data-[front]:not-data-[swipe-out]:not-data-[expanded]:opacity-0 data-[removed]:not-data-[front]:not-data-[swipe-out]:not-data-[expanded]:[transform:translateX(var(--offset-x))_translateY(calc(var(--raise-factor)*40%*-1))] data-[removed]:not-data-[front]:not-data-[swipe-out]:not-data-[expanded]:[transition:transform_500ms,opacity_200ms]', |
| 50 | + // swiping |
| 51 | + 'data-[swiping]:![transition:none] data-[swiping]:![transform:translateX(var(--offset-x))_translateY(var(--offset-y))]', |
| 52 | + 'data-[swiped]:select-none', |
| 53 | + // swipe out directions |
| 54 | + 'data-[swipe-out]:data-[swipe-direction=up]:opacity-0 data-[swipe-out]:data-[swipe-direction=up]:![transform:translateX(var(--offset-x))_translateY(calc(var(--offset-y)-100%))]', |
| 55 | + 'data-[swipe-out]:data-[swipe-direction=down]:opacity-0 data-[swipe-out]:data-[swipe-direction=down]:![transform:translateX(var(--offset-x))_translateY(calc(var(--offset-y)+100%))]', |
| 56 | + 'data-[swipe-out]:data-[swipe-direction=left]:opacity-0 data-[swipe-out]:data-[swipe-direction=left]:![transform:translateX(calc(var(--offset-x)-100%))_translateY(var(--offset-y))]', |
| 57 | + 'data-[swipe-out]:data-[swipe-direction=right]:opacity-0 data-[swipe-out]:data-[swipe-direction=right]:![transform:translateX(calc(var(--offset-x)+100%))_translateY(var(--offset-y))] data-[swipe-out]:data-[swipe-direction=right]:[transition:transform_500ms,opacity_200ms]' |
| 58 | + ].join(' ')} |
| 59 | + style={ |
| 60 | + { |
| 61 | + ...rootProps.style, |
| 62 | + '--offset-y': `calc(var(--swipe-amount-y) + (var(--toast-offset) + var(--toast-index) * var(--gap)) * var(--raise-factor))`, |
| 63 | + '--offset-x': 'var(--swipe-amount-x)', |
| 64 | + bottom: 0, |
| 65 | + right: 0, |
| 66 | + '--raise-factor': '-1' |
| 67 | + } as React.CSSProperties |
| 68 | + } |
| 69 | + > |
| 70 | + <div className="grid grid-cols-[auto_1fr] items-start gap-3"> |
| 71 | + {variantIcons[toastData.variant as string] && ( |
| 72 | + <span className="[&>svg]:size-3.5 mt-1 text-surface-800 dark:text-surface-100">{variantIcons[toastData.variant as string]}</span> |
| 73 | + )} |
| 74 | + <div> |
| 75 | + {toastData.title && <div className="text-sm font-semibold text-surface-800 dark:text-surface-100">{toastData.title}</div>} |
| 76 | + {toastData.description && <div className="text-sm text-surface-500 dark:text-surface-400 mt-1">{toastData.description}</div>} |
| 77 | + </div> |
| 78 | + </div> |
| 79 | + {toastData.dismissible !== false && toastData.variant !== 'loading' && ( |
| 80 | + <button |
| 81 | + {...closeProps} |
| 82 | + className="absolute top-2 right-2 p-1 rounded text-surface-400 hover:text-surface-600 dark:hover:text-surface-200 cursor-pointer focus-visible:outline-1 focus-visible:outline-primary" |
| 83 | + > |
| 84 | + <Times className="size-3" /> |
| 85 | + </button> |
| 86 | + )} |
| 87 | + </div> |
| 88 | + ); |
| 89 | +} |
| 90 | + |
| 91 | +export default function BasicDemo() { |
| 92 | + const toaster = useToaster({ position: 'bottom-right', group: 'headless-basic' }); |
| 93 | + const portal = usePortal(); |
| 94 | + |
| 95 | + return ( |
| 96 | + <div className="flex flex-wrap items-center justify-center gap-4"> |
| 97 | + <button |
| 98 | + onClick={() => |
| 99 | + toast.success({ |
| 100 | + title: 'Saved successfully', |
| 101 | + description: 'Your changes have been saved.', |
| 102 | + group: 'headless-basic' |
| 103 | + }) |
| 104 | + } |
| 105 | + className="px-2.5 py-1.5 text-sm rounded-md border border-surface-200 dark:border-surface-700 text-surface-700 dark:text-surface-200 hover:bg-surface-50 dark:hover:bg-surface-800 cursor-pointer focus-visible:outline-1 focus-visible:outline-primary" |
| 106 | + > |
| 107 | + Create toast |
| 108 | + </button> |
| 109 | + {portal.state.mounted && |
| 110 | + createPortal( |
| 111 | + <div {...toaster.regionProps} className="fixed w-75 z-[2000] right-8 bottom-8" style={toaster.regionProps.style}> |
| 112 | + {toaster.toasts.map((t) => ( |
| 113 | + <ToastItem key={t.id} toastData={t} toaster={toaster} /> |
| 114 | + ))} |
| 115 | + </div>, |
| 116 | + |
| 117 | + document.body |
| 118 | + )} |
| 119 | + </div> |
| 120 | + ); |
| 121 | +} |
0 commit comments