Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Update NotificationToast provider with position and className props #2577

Merged
merged 13 commits into from
Jun 17, 2024
5 changes: 5 additions & 0 deletions .changeset/clever-islands-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sumup/circuit-ui": minor
---

Added new `position` and `className` props to the ToastProvider component.
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ export function App({}) {
}
```

### Custom positioning

Change the position of notifications by passing the `position` prop. Supported values are `bottom` (default), `top` and `top-right`:

<Story of={Stories.Position} />

## Accessibility

By their nature as status messages, toasts are rendered inside a [live region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) using `role="status"` and `aria-live="polite"`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ import {
export default {
title: 'Notification/NotificationToast',
component: NotificationToast,
argTypes: {
position: {
options: ['top', 'top-right', 'bottom'],
control: { type: 'select' },
},
},
};

const TOASTS = [
Expand All @@ -55,29 +61,25 @@ const TOASTS = [
},
] as NotificationToastProps[];

export const Base = (toast: NotificationToastProps): JSX.Element => {
const App = () => {
const { setToast } = useNotificationToast();
const randomIndex = isChromatic()
? 1
: Math.floor(Math.random() * TOASTS.length);
return (
<Button
type="button"
onClick={() => setToast({ ...toast, ...TOASTS[randomIndex] })}
>
Open toast
</Button>
);
};
const App = ({ toast }: { toast: NotificationToastProps }) => {
const { setToast } = useNotificationToast();
const randomIndex = isChromatic()
? 1
: Math.floor(Math.random() * TOASTS.length);
return (
<ToastProvider>
<App />
</ToastProvider>
<Button
type="button"
onClick={() => setToast({ ...toast, ...TOASTS[randomIndex] })}
>
Open toast
</Button>
);
};

Base.play = async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
const play = async ({
canvasElement,
}: {
canvasElement: HTMLCanvasElement;
}) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button', {
name: 'Open toast',
Expand All @@ -87,6 +89,26 @@ Base.play = async ({ canvasElement }: { canvasElement: HTMLCanvasElement }) => {
await screen.findByRole('status');
};

export const Base = (toast: NotificationToastProps): JSX.Element => (
<ToastProvider>
<App toast={toast} />
</ToastProvider>
);

Base.play = play;

export const Position = (toast: NotificationToastProps): JSX.Element => (
<ToastProvider {...toast}>
<App toast={toast} />
</ToastProvider>
);

Position.args = {
position: 'top',
};

Position.play = play;

const variants = ['info', 'success', 'warning', 'danger'] as const;

export const Variants = (toast: NotificationToastProps) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
.base {
position: fixed;
bottom: var(--cui-spacings-giga);
left: 0;
z-index: var(--cui-z-index-toast);
display: flex;
Expand All @@ -9,13 +8,31 @@
padding: 0 var(--cui-spacings-giga);
}

.top,
.top-right {
top: var(--cui-spacings-giga);
}

.bottom {
bottom: var(--cui-spacings-giga);
}

@media (min-width: 480px) {
.base {
left: 50%;
width: auto;
padding: 0;
}

.top,
.bottom {
left: 50%;
transform: translateX(-50%);
}

.top-right {
right: var(--cui-spacings-giga);
left: inherit;
}
}

.toast {
Expand Down
13 changes: 12 additions & 1 deletion packages/circuit-ui/components/ToastContext/ToastContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from 'react';

import { useStack, type StackItem } from '../../hooks/useStack/index.js';
import { clsx } from '../../styles/clsx.js';

import type { BaseToastProps, ToastComponent } from './types.js';
import classes from './ToastContext.module.css';
Expand All @@ -50,10 +51,20 @@ export interface ToastProviderProps {
* The ToastProvider should wrap your entire application.
*/
children: ReactNode;
/**
* Choose the position of all toasts on screen (please consider sticking to default value if possible). Default: 'bottom'.
*/
position?: 'bottom' | 'top' | 'top-right';
connor-baer marked this conversation as resolved.
Show resolved Hide resolved
/**
* The class name to add to the toast wrapper element.
*/
className?: string;
}

export function ToastProvider<TProps extends BaseToastProps>({
children,
position = 'bottom',
className,
}: ToastProviderProps): JSX.Element {
const [toasts, dispatch] = useStack<ToastState<TProps>>([]);

Expand Down Expand Up @@ -109,7 +120,7 @@ export function ToastProvider<TProps extends BaseToastProps>({
<ToastContext.Provider value={context}>
{children}
<div
className={classes.base}
className={clsx(classes.base, classes[position], className)}
role="status"
aria-live="polite"
aria-atomic="false"
Expand Down
Loading