;
-export const Default: Story = {
- args: {},
+export const Default = () => {
+ const { openSnackbar } = useSnackbar();
+
+ return (
+
+
+
+
+
+
+
+
+ );
};
diff --git a/lib/components/Snackbar/Snackbar.tsx b/lib/components/Snackbar/Snackbar.tsx
index 6fc23f7f..29a63922 100644
--- a/lib/components/Snackbar/Snackbar.tsx
+++ b/lib/components/Snackbar/Snackbar.tsx
@@ -1,31 +1,30 @@
-import type { ElementType } from 'react';
-import type { IconName } from '../Icon';
-import { SnackbarBase, type SnackbarColor } from './SnackbarBase';
-import { SnackbarLabel } from './SnackbarLabel';
-import { SnackbarMedia } from './SnackbarMedia';
+'use client';
+import { SnackbarBase } from './SnackbarBase.tsx';
+import { SnackbarItem } from './SnackbarItem';
+import { useSnackbar } from './useSnackbar';
export interface SnackbarProps {
- /** Element type to render */
- as?: ElementType;
- /** Color */
- color?: SnackbarColor;
- /** Message */
- message?: string;
- /** Icon */
- icon?: IconName;
- /** Dismissable */
- dismissable?: boolean;
- /** onDismiss */
- onDismiss?: () => void;
/** Optional classname */
className?: string;
}
-export const Snackbar = ({ as = 'a', color, message, icon, ...rest }: SnackbarProps) => {
+export const Snackbar = ({ className }: SnackbarProps) => {
+ const { storedMessages, open, closeSnackbarItem } = useSnackbar();
+
+ if (!open) {
+ return null;
+ }
+
return (
-
-
- {message}
+
+ {(storedMessages || []).map((item) => (
+ closeSnackbarItem(item.id)}
+ dismissable={item.dismissable}
+ {...item}
+ />
+ ))}
);
};
diff --git a/lib/components/Snackbar/SnackbarBase.tsx b/lib/components/Snackbar/SnackbarBase.tsx
index 159d0f95..266c7d45 100644
--- a/lib/components/Snackbar/SnackbarBase.tsx
+++ b/lib/components/Snackbar/SnackbarBase.tsx
@@ -1,39 +1,17 @@
-import cx from 'classnames';
-import type { ElementType, ReactNode } from 'react';
-import { IconButton } from '../Button';
+import type { ReactNode } from 'react';
import styles from './snackbarBase.module.css';
-export type SnackbarColor = 'default' | 'accent';
-
-interface SnackbarBaseProps {
- as?: ElementType;
- color?: SnackbarColor;
- href?: string;
+export interface SnackbarBaseProps {
+ /** Optional classname */
className?: string;
- dismissable?: boolean;
- onDismiss?: () => void;
- children?: ReactNode;
+ /** Children */
+ children: string | ReactNode;
}
-export const SnackbarBase = ({
- as,
- children,
- className,
- color,
- dismissable = true,
- onDismiss,
- ...rest
-}: SnackbarBaseProps) => {
- const Component = as || 'div';
-
+export const SnackbarBase = ({ children }: SnackbarBaseProps) => {
return (
-
- {children}
- {dismissable && (
-
-
-
- )}
-
+
);
};
diff --git a/lib/components/Snackbar/SnackbarItem.tsx b/lib/components/Snackbar/SnackbarItem.tsx
new file mode 100644
index 00000000..5609d285
--- /dev/null
+++ b/lib/components/Snackbar/SnackbarItem.tsx
@@ -0,0 +1,45 @@
+import cx from 'classnames';
+import type { ElementType, ReactNode } from 'react';
+import { Icon, IconButton, type IconName } from '..';
+import styles from './snackbarItem.module.css';
+
+export type SnackbarColor = 'default' | 'accent' | 'alert';
+
+interface SnackbarItemProps {
+ as?: ElementType;
+ color?: SnackbarColor;
+ icon?: IconName;
+ message: string | ReactNode;
+ href?: string;
+ className?: string;
+ dismissable?: boolean;
+ onDismiss?: () => void;
+ children?: ReactNode;
+}
+
+export const SnackbarItem = ({
+ as,
+ message,
+ className,
+ color,
+ icon = 'bell',
+ dismissable = true,
+ onDismiss,
+ ...rest
+}: SnackbarItemProps) => {
+ const Component = as || 'div';
+
+ return (
+
+
+
+
+ {message}
+ {dismissable && (
+
+
+
+ )}
+
+ );
+};
diff --git a/lib/components/Snackbar/SnackbarLabel.tsx b/lib/components/Snackbar/SnackbarLabel.tsx
deleted file mode 100644
index dd068b31..00000000
--- a/lib/components/Snackbar/SnackbarLabel.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import type { ReactNode } from 'react';
-import styles from './snackbarLabel.module.css';
-
-export interface SnackbarLabelProps {
- children?: ReactNode;
-}
-
-export const SnackbarLabel = ({ children }: SnackbarLabelProps) => {
- return {children};
-};
diff --git a/lib/components/Snackbar/SnackbarMedia.tsx b/lib/components/Snackbar/SnackbarMedia.tsx
deleted file mode 100644
index 4504a2fa..00000000
--- a/lib/components/Snackbar/SnackbarMedia.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Icon, type IconName } from '../Icon';
-import styles from './snackbarMedia.module.css';
-
-interface SnackbarMediaProps {
- icon?: IconName;
-}
-
-export const SnackbarMedia = ({ icon = 'bell' }: SnackbarMediaProps) => {
- return (
-
-
-
- );
-};
diff --git a/lib/components/Snackbar/index.ts b/lib/components/Snackbar/index.ts
index 1ca8d9ec..98935a0b 100644
--- a/lib/components/Snackbar/index.ts
+++ b/lib/components/Snackbar/index.ts
@@ -1,4 +1,4 @@
-export * from './Snackbar';
-export * from './SnackbarBase';
-export * from './SnackbarMedia';
-export * from './SnackbarLabel';
+export * from './Snackbar.tsx';
+export * from './SnackbarItem';
+//export * from './SnackbarMedia';
+//export * from './SnackbarLabel';
diff --git a/lib/components/Snackbar/snackbarBase.module.css b/lib/components/Snackbar/snackbarBase.module.css
index 17798857..607809eb 100644
--- a/lib/components/Snackbar/snackbarBase.module.css
+++ b/lib/components/Snackbar/snackbarBase.module.css
@@ -1,55 +1,17 @@
-.item {
- border: none;
-
- color: inherit;
- font: inherit;
- text-align: inherit;
-
- line-height: normal;
-
- -webkit-font-smoothing: inherit;
- -moz-osx-font-smoothing: inherit;
-
- user-select: none;
-
- border-radius: 0.5rem;
-}
-
-.item {
- position: relative;
- display: flex;
- align-items: center;
- text-decoration: none;
- color: inherit;
-}
-
-.content {
- flex-grow: 1;
+.stack {
+ position: fixed;
+ top: auto;
+ right: 0;
+ bottom: 0;
+ left: 0;
display: flex;
- align-items: center;
- gap: 0.625rem;
- margin: 0.625rem;
-}
-
-.action {
- flex-shrink: 0;
- display: flex;
- align-items: center;
- margin: 0.625rem;
-}
-
-/* colors */
-
-.item {
- background-color: var(--global-base-default);
- color: #fff;
- box-shadow: var(--ds-shadow-xs);
-}
-
-.item[data-color="accent"] {
- background-color: var(--theme-surface-default);
+ flex-direction: column;
+ row-gap: 0.5rem;
+ padding: 1rem;
}
-.item[data-color="accent"]:hover {
- background-color: var(--theme-surface-hover);
+@media (min-width: 1024px) {
+ .stack {
+ align-items: center;
+ }
}
diff --git a/lib/components/Snackbar/snackbarItem.module.css b/lib/components/Snackbar/snackbarItem.module.css
new file mode 100644
index 00000000..50534331
--- /dev/null
+++ b/lib/components/Snackbar/snackbarItem.module.css
@@ -0,0 +1,70 @@
+.item {
+ border: none;
+ color: inherit;
+ font: inherit;
+ text-align: inherit;
+ line-height: normal;
+ -webkit-font-smoothing: inherit;
+ -moz-osx-font-smoothing: inherit;
+ user-select: none;
+ border-radius: 0.5rem;
+ box-shadow: var(--ds-shadow-xs);
+}
+
+.item {
+ position: relative;
+ display: flex;
+ align-items: center;
+ text-decoration: none;
+ color: inherit;
+ padding: 0.625rem;
+}
+
+.media {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2.75rem;
+ height: 2.75rem;
+}
+
+.icon {
+ font-size: 1.5rem;
+}
+
+.content {
+ flex-grow: 1;
+ display: flex;
+ align-items: center;
+ margin-left: 0.625rem;
+ margin-right: 1.25rem;
+}
+
+.action {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* colors */
+
+.item {
+ background-color: var(--global-base-default);
+ color: white;
+}
+
+.item[data-color="accent"] {
+ background-color: var(--global-base-default);
+ color: white;
+}
+
+.item[data-color="accent"]:hover {
+ background-color: var(--global-base-hover);
+}
+
+.item[data-color="alert"] {
+ background-color: #e02e49;
+ color: white;
+}
diff --git a/lib/components/Snackbar/snackbarLabel.module.css b/lib/components/Snackbar/snackbarLabel.module.css
deleted file mode 100644
index 4a40e62f..00000000
--- a/lib/components/Snackbar/snackbarLabel.module.css
+++ /dev/null
@@ -1,6 +0,0 @@
-/* label */
-
-.label {
- display: flex;
- justify-content: center;
-}
diff --git a/lib/components/Snackbar/snackbarMedia.module.css b/lib/components/Snackbar/snackbarMedia.module.css
deleted file mode 100644
index 8e9a2530..00000000
--- a/lib/components/Snackbar/snackbarMedia.module.css
+++ /dev/null
@@ -1,10 +0,0 @@
-.media {
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.icon {
- font-size: 1.5rem;
- margin: 0.625rem;
-}
diff --git a/lib/components/Snackbar/useSnackbar.tsx b/lib/components/Snackbar/useSnackbar.tsx
new file mode 100644
index 00000000..1e56bc67
--- /dev/null
+++ b/lib/components/Snackbar/useSnackbar.tsx
@@ -0,0 +1,118 @@
+'use client';
+import {
+ type ElementType,
+ type ReactNode,
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from 'react';
+import type { IconName } from '../Icon';
+import type { SnackbarColor } from './SnackbarItem.tsx';
+
+export enum SnackbarDuration {
+ infinite = 0,
+ short = 1000,
+ normal = 3000,
+ long = 5000,
+}
+
+export interface OpenSnackbarInput extends Omit {}
+
+export interface SnackbarQueueItem extends SnackbarItemProps {
+ id: string;
+ message: string;
+ color: SnackbarColor;
+ duration?: number;
+ icon?: IconName;
+}
+
+interface SnackbarItemProps {
+ as?: ElementType;
+ color?: SnackbarColor;
+ href?: string;
+ className?: string;
+ dismissable?: boolean;
+ onDismiss?: () => void;
+ children?: ReactNode;
+}
+
+interface SnackbarContextValue {
+ open: boolean;
+ storedMessages: SnackbarQueueItem[];
+ openSnackbar: (input: OpenSnackbarInput) => string;
+ closeSnackbarItem: (id: string) => void;
+ dismissSnackbar: () => void;
+}
+
+const SnackbarContext = createContext(undefined);
+const defaultDuration = SnackbarDuration.normal;
+
+export const SnackbarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [storedMessages, setStoredMessages] = useState([]);
+ const closingTime = useRef(null);
+
+ const dismissSnackbar = () => {
+ setStoredMessages([]);
+ setIsOpen(false);
+ if (closingTime.current) {
+ clearTimeout(closingTime.current);
+ }
+ };
+
+ const openSnackbar = (message: OpenSnackbarInput): string => {
+ const id = btoa(String(Math.random())).substring(0, 12);
+ setStoredMessages((prevMessages) => [...prevMessages, { ...message, id }]);
+ setIsOpen(true);
+ return id;
+ };
+
+ const closeSnackbarItem = useCallback((id: string) => {
+ setStoredMessages((prevMessages) => {
+ const updatedMessages = prevMessages.filter((item) => item.id !== id);
+ setIsOpen(updatedMessages.length > 0);
+ return updatedMessages;
+ });
+ }, []);
+
+ useEffect(() => {
+ const activeMessage = storedMessages.find((item) => (item.duration ?? defaultDuration) > 0);
+ const duration = activeMessage?.duration || defaultDuration;
+ if (activeMessage) {
+ closingTime.current = setTimeout(() => {
+ closeSnackbarItem(activeMessage.id);
+ }, duration);
+ }
+
+ return () => {
+ if (closingTime.current) {
+ clearTimeout(closingTime.current);
+ }
+ };
+ }, [storedMessages, closeSnackbarItem]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useSnackbar = (): SnackbarContextValue => {
+ const context = useContext(SnackbarContext);
+ if (!context) {
+ throw new Error('useSnackbar must be used within a SnackbarProvider');
+ }
+ return context;
+};