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

OPHJOD-296: Add toast component #26

Merged
merged 2 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions lib/components/Toast/Toast.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { Meta, StoryObj } from '@storybook/react';

import { Toast } from './Toast';

const meta = {
title: 'Snackbars/Toast',
component: Toast,
tags: ['autodocs'],
} satisfies Meta<typeof Toast>;

export default meta;

type Story = StoryObj<typeof meta>;

const url = 'https://www.figma.com/file/6M2LrpSCcB0thlFDaQAI2J/cx_jod_client?node-id=542%3A8376';
const text = 'Lorem ipsum dolor';

export const Success: Story = {
parameters: {
design: {
type: 'figma',
url,
},
docs: {
description: {
story: 'This is a success toast component for displaying a text.',
},
},
},
args: {
text,
icon: 'check_circle',
iconAriaLabel: 'Check Circle',
},
};

export const Warning: Story = {
parameters: {
design: {
type: 'figma',
url,
},
docs: {
description: {
story: 'This is a warning toast component for displaying a text.',
},
},
},
args: {
text,
icon: 'warning',
iconAriaLabel: 'Warning',
variant: 'warning',
},
};

export const ErrorStory: Story = {
name: 'Error',
parameters: {
design: {
type: 'figma',
url,
},
docs: {
description: {
story: 'This is an error toast component for displaying a text.',
},
},
},
args: {
text,
icon: 'error',
iconAriaLabel: 'Error',
variant: 'error',
},
};

export const Iconless: Story = {
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/6M2LrpSCcB0thlFDaQAI2J/cx_jod_client?node-id=542%3A8376',
},
docs: {
description: {
story: 'This is a toast component for displaying a text without an icon.',
},
},
},
args: {
text: 'Lorem ipsum dolor',
},
};
30 changes: 30 additions & 0 deletions lib/components/Toast/Toast.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { afterEach, describe, expect, it } from 'vitest';
import { render, screen, cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';

import { Toast } from './Toast';

afterEach(() => {
cleanup();
});

describe('Toast', () => {
it('renders the toast component with text', () => {
const { container } = render(<Toast text="This is a toast message" />);
const toastElement = screen.getByRole('alert');
expect(toastElement).toBeInTheDocument();
expect(toastElement).toHaveTextContent('This is a toast message');
expect(container.firstChild).toMatchSnapshot();
});

it('renders the toast component with icon', () => {
const { container } = render(<Toast text="This is a toast message" icon="mood" iconAriaLabel="Mood" />);
const toastElement = screen.getByRole('alert');
const iconElement = screen.getByLabelText('Mood');
expect(toastElement).toBeInTheDocument();
expect(toastElement).toHaveTextContent('This is a toast message');
expect(iconElement).toBeInTheDocument();
expect(iconElement).toHaveTextContent('mood');
expect(container.firstChild).toMatchSnapshot();
});
});
36 changes: 36 additions & 0 deletions lib/components/Toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export interface ToastProps {
sauanto marked this conversation as resolved.
Show resolved Hide resolved
/** Text shown on the toast */
text: string;
/** Icon shown on the toast */
icon?: string;
/** Aria label for the icon */
iconAriaLabel?: string;
/** Variant of the toast */
variant?: 'success' | 'warning' | 'error';
}

/**
* Toast component for displaying a text.
*/
export const Toast = ({ text, icon, iconAriaLabel, variant = 'success' }: ToastProps) => (
<div
role="alert"
aria-live="assertive"
aria-atomic="true"
className={`
${variant === 'success' ? 'bg-success text-white' : ''}
${variant === 'warning' ? 'bg-warning text-black' : ''}
${variant === 'error' ? 'bg-alert text-white' : ''}
inline-flex h-8 items-center gap-3 rounded-[4px] pl-5
text-body-sm ${icon ? 'pr-7' : 'pr-5'}`
.replace(/\s+/g, ' ')
.trim()}
>
{icon && (
<span className="material-symbols-outlined size-24 select-none" role="img" aria-label={iconAriaLabel}>
{icon}
</span>
)}
{text}
</div>
);
30 changes: 30 additions & 0 deletions lib/components/Toast/__snapshots__/Toast.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`Toast > renders the toast component with icon 1`] = `
<div
aria-atomic="true"
aria-live="assertive"
class="bg-success text-white inline-flex h-8 items-center gap-3 rounded-[4px] pl-5 text-body-sm pr-7"
role="alert"
>
<span
aria-label="Mood"
class="material-symbols-outlined size-24 select-none"
role="img"
>
mood
</span>
This is a toast message
</div>
`;

exports[`Toast > renders the toast component with text 1`] = `
<div
aria-atomic="true"
aria-live="assertive"
class="bg-success text-white inline-flex h-8 items-center gap-3 rounded-[4px] pl-5 text-body-sm pr-5"
role="alert"
>
This is a toast message
</div>
`;
2 changes: 2 additions & 0 deletions lib/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import 'symbols.css';

@tailwind base;
@tailwind components;
@tailwind utilities;
45 changes: 45 additions & 0 deletions lib/symbols.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* From https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200 */

/* fallback */
@font-face {
font-family: 'Material Symbols Outlined';
font-style: normal;
font-weight: 100 700;
src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v170/kJEhBvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oFsLjBuVY.woff2)
format('woff2');
}

.material-symbols-outlined {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
direction: ltr;
display: inline-block;
font-family: 'Material Symbols Outlined';
font-feature-settings: 'liga';
font-size: 24px;
font-style: normal;
font-weight: normal;
letter-spacing: normal;
line-height: 1;
text-transform: none;
white-space: nowrap;
word-wrap: normal;
}

/* Recommended icon sizes */
span.size-20 {
font-size: 20px;
font-variation-settings: 'OPSZ' 20;
}
span.size-24 {
font-size: 24px;
font-variation-settings: 'OPSZ' 24;
}
span.size-40 {
font-size: 40px;
font-variation-settings: 'OPSZ' 40;
}
span.size-48 {
font-size: 48px;
font-variation-settings: 'OPSZ' 48;
}
2 changes: 1 addition & 1 deletion tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default {
'heading-5': ['16px', { fontWeight: '700', lineHeight: '110%' }], // Headline 5
'body-lg': ['18px', { fontWeight: '500', lineHeight: '140%' }], // Body L
'body-md': ['16px', { fontWeight: '500', lineHeight: '140%' }], // Body M
'body-sm': ['14px', { fontWeight: '700', lineHeight: '140%' }], // Body S
'body-sm': ['14px', { fontWeight: '500', lineHeight: '140%' }], // Body S
'body-xs': ['12px', { fontWeight: '700', lineHeight: '140%' }], // Body XS
'form-label': ['14px', { fontWeight: '700', lineHeight: '110%' }], // Form label
help: ['12px', { fontWeight: '500', lineHeight: '110%' }], // Help, hint
Expand Down