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(web-react): Add front to toast component #1397

Merged
merged 2 commits into from
May 14, 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
8 changes: 4 additions & 4 deletions apps/web-twig-demo/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2268,10 +2268,10 @@ core-js-compat@^3.36.1:
dependencies:
browserslist "^4.23.0"

core-js@3.36.1:
version "3.36.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.36.1.tgz#c97a7160ebd00b2de19e62f4bbd3406ab720e578"
integrity sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==
core-js@3.37.0:
version "3.37.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.37.0.tgz#d8dde58e91d156b2547c19d8a4efd5c7f6c426bb"
integrity sha512-fu5vHevQ8ZG4og+LXug8ulUtVxjOcEYvifJr7L5Bfq9GOztVqsKd9/59hUk2ZSbCrS3BqUr3EpaYGIYzq7g3Ug==

core-util-is@~1.0.0:
version "1.0.3"
Expand Down
70 changes: 45 additions & 25 deletions packages/web-react/src/components/Toast/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,25 +102,41 @@ sorted from top to bottom for the `top` vertical alignment, and from bottom to t
👉 Please note the _actual_ order in the DOM is followed when users tab over the interface, no matter the _visual_
order of the toast queue.

#### Toast Queue Limitations
#### Collapsing

The collapsible Toast queue is turned on by default and can hold up to 3 ToastBar components.
When the queue is full, the oldest ToastBar components are collapsed at the start of
the queue and are only accessible by closing the newer ones.
literat marked this conversation as resolved.
Show resolved Hide resolved

While the Toast queue becomes scrollable when it does not fit the screen, we recommend displaying only a few toasts at
once for several reasons:
#### Scrolling

⚠️ **We strongly discourage from displaying too many toasts at once as it may cause the page to be unusable,
especially on mobile screens. As of now, there is no automatic stacking of the toast queue items. It is the
responsibility of the developer to ensure that the Toast queue does not overflow the screen.**
By default, the Toast queue collapses when there are more than 3 ToastBars. To turn off this behavior and make the queue scrollable when it does not fit the screen,
set the `isCollapsible` prop to `false`.

⚠️ Please note that scrolling is not available on iOS devices due to a limitation in the WebKit engine.

👉 Please note that the initial scroll position is always at the **top** of the queue.

```jsx
<Toast isCollapsible={false}>
<!-- ToastBar components go here -->
</Toast>
```

#### Toast Queue Limitations

👉 Please note only the _visible_ ToastBar components are scrollable. Collapsed items are not accessible until visible
items are dismissed.

👉 For the sake of simplicity, the collapsible items limit cannot be configured at the moment.

### API

| Name | Type | Default | Required | Description |
| ------------ | ----------------------------------------------------------- | -------- | -------- | --------------------------------------- |
| `alignmentX` | [[AlignmentX dictionary][dictionary-alignment] \| `object`] | `center` | ✕ | Horizontal alignment of the toast queue |
| `alignmentY` | [`top` \| `bottom` \| `object`] | `bottom` | ✕ | Vertical alignment of the toast queue |
| Name | Type | Default | Required | Description |
| --------------- | ----------------------------------------------------------- | -------- | -------- | ----------------------------------------------------------------- |
| `alignmentX` | [[AlignmentX dictionary][dictionary-alignment] \| `object`] | `center` | ✕ | Horizontal alignment of the toast queue |
| `alignmentY` | [`top` \| `bottom` \| `object`] | `bottom` | ✕ | Vertical alignment of the toast queue |
| `isCollapsible` | `bool` | `true` | ✕ | If true, Toast queue collapses if there are more than 3 ToastBars |

On top of the API options, the components accept [additional attributes][readme-additional-attributes].
If you need more control over the styling of a component, you can use [style props][readme-style-props]
Expand Down Expand Up @@ -302,7 +318,7 @@ const { show } = useToast(); // must be inside ToastProvider
Show Toast
</Button>

<UncontrolledToast alignmentX="right" alignmentY="top" closeLabel="Close toast" hasIcon isDismissible />
<UncontrolledToast alignmentX="right" alignmentY="top" closeLabel="Close toast" hasIcon isDismissible isCollapsible />
</ToastProvider>;
```

Expand All @@ -315,44 +331,48 @@ This hook returns:

| Name | Type | Default | Description |
| ---------- | ------------------------------------------------------------ | ---------- | --------------------------------------------------- |
| `clear` | `() => void` | () => {} | Function that will clear toast queue |
pavelklibani marked this conversation as resolved.
Show resolved Hide resolved
| `color` | [[Emotion Color dictionary][dictionary-color] \| `inverted`] | `inverted` | Color variant |
| `hide` | `function` | () => {} | Function that will hide UncontrolledToast |
| `hide` | `(toastId) => void` | () => {} | Function that will hide UncontrolledToast |
pavelklibani marked this conversation as resolved.
Show resolved Hide resolved
| `iconName` | `string` | — | Name of a custom icon to be shown along the message |
| `id` | `string` | `''` | ToastBar ID |
| `isOpen` | `bool` | `false` | Open state of UncontrolledToast |
| `message` | [`string` \| `ReactNode`] | null | Message inside UncontrolledToast |
| `show` | `function` | () => {} | Function that will show UncontrolledToast |
| `show` | `(message, toastId, options?) => void` | () => {} | Function that will show UncontrolledToast |

#### How to use `showToast` function:
#### How to use `show` function:

This function has two required parameters: message and ID.
All other options are not required and can be omitted entirely.

```jsx
const { show } = useToast();

┌─⫸ Message inside UncontrolledToast (required)
│ ┌─⫸ ToastBar ID (required)
│ │
show('Toast message', 'toast-id', {
color: 'danger', // Color variant, default: 'inverted'
iconName: 'download', // Name of a custom icon to be shown along the message, default: undefined
})
hasIcon: true // If true, an icon is shown along the message, default: false \*
isDismissible: true // If true, ToastBar can be dismissed by user, default: false
});
```

### API

| Name | Type | Default | Required | Description |
| --------------- | ----------------------------------------------------------- | ---------- | -------- | ------------------------------------------- |
| `alignmentX` | [[AlignmentX dictionary][dictionary-alignment] \| `object`] | `center` | ✕ | Horizontal alignment of the toast queue |
| `alignmentY` | [`top` \| `bottom` \| `object`] | `bottom` | ✕ | Vertical alignment of the toast queue |
| `closeLabel` | `string` | `Close` | ✕ | Close label |
| `hasIcon` | `bool` | `false` \* | ✕ | If true, an icon is shown along the message |
| `isDismissible` | `bool` | `true` | ✕ | If true, ToastBar can be dismissed by user |

(\*) For each emotion color, a default icon is defined.
The icons come from the [Icon package][icon-package], or from your custom source of icons.
Read the section [Default Icons according to Color Variant](#default-icons-according-to-color-variant).

### API

| Name | Type | Default | Required | Description |
| --------------- | ----------------------------------------------------------- | -------- | -------- | ----------------------------------------------------------------- |
| `alignmentX` | [[AlignmentX dictionary][dictionary-alignment] \| `object`] | `center` | ✕ | Horizontal alignment of the toast queue |
| `alignmentY` | [`top` \| `bottom` \| `object`] | `bottom` | ✕ | Vertical alignment of the toast queue |
| `closeLabel` | `string` | `Close` | ✕ | Close label |
| `isCollapsible` | `bool` | `true` | ✕ | If true, Toast queue collapses if there are more than 3 ToastBars |

On top of the API options, the components accept [additional attributes][readme-additional-attributes].
If you need more control over the styling of a component, you can use [style props][readme-style-props]
and [escape hatches][readme-escape-hatches].
Expand Down
10 changes: 9 additions & 1 deletion packages/web-react/src/components/Toast/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@ import classNames from 'classnames';
import React from 'react';
import { useStyleProps } from '../../hooks';
import { SpiritToastProps } from '../../types';
import { AlignmentX, AlignmentY } from '../../constants';
import { useToastStyleProps } from './useToastStyleProps';

const defaultProps: SpiritToastProps = {
alignmentX: AlignmentX.CENTER,
alignmentY: AlignmentY.BOTTOM,
isCollapsible: true,
};

const Toast = (props: SpiritToastProps) => {
const { children, alignmentX = 'center', alignmentY = 'bottom', ...restProps } = props;
const propsWithDefaults = { ...defaultProps, ...props };
const { children, alignmentX, alignmentY, ...restProps } = propsWithDefaults;
const { classProps, props: modifiedProps } = useToastStyleProps({ ...restProps, alignmentX, alignmentY });
const { styleProps, props: otherProps } = useStyleProps(modifiedProps);

Expand Down
26 changes: 14 additions & 12 deletions packages/web-react/src/components/Toast/ToastBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,21 @@ const ToastBar = (props: SpiritToastBarProps) => {
className={classNames(classProps.root, TRANSITIONING_STYLES[transitionState], styleProps.className)}
ref={rootElementRef}
>
<div className={classProps.content}>
{(hasIcon || iconName) && <Icon name={toastIconName} boxSize={ICON_BOX_SIZE} />}
<div className={classProps.message}>{children}</div>
</div>
<div className={classProps.box}>
<div className={classProps.content}>
{(hasIcon || iconName) && <Icon name={toastIconName} boxSize={ICON_BOX_SIZE} />}
<div className={classProps.message}>{children}</div>
</div>

<ToastCloseButton
id={id}
color={color}
isOpen={isOpen}
closeLabel={closeLabel}
onClose={onClose}
isDismissible={isDismissible}
/>
<ToastCloseButton
id={id}
color={color}
isOpen={isOpen}
closeLabel={closeLabel}
onClose={onClose}
isDismissible={isDismissible}
/>
</div>
</div>
)}
</Transition>
Expand Down
123 changes: 87 additions & 36 deletions packages/web-react/src/components/Toast/ToastContext.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
import React, { FC, ReactNode, createContext, useCallback, useMemo, useReducer } from 'react';
import { ToastColorType } from '../../types';

interface ToastState {
type ToastState = {
queue: ToastItem[];
};

export interface ToastItem {
color: ToastColorType | undefined;
iconName: string | undefined;
hasIcon: boolean;
isDismissible: boolean;
id: string;
isOpen: boolean;
message: string | JSX.Element | null;
message: string | JSX.Element;
}

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

const initialToastState: ToastState = {
color: undefined,
iconName: undefined,
id: '',
isOpen: false,
message: null,
};

const defaultToastContext: ToastContextType = {
clear: () => {},
hide: () => {},
queue: [],
setQueue: () => {},
show: () => {},
...initialToastState,
};

export const ToastContext = createContext<ToastContextType>(defaultToastContext);
Expand All @@ -36,27 +42,41 @@ type ActionType =
payload: {
text: string | JSX.Element;
toastId: string;
options?: { color?: ToastColorType; iconName?: string };
options?: { color?: ToastColorType; iconName?: string; hasIcon?: boolean; isDismissible?: boolean };
};
}
| { type: 'hide'; payload: null };
| { type: 'hide'; payload: { id: string } }
| { type: 'clear'; payload: null }
| { type: 'setQueue'; payload: { newQueue: ToastItem[] } };

const reducer = (state: ToastState, action: ActionType): ToastState => {
const { type, payload } = action;
const { queue: currentQueue } = state;

switch (type) {
case 'show':
return {
...state,
message: payload?.text,
id: payload?.toastId,
color: payload?.options?.color || undefined,
iconName: payload?.options?.iconName || undefined,
isOpen: true,
};
case 'show': {
const newQueue = [
...currentQueue,
{
color: payload.options?.color || undefined,
hasIcon: payload.options?.hasIcon || false,
iconName: payload.options?.iconName,
id: payload.toastId,
isDismissible: payload.options?.isDismissible || false,
isOpen: true,
message: payload.text,
},
];

return { queue: newQueue };
}

case 'hide': {
return { queue: currentQueue.filter((item) => item.id !== payload.id) };
}

case 'hide':
return { ...state, isOpen: false };
case 'clear':
return { queue: [] };

default:
throw new Error();
Expand All @@ -67,31 +87,62 @@ interface ToastProviderProps {
children: ReactNode;
}

const initialToastState: ToastState = {
queue: [],
};

export const ToastProvider: FC<ToastProviderProps> = ({ children }) => {
const [{ message, id, color, iconName, isOpen }, dispatch] = useReducer(reducer, initialToastState);
const [state, dispatch] = useReducer(reducer, initialToastState);
const { queue } = state;

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

const hide = useCallback(() => {
dispatch({ type: 'hide', payload: null });
const hide = useCallback((id: string) => {
dispatch({ type: 'hide', payload: { id } });
}, []);

const clear = useCallback(() => {
dispatch({ type: 'clear', payload: null });
}, []);

const setQueue = useCallback((newQueue: ToastItem[]) => {
dispatch({ type: 'clear', payload: null });

newQueue.forEach((item) => {
dispatch({
type: 'show',
payload: {
text: item.message,
toastId: item.id,
options: {
color: item.color,
iconName: item.iconName,
hasIcon: item.hasIcon,
isDismissible: item.isDismissible,
},
},
});
});
}, []);

const contextValue = useMemo(
() => ({
color,
clear,
hide,
iconName,
id,
isOpen,
message,
queue,
setQueue,
show,
}),
[color, hide, iconName, id, isOpen, message, show],
[hide, show, clear, queue.length, setQueue],
);

return <ToastContext.Provider value={contextValue}>{children}</ToastContext.Provider>;
Expand Down
Loading
Loading