Skip to content

Commit

Permalink
Feat(web-react): Add Message and Link for ToastBar #DS-1213
Browse files Browse the repository at this point in the history
  • Loading branch information
curdaj authored and crishpeen committed May 24, 2024
1 parent 76ccd52 commit ba89802
Show file tree
Hide file tree
Showing 17 changed files with 246 additions and 92 deletions.
77 changes: 57 additions & 20 deletions packages/web-react/src/components/Toast/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ Toast is a composition of a few subcomponents:

- [Toast](#toast)
- [ToastBar](#toastbar)
- [ToastBarComponents](#toastbar-components)
- [ToastBarMessage](#toastbarmessage)
- [ToastBarLink](#toastbarlink)
- [UncontrolledToast](#uncontrolledToast)

## Toast
Expand Down Expand Up @@ -185,21 +188,51 @@ Alternatively, a custom icon can be used:
| `success` | `check-plain` |
| `warning` | `warning` |

### Action Link
### ToastBar Components

An action link can be added to the ToastBar component:
The content of `ToastBar` can be assembled from the following components:

#### ToastBarMessage

`ToastBarMessage` is a component designates for main message in `ToastBar`.

Usage example:

```jsx
<ToastBar id="my-toast">
Message with action
<Link href="#" color="inverted" isUnderlined>
Action
</Link>
<ToastBar id="my-toast" isOpen={isOpen} onClose={() => setIsOpen(false)} isDismissible>
<ToastBarMessage>This is the main toast message.</ToastBarMessage>
</ToastBar>
```

👉 For the sake of flexibility, developers can pass the link as part of the message. However, it is strongly recommended
to use the **inverted underlined** variant of the link (for all ToastBar colors) to make it stand out from the message.
#### API

| Name | Type | Default | Required | Description |
| ---------- | ----------- | ------- | -------- | ------------------------------ |
| `children` | `ReactNode` ||| Content of the ToastBarMessage |

#### ToastBarLink

`ToastBarLink` is a component designated to create an action link within `ToastBar`.

Usage example:

```jsx
<ToastBar id="my-toast" isOpen={isOpen} onClose={() => setIsOpen(false)} isDismissible>
<ToastBarLink href="#">This is the action link.</ToastBarLink>
</ToastBar>
```

#### API

| Name | Type | Default | Required | Description |
| -------------- | ------------------------------------------------ | ---------- | -------- | ------------------------------ |
| `children` | `ReactNode` ||| Content of the ToastBarLink |
| `color` | [Action Link Color dictionary][dictionary-color] | `inverted` || Color of the link |
| `elementType` | `ElementType` | `a` || Type of element used as |
| `href` | `string` ||| ToastBarLink's href attribute |
| `isDisabled` | `bool` | `false` || Whether is the link disabled |
| `isUnderlined` | `bool` | `true` || Whether is the link underlined |
| `ref` | `ForwardedRef<HTMLAnchorElement>` ||| Link element reference |

👉 **Do not put any important actions** like "Undo" in the ToastBar component (unless there are other means to perform
said action), as it is very hard (if not impossible) to reach for users with assistive technologies. Read more about
Expand All @@ -213,9 +246,11 @@ Use the `color` option to change the color of the ToastBar component.
For example:

```jsx
import ToastBarMessage from './ToastBarMessage';

<ToastBar id="my-toast" color="success">
Success message
</ToastBar>
<ToastBarMessage>Success message</ToastBarMessage>
</ToastBar>;
```

### Opening the ToastBar
Expand All @@ -224,7 +259,7 @@ Set `isOpen` prop to `true` to open a Toast **that is present in the DOM,** e.g.

```jsx
<ToastBar id="my-toast" isOpen>
Opened ToastBar
<ToastBarMessage>Opened ToastBar</ToastBarMessage>
</ToastBar>
```

Expand All @@ -236,7 +271,7 @@ To make the ToastBar dismissible, add the `isDismissible` prop along with a `onC

```jsx
<ToastBar id="my-toast" onClose={() => {}} isDismissible>
Dismissible message
<ToastBarMessage>Dismissible message</ToastBarMessage>
</ToastBar>
```

Expand Down Expand Up @@ -264,18 +299,20 @@ and [escape hatches][readme-escape-hatches].
## Full Example

```jsx
import { Button, Toast, ToastBar } from '@lmc-eu/spirit-web-react/components';
import {Button, Toast, ToastBar} from '@lmc-eu/spirit-web-react/components';
import ToastBarMessage from "@lmc-eu/spirit-web-react/components/ToastBarMessage";
import ToastBarLink from "@lmc-eu/spirit-web-react/components/ToastBarLink";

const [isOpen, setIsOpen] = React.useState(false)
const [isOpen, setIsOpen] = useState(false);

<Button onClick={() => setIsOpen(true)} aria-expanded={isOpen} aria-controls="my-toast">
{buttonLabel}
</Button>

<Toast>
<ToastBar id="my-toast" isOpen={isOpen} onClose={() => setIsOpen(false)} isDismissible>
Toast message
<Link href="#" color="inverted" isUnderlined>Action</Link>
<ToastBarMessage>Toast message</ToastBarMessage>
<ToastBarLink href="#">Action</ToastBarLink>
</ToastBar>
</Toast>
```
Expand Down Expand Up @@ -353,10 +390,10 @@ const { show } = useToast();
│ ┌─⫸ ToastBar ID (required)
│ │
show('Toast message', 'toast-id', {
autoCloseInterval: 3000 // Set interval in ms after ToastBar will be closed, default: 3000
autoCloseInterval: 3000, // Set interval in ms after ToastBar will be closed, default: 3000
color: 'danger', // Color variant, default: 'inverted'
enableAutoClose: true // If true, ToastBar will close after `autoCloseInterval`, default: true
hasIcon: true // If true, an icon is shown along the message, default: false \*
enableAutoClose: true, // If true, ToastBar will close after `autoCloseInterval`, default: true
hasIcon: true, // If true, an icon is shown along the message, default: false \*
iconName: 'download', // Name of a custom icon to be shown along the message, default: undefined
isDismissible: true // If true, ToastBar can be dismissed by user, default: false
});
Expand Down
7 changes: 3 additions & 4 deletions packages/web-react/src/components/Toast/ToastBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ const ToastBar = (props: SpiritToastBarProps) => {
...restProps,
color,
isDismissible,
id,
});
const { styleProps, props: otherProps } = useStyleProps(modifiedProps);

Expand All @@ -44,15 +43,15 @@ const ToastBar = (props: SpiritToastBarProps) => {
<div
{...styleProps}
{...otherProps}
id={id}
className={classNames(classProps.root, TRANSITIONING_STYLES[transitionState], styleProps.className)}
ref={rootElementRef}
>
<div className={classProps.box}>
<div className={classProps.content}>
<div className={classProps.container}>
{(hasIcon || iconName) && <Icon name={toastIconName} boxSize={ICON_BOX_SIZE} />}
<div className={classProps.message}>{children}</div>
<div className={classProps.content}>{children}</div>
</div>

<ToastCloseButton
id={id}
color={color}
Expand Down
39 changes: 39 additions & 0 deletions packages/web-react/src/components/Toast/ToastBarLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { ElementType, ForwardedRef, forwardRef } from 'react';
import classNames from 'classnames';
import { SpiritLinkProps } from '../../types';
import { useStyleProps } from '../../hooks';
import { Link } from '../Link';
import { useToastBarStyleProps } from './useToastBarStyleProps';

const defaultProps: Partial<SpiritLinkProps> = {
color: 'inverted',
isUnderlined: true,
};

/* We need an exception for components exported with forwardRef */
/* eslint no-underscore-dangle: ['error', { allow: ['_ToastBarLink'] }] */
const _ToastBarLink = <E extends ElementType = typeof Link, C = void>(
props: SpiritLinkProps<E, C>,
ref: ForwardedRef<HTMLAnchorElement>,
) => {
const propsWithDefaults = { ...defaultProps, ...props };
const { children, ...restProps } = propsWithDefaults;
const { classProps, props: modifiedProps } = useToastBarStyleProps({ ...restProps });
const { styleProps, props: otherProps } = useStyleProps(modifiedProps);

return (
<Link
{...propsWithDefaults}
{...otherProps}
ref={ref}
UNSAFE_className={classNames(classProps.link, styleProps.className)}
style={styleProps.style}
>
{children}
</Link>
);
};

export const ToastBarLink = forwardRef<HTMLAnchorElement, SpiritLinkProps<ElementType>>(_ToastBarLink);

export default ToastBarLink;
21 changes: 21 additions & 0 deletions packages/web-react/src/components/Toast/ToastBarMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import classNames from 'classnames';
import { useClassNamePrefix, useStyleProps } from '../../hooks';
import { ChildrenProps } from '../../types';

const ToastBarMessage = (props: ChildrenProps) => {
const { children, ...restProps } = props;
const { styleProps, props: otherProps } = useStyleProps(restProps);

return (
<div
{...styleProps}
{...otherProps}
className={classNames(useClassNamePrefix('text-truncate-multiline'), styleProps.className)}
>
{children}
</div>
);
};

export default ToastBarMessage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import ToastBarLink from '../ToastBarLink';
import { restPropsTest } from '../../../../tests/providerTests/restPropsTest';
import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest';

describe('ToastBarLink', () => {
stylePropsTest(ToastBarLink);

restPropsTest(ToastBarLink, 'a');

beforeEach(() => {
render(<ToastBarLink href="example-href">Example action</ToastBarLink>);
});

it('should render with correct href', () => {
const element = screen.getByRole('link');

expect(element).toBeInTheDocument();
expect(element).toHaveAttribute('href', 'example-href');
});

it('should render children', () => {
expect(screen.getByText('Example action')).toBeInTheDocument();
});

it('should render with correct classnames', () => {
const element = screen.getByRole('link');

expect(element).toHaveClass('link-inverted');
expect(element).toHaveClass('link-underlined');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import React from 'react';
import ToastBarMessage from '../ToastBarMessage';
import { restPropsTest } from '../../../../tests/providerTests/restPropsTest';
import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest';

describe('ToastBarMessage', () => {
stylePropsTest(ToastBarMessage);

restPropsTest(ToastBarMessage, 'div');

beforeEach(() => {
render(<ToastBarMessage>Example children</ToastBarMessage>);
});

it('should render children', () => {
expect(screen.getByText('Example children')).toBeInTheDocument();
});

it('should render with truncate className', () => {
expect(screen.getByText('Example children')).toHaveClass('text-truncate-multiline');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('UncontrolledToast', () => {
expect(elementToast).toBeInTheDocument();
expect(elementToastBar).toBeInTheDocument();
expect(elementToastBar).toHaveClass('is-open ToastBar--inverted');
expect(elementToastBar.querySelector('.ToastBar .ToastBar__content svg')).not.toBeInTheDocument();
expect(elementToastBar.querySelector('.ToastBar .ToastBar__container svg')).not.toBeInTheDocument();
});

it('should render opened toast with params', () => {
Expand All @@ -75,7 +75,7 @@ describe('UncontrolledToast', () => {
expect(elementToastBar).toBeInTheDocument();
expect(elementToast).toHaveClass('Toast--right Toast--top');
expect(elementToastBar).toHaveClass('ToastBar ToastBar--inverted ToastBar--dismissible is-open');
expect(elementToastBar.querySelector('.ToastBar__content svg')).toBeInTheDocument();
expect(elementToastBar.querySelector('.ToastBar__container svg')).toBeInTheDocument();
expect(elementToastBar.querySelector('button')).toHaveTextContent('Close test');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('useToastBarStyleProps', () => {

expect(result.current.classProps.root).toBe('ToastBar ToastBar--inverted');
expect(result.current.classProps.content).toBe('ToastBar__content');
expect(result.current.classProps.message).toBe('ToastBar__message');
expect(result.current.classProps.container).toBe('ToastBar__container');
});

it('should return dismissible class', () => {
Expand Down
33 changes: 12 additions & 21 deletions packages/web-react/src/components/Toast/demo/ToastColors.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,30 @@
import React from 'react';
import { Link } from '../../Link';
import ToastBar from '../ToastBar';
import ToastBarMessage from '../ToastBarMessage';
import ToastBarLink from '../ToastBarLink';

const ToastColors = () => {
return (
<>
<ToastBar id="inverted" onClose={() => {}} color="inverted" hasIcon isDismissible>
Inverted
<Link href="#" color="inverted" isUnderlined>
Action
</Link>
<ToastBarMessage>Inverted</ToastBarMessage>
<ToastBarLink href="#">Action</ToastBarLink>
</ToastBar>
<ToastBar id="informative" onClose={() => {}} color="informative" hasIcon isDismissible>
Informative
<Link href="#" color="inverted" isUnderlined>
Action
</Link>
<ToastBarMessage>Informative</ToastBarMessage>
<ToastBarLink href="#">Action</ToastBarLink>
</ToastBar>
<ToastBar id="success" onClose={() => {}} color="success" hasIcon isDismissible>
Success
<Link href="#" color="inverted" isUnderlined>
Action
</Link>
<ToastBarMessage>Success</ToastBarMessage>
<ToastBarLink href="#">Action</ToastBarLink>
</ToastBar>
<ToastBar id="warning" onClose={() => {}} color="warning" hasIcon isDismissible>
Warning
<Link href="#" color="inverted" isUnderlined>
Action
</Link>
<ToastBarMessage>Warning</ToastBarMessage>
<ToastBarLink href="#">Action</ToastBarLink>
</ToastBar>
<ToastBar id="danger" onClose={() => {}} color="danger" hasIcon isDismissible>
Danger
<Link href="#" color="inverted" isUnderlined>
Action
</Link>
<ToastBarMessage>Danger</ToastBarMessage>
<ToastBarLink href="#">Action</ToastBarLink>
</ToastBar>
</>
);
Expand Down
Loading

0 comments on commit ba89802

Please sign in to comment.