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

[Component] Alerts Component (Replaces Notifications) #263

Merged
merged 17 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,15 @@ If you're using yarn v2 or greater, [`yarn pack`](https://yarnpkg.com/advanced/l
## Usage

```ts
import { Button, Notification } from 'design-system-react';
import { Button, Alert } from 'design-system-react';
import type { ReactElement } from 'react';

export default function SomePage(): ReactElement {
return (
<main>
<Notification
message='2025-Q1 Quarterly filing period is open'
type='success'
>
<Alert message='2025-Q1 Quarterly filing period is open' type='success'>
Submissions of 2025-Q1 SBL data will be accepted through May 2025.
</Notification>
</Alert>
<Button onClick={async () => login()} label='Log in' />
<Button
onClick={async () => register()}
Expand Down Expand Up @@ -64,9 +61,11 @@ Run `yarn test` to watch for changes and run tests automatically.
[Netlify](https://www.netlify.com/) will build and deploy a preview of any pull requests you open.

## Integrating changes to the CFPB Design System

After modifications made to the Design System have been released in the [NPM package](https://www.npmjs.com/package/@cfpb/cfpb-design-system), we can integrate those changes by updating our dependencies.

1. Create a PR in DSR that updates the Design System library versions

```
git checkout main
git pull --rebase
Expand All @@ -78,6 +77,7 @@ git add --all
git commit -m 'chore: Update CFPB DS to <new.version.number>'
git push
```

2. Review & merge the PR
3. Upon merge, updates will be auto-deployed to [cfpb.github.io/design-system-react/](https://cfpb.github.io/design-system-react/)

Expand Down
134 changes: 134 additions & 0 deletions src/components/Alert/Alert.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Alert, AlertFieldLevel, TextInput } from '~/src/index';
import type { StatusType } from '../TextInput/TextInput';

const meta: Meta<typeof Alert> = {
title: 'Components (Draft)/Alerts',
component: Alert,
argTypes: {
message: { control: 'text' }
}
};

export default meta;

type Story = StoryObj<typeof meta>;

export const Information: Story = {
render: arguments_ => <Alert {...arguments_} />,
args: { status: 'info', message: 'A Notification' }
};

export const InformationWithExplanation: Story = {
...Information,
name: 'Information with explanation',
args: {
...Information.args,
children: 'You can also add an explanation to the notification.'
}
};

export const InformationWithLinks: Story = {
...Information,
name: 'Information with explanation and links',
args: {
...Information.args,
children: 'This is the explanation of the notification.',
links: [
{
href: '/',
label: 'This is a link below the explanation'
},
{
href: '/',
label: 'This is an external link',
isExternal: true
}
]
}
};

export const Success: Story = {
...Information,
args: { ...Information.args, status: 'success', message: '11 results' }
};

export const Warning: Story = {
...Information,
args: { ...Information.args, status: 'warning', message: 'No results found.' }
};

export const Error: Story = {
...Information,
args: { ...Information.args, status: 'error', message: 'Page not found.' }
};

export const InProgress: Story = {
...Information,
name: 'In-progress',
args: {
...Information.args,
status: 'loading',
message: 'The page is loading….'
}
};

export const SuccessFieldLevel: Story = {
render: _arguments => (
<div className='m-form-field'>
<TextInput
id={_arguments.status as string}
name={_arguments.status as string}
status={_arguments.status as StatusType}
value='Input Text'
type='text'
/>
<AlertFieldLevel {..._arguments} />
</div>
),
name: 'Success (field-level)',
args: {
status: 'success',
message: 'This is a field-level alert with a success status.'
}
};

export const WarningFieldLevel: Story = {
render: _arguments => (
<div className='m-form-field'>
<TextInput
id={_arguments.status as string}
name={_arguments.status as string}
status={_arguments.status as StatusType}
value='Input Text'
type='text'
/>
<AlertFieldLevel {..._arguments} />
</div>
),
name: 'Warning (field-level)',
args: {
status: 'warning',
message: 'This is a field-level alert with a warning status.'
}
};

export const ErrorFieldLevel: Story = {
render: _arguments => (
<div className='m-form-field'>
<TextInput
id={_arguments.status as string}
name={_arguments.status as string}
status={_arguments.status as StatusType}
value='Input Text'
type='text'
/>
<AlertFieldLevel {..._arguments} />
</div>
),
name: 'Error (field-level)',
args: {
status: 'error',
message: 'This is a field-level alert with an error status.'
}
};
Original file line number Diff line number Diff line change
@@ -1,46 +1,52 @@
import '@testing-library/jest-dom';
import { render, screen, within } from '@testing-library/react';
import { Notification } from '~/src/index';
import { Alert, AlertFieldLevel } from '~/src/index';
import { AlertType } from './Alert';

const testType = (type: string) => async (): Promise<void> => {
render(<Notification type={type} />);
const testType = (status: AlertType) => async (): Promise<void> => {
render(<Alert status={status} />);
const element = screen.getByTestId('notification');
expect(element).toHaveClass(`m-notification__${type}`);
expect(element).toHaveClass(`m-notification__${status}`);

// Renders Icon
const icon = await within(element).findByRole('img');
expect(icon).toBeInTheDocument();
};

const notificationTypes = ['success', 'warning', 'error', 'info'];
const notificationStatuses: AlertType[] = [
'success',
'warning',
'error',
'info'
];

describe('<Notification />', () => {
for (const type of notificationTypes) {
it(`renders notification of type "${type}"`, testType(type));
describe('<Alert />', () => {
for (const status of notificationStatuses) {
it(`renders alert of type "${status}"`, testType(status));
}

it('displays message when provided', () => {
render(<Notification type='info' />);
render(<Alert status='info' />);
const noMessage = screen.queryByTestId('message');
expect(noMessage).not.toBeInTheDocument();

render(<Notification type='info' message='testing' />);
render(<Alert status='info' message='testing' />);
const message = screen.queryByTestId('message');
expect(message).toBeInTheDocument();
});

it('displays explaination when provided', () => {
render(<Notification type='info' />);
const noExplaination = screen.queryByTestId('explaination');
expect(noExplaination).not.toBeInTheDocument();
it('displays explanation when provided', () => {
render(<Alert status='info' />);
const noExplanation = screen.queryByTestId('explanation');
expect(noExplanation).not.toBeInTheDocument();

render(<Notification type='info'>Explaination</Notification>);
const explaination = screen.queryByTestId('explaination');
expect(explaination).toBeInTheDocument();
render(<Alert status='info'>Explanation</Alert>);
const explanation = screen.queryByTestId('explanation');
expect(explanation).toBeInTheDocument();
});

it('displays links when provided', async () => {
render(<Notification type='info' />);
render(<Alert status='info' />);
const noLinks = screen.queryAllByRole('listitem');
expect(noLinks.length).toBe(0);

Expand All @@ -49,7 +55,7 @@ describe('<Notification />', () => {
{ href: '/2', label: 'two', isExternal: true }
];

render(<Notification type='info' links={linkItems} />);
render(<Alert status='info' links={linkItems} />);
const links = screen.queryAllByRole('listitem');
expect(links.length).toBe(2);

Expand All @@ -66,12 +72,12 @@ describe('<Notification />', () => {
expect(externalIcon).toHaveClass('cf-icon-svg__external-link');
});

it('renders field-level notifications', async () => {
it('renders field-level alerts', async () => {
const testId = 'field-level-warning';
render(
<Notification
<Alert
data-testid={testId}
type='warning'
status='warning'
isFieldLevel
message='squish'
/>
Expand All @@ -87,19 +93,29 @@ describe('<Notification />', () => {
expect(message).toBeInTheDocument();
});

it('provides feedback for unsupported field-level notification type', async () => {
it('renders field-level alerts as its own component', async () => {
const testId = 'field-level-warning';
render(
<Notification
data-testid={testId}
type='info'
isFieldLevel
message='squish'
/>
<AlertFieldLevel data-testid={testId} status='warning' message='squish' />
);
const element = screen.getByTestId(testId);

// Renders Icon
const icon = await within(element).findByRole('img');
expect(icon).toBeInTheDocument();

// Render message
const message = screen.queryByTestId('message');
expect(message).toBeInTheDocument();
});

it('provides feedback for unsupported field-level alert type', async () => {
const testId = 'field-level-warning';
render(
<Alert data-testid={testId} status='info' isFieldLevel message='squish' />
);
// Renders error message
const message =
'[Error] Unsupported field-level notification type provided: info';
const message = '[Error] Unsupported field-level alert type provided: info';
expect(await screen.findByText(message)).toBeVisible();
});
});