Skip to content

Commit

Permalink
feat: make Snackbar context-aware (#159)
Browse files Browse the repository at this point in the history
  • Loading branch information
seanes authored Jan 7, 2025
1 parent afbf91e commit 7701241
Show file tree
Hide file tree
Showing 14 changed files with 334 additions and 183 deletions.
25 changes: 0 additions & 25 deletions lib/components/LayoutAction/ActionFooter.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import type { Meta, StoryObj } from '@storybook/react';

import { Snackbar } from '../Snackbar';
import { ActionFooter } from './ActionFooter';
import { ActionMenu, type ActionMenuProps } from './ActionMenu';

Expand Down Expand Up @@ -45,26 +43,3 @@ export const Menu: Story = {
children: <ActionMenu {...menu} />,
},
};

export const Snackbars: Story = {
args: {
children: (
<>
<Snackbar message="A message" />
<Snackbar message="Some other message" />
<Snackbar message="Another message" />
</>
),
},
};

export const MenuAndSnackbar: Story = {
args: {
children: (
<>
<Snackbar message="Snack 1" />
<ActionMenu {...menu} theme="global-dark" />
</>
),
},
};
3 changes: 2 additions & 1 deletion lib/components/RootProvider/RootProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';
import { type ReactNode, createContext, useContext, useState } from 'react';
import { useEscapeKey } from '../../hooks';
import { SnackbarProvider } from '../Snackbar/useSnackbar.tsx';

type OpenElementId = 'search' | 'menu' | string;

Expand Down Expand Up @@ -36,7 +37,7 @@ export const RootProvider = ({ children, initialValue }: ProviderProps) => {
setCurrentId,
}}
>
{children}
<SnackbarProvider>{children}</SnackbarProvider>
</RootContext.Provider>
);
};
Expand Down
63 changes: 53 additions & 10 deletions lib/components/Snackbar/Snackbar.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,63 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Snackbar } from './Snackbar';
import type { Meta } from '@storybook/react';
import { Button } from '../Button';
import { Flex } from '../Page';
import { Snackbar } from './Snackbar.tsx';
import { useSnackbar } from './useSnackbar.tsx';

const meta = {
title: 'Snackbar/Snackbar',
component: Snackbar,
tags: ['autodocs'],
tags: ['autodocs', 'beta'],
parameters: {},
args: {
icon: 'bell',
message: 'Message',
},
args: {},
} satisfies Meta<typeof Snackbar>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {},
export const Default = () => {
const { openSnackbar } = useSnackbar();

return (
<div>
<Flex direction="row" spacing="md">
<Button
onClick={() =>
openSnackbar({
message: 'Message',
color: 'alert',
duration: 1000,
dismissable: true,
})
}
>
Alert
</Button>
<Button
onClick={() =>
openSnackbar({
message: 'This is a longer message',
color: 'accent',
duration: 1000,
dismissable: true,
})
}
>
Accent
</Button>
<Button
onClick={() =>
openSnackbar({
message: 'Message',
color: 'alert',
duration: 1000 * 10,
dismissable: false,
})
}
>
Non-dismissable, 10 seconds
</Button>
</Flex>
<Snackbar />
</div>
);
};
41 changes: 20 additions & 21 deletions lib/components/Snackbar/Snackbar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<SnackbarBase as={as} color={color} {...rest}>
<SnackbarMedia icon={icon} />
<SnackbarLabel>{message}</SnackbarLabel>
<SnackbarBase className={className}>
{(storedMessages || []).map((item) => (
<SnackbarItem
key={item.id}
onDismiss={() => closeSnackbarItem(item.id)}
dismissable={item.dismissable}
{...item}
/>
))}
</SnackbarBase>
);
};
40 changes: 9 additions & 31 deletions lib/components/Snackbar/SnackbarBase.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Component className={cx(styles.item, className)} data-color={color} {...rest}>
<div className={styles.content}>{children}</div>
{dismissable && (
<div className={styles.action}>
<IconButton icon="x-mark" variant="text" onClick={onDismiss} className={styles.dismiss} />
</div>
)}
</Component>
<section role="alert" className={styles.stack}>
{children}
</section>
);
};
45 changes: 45 additions & 0 deletions lib/components/Snackbar/SnackbarItem.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Component className={cx(styles.item, className)} data-color={color} {...rest}>
<div className={styles.media}>
<Icon name={icon} variant="solid" className={styles.icon} />
</div>
<div className={styles.content}>{message}</div>
{dismissable && (
<div className={styles.action}>
<IconButton icon="x-mark" variant="text" onClick={onDismiss} className={styles.dismiss} />
</div>
)}
</Component>
);
};
10 changes: 0 additions & 10 deletions lib/components/Snackbar/SnackbarLabel.tsx

This file was deleted.

14 changes: 0 additions & 14 deletions lib/components/Snackbar/SnackbarMedia.tsx

This file was deleted.

8 changes: 4 additions & 4 deletions lib/components/Snackbar/index.ts
Original file line number Diff line number Diff line change
@@ -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';
64 changes: 13 additions & 51 deletions lib/components/Snackbar/snackbarBase.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit 7701241

Please sign in to comment.