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

fixup! Feat(web-react): Add Message and Link for ToastBar #DS-1213 #1443

Closed
wants to merge 4 commits into from
Closed
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
82 changes: 58 additions & 24 deletions packages/web-react/src/components/Toast/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Toast is a composition of a few subcomponents:

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

## Toast
Expand Down Expand Up @@ -150,11 +152,11 @@ elements.
Minimum example:

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

```jsx
<ToastBar id="my-toast">Message only</ToastBar>
<ToastBar id="my-toast">
<ToastBarMessage>Message only</ToastBarMessage>
</ToastBar>;
```

### Optional Icon
Expand All @@ -163,15 +165,15 @@ An icon can be displayed in the ToastBar component, depending on the color of th

```jsx
<ToastBar id="my-toast" color="success" hasIcon>
Message with icon
<ToastBarMessage>Message with icon</ToastBarMessage>
</ToastBar>
```

Alternatively, a custom icon can be used:

```jsx
<ToastBar id="my-toast" iconName="download">
Message with custom icon
<ToastBarMessage>Message with custom icon</ToastBarMessage>
</ToastBar>
```

Expand All @@ -185,21 +187,51 @@ Alternatively, a custom icon can be used:
| `success` | `check-plain` |
| `warning` | `warning` |

### Action Link
### ToastBar Components

The content of `ToastBar` can be assembled from the following subcomponents:

#### ToastBarMessage

An action link can be added to the ToastBar component:
`ToastBarMessage` is a subcomponent designated for the 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 subcomponent 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 +245,11 @@ Use the `color` option to change the color of the ToastBar component.
For example:

```jsx
import { ToastBarMessage } from '@lmc-eu/spirit-web-react/components';

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

### Opening the ToastBar
Expand All @@ -224,7 +258,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 +270,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 All @@ -248,7 +282,7 @@ To make the ToastBar dismissible, add the `isDismissible` prop along with a `onC
| `color` | [[Emotion Color dictionary][dictionary-color] \| `inverted`] | `inverted` | ✕ | Color variant |
| `hasIcon` | `bool` | `false` \* | ✕ | If true, an icon is shown along the message |
| `iconName` | `string` | `info` \* | ✕ | Name of a custom icon to be shown along the message |
| `id` | `string` | — | | ToastBar ID |
| `id` | `string` | — | | ToastBar ID |
| `isDismissible` | `bool` | `false` | ✕ | If true, ToastBar can be dismissed by user |
| `isOpen` | `bool` | `true` | ✕ | If true, ToastBar is visible |
| `onClose` | `function` | — | ✕ | Close button callback |
Expand All @@ -264,18 +298,18 @@ and [escape hatches][readme-escape-hatches].
## Full Example

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

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
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
40 changes: 40 additions & 0 deletions packages/web-react/src/components/Toast/ToastBarLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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';
import { ActionLinkColors } from '../../constants';

const defaultProps: Partial<SpiritLinkProps> = {
color: ActionLinkColors.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;
65 changes: 21 additions & 44 deletions packages/web-react/src/components/Toast/ToastContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,23 @@ export interface ToastItem {
id: string;
isDismissible: boolean;
isOpen: boolean;
message: string | JSX.Element;
content: ReactNode;
}

type ShowPayloadOptions = {
autoCloseInterval?: number;
color?: ToastColorType;
enableAutoClose?: boolean;
hasIcon?: boolean;
iconName?: string;
isDismissible?: boolean;
};

export interface ToastContextType extends ToastState {
clear: () => void;
hide: (id: string) => void;
setQueue: (newQueue: ToastItem[]) => void;
show: (
text: string | JSX.Element,
id: string,
options?: {
autoCloseInterval?: number;
color?: ToastColorType;
enableAutoClose?: boolean;
hasIcon?: boolean;
iconName?: string;
isDismissible?: boolean;
},
) => void;
show: (content: ReactNode, id: string, options?: ShowPayloadOptions) => void;
}

const defaultToastContext: ToastContextType = {
Expand All @@ -51,16 +49,9 @@ type ActionType =
| {
type: 'show';
payload: {
text: string | JSX.Element;
content: ReactNode;
toastId: string;
options?: {
autoCloseInterval?: number;
color?: ToastColorType;
enableAutoClose?: boolean;
hasIcon?: boolean;
iconName?: string;
isDismissible?: boolean;
};
options?: ShowPayloadOptions;
};
}
| { type: 'hide'; payload: { id: string } }
Expand All @@ -84,7 +75,7 @@ const reducer = (state: ToastState, action: ActionType): ToastState => {
id: payload.toastId,
isDismissible: payload.options?.isDismissible || false,
isOpen: true,
message: payload.text,
content: payload.content,
},
];

Expand Down Expand Up @@ -123,26 +114,12 @@ export const ToastProvider: FC<ToastProviderProps> = ({ children }) => {
dispatch({ type: 'clear', payload: null });
}, []);

const show = useCallback(
(
text: string | JSX.Element,
toastId: string,
options?: {
autoCloseInterval?: number;
color?: ToastColorType;
enableAutoClose?: boolean;
hasIcon?: boolean;
iconName?: string;
isDismissible?: boolean;
},
) => {
dispatch({ type: 'show', payload: { text, toastId, options } });

options?.enableAutoClose &&
delayedCallback(() => hide(toastId), options?.autoCloseInterval || DEFAULT_TOAST_AUTO_CLOSE_INTERVAL);
},
[],
);
const show = useCallback((content: ReactNode, toastId: string, options?: ShowPayloadOptions) => {
dispatch({ type: 'show', payload: { content, toastId, options } });

options?.enableAutoClose &&
delayedCallback(() => hide(toastId), options?.autoCloseInterval || DEFAULT_TOAST_AUTO_CLOSE_INTERVAL);
}, []);
const setQueue = useCallback((newQueue: ToastItem[]) => {
dispatch({ type: 'clear', payload: null });

Expand All @@ -153,7 +130,7 @@ export const ToastProvider: FC<ToastProviderProps> = ({ children }) => {
dispatch({
type: 'show',
payload: {
text: item.message,
content: item.content,
toastId: item.id,
options: {
autoCloseInterval,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const UncontrolledToast = (props: UncontrolledToastProps) => {
return (
<Toast alignmentX={alignmentX} alignmentY={alignmentY} isCollapsible={isCollapsible}>
{queue.map((item) => {
const { color, iconName, id, isOpen, message, hasIcon, isDismissible } = item;
const { color, iconName, id, isOpen, content, hasIcon, isDismissible } = item;

return (
<ToastBar
Expand All @@ -24,9 +24,9 @@ const UncontrolledToast = (props: UncontrolledToastProps) => {
iconName={iconName}
isDismissible={isDismissible}
onClose={() => hide(id)}
isOpen={isOpen && !!message}
isOpen={isOpen && !!content}
>
{message}
{content}
</ToastBar>
);
})}
Expand Down
Loading
Loading