Skip to content

Commit

Permalink
[Toast] Add tone, actionOnComponent and leadingIcon props to Toast (S…
Browse files Browse the repository at this point in the history
…hopify#11431)

<!--
  ☝️How to write a good PR title:
- Prefix it with [ComponentName] (if applicable), for example: [Button]
  - Start with a verb, for example: Add, Delete, Improve, Fix…
  - Give as much context as necessary and as little as possible
  - Open it as a draft if it’s a work in progress
-->

### WHY are these changes introduced?

We would like to add more customisation to the current Toast component:
- tone: this allows us to provide specific tones for styling.
- actionOnComponent: this makes the whole component clickable instead of
having a link inside
- leadingIcon: we can use custom icons in the component.

### WHAT is this pull request doing?

#### Magic tone

We can change the tone of the toast to `magic`

<img width="224" alt="Screenshot 2024-01-11 at 20 19 45"
src="https://github.com/Shopify/polaris/assets/78884/43f7809d-5348-425a-a4cc-3aaa797fffb8">

#### Action on component

The whole component is now clickable.


https://github.com/Shopify/polaris/assets/78884/56e06a79-8e37-40d9-becf-828e8aa5c9f0

#### Leading icon

In this example, we have both the magic tone, the leading icon, and the
Action On Component



https://github.com/Shopify/polaris/assets/78884/64ec9807-a340-472e-918d-d881c20d0172


### How to 🎩

🖥 [Local development
instructions](https://github.com/Shopify/polaris/blob/main/README.md#install-dependencies-and-build-workspaces)
🗒 [General tophatting
guidelines](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting.md)
📄 [Changelog
guidelines](https://github.com/Shopify/polaris/blob/main/.github/CONTRIBUTING.md#changelog)

### 🎩 checklist

- [x] Tested a
[snapshot](https://github.com/Shopify/polaris/blob/main/documentation/Releasing.md#-snapshot-releases)
- [x] Tested on
[mobile](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting.md#cross-browser-testing)
- [x] Tested on [multiple
browsers](https://help.shopify.com/en/manual/shopify-admin/supported-browsers)
- [x] Tested for
[accessibility](https://github.com/Shopify/polaris/blob/main/documentation/Accessibility%20testing.md)
- [x] Updated the component's `README.md` with documentation changes
- [x] [Tophatted
documentation](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting%20documentation.md)
changes in the style guide
  • Loading branch information
lone-star authored Jan 24, 2024
1 parent d2b382f commit f3fbabc
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/unlucky-schools-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/polaris': minor
---

Added `tone`, `icon`, and `onClick` props to `Toast`
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,35 @@ $Backdrop-opacity: 0.88;
color: var(--p-color-text-inverse);
}
}

.toneMagic {
background-color: var(--p-color-bg-fill-magic-secondary);
color: var(--p-color-text-magic);

.CloseButton {
color: var(--p-color-text-magic);
}

.Action {
color: var(--p-color-text-magic);
}
}

.WithActionOnComponent {
border: none;
cursor: pointer;
line-height: var(--p-font-line-height-500);
font-size: var(--p-font-size-325);
padding-right: var(--p-space-500);
}

.WithActionOnComponent.toneMagic {
&:focus,
&:hover {
background-color: var(--p-color-bg-fill-magic-secondary-hover);
}

&:active {
background-color: var(--p-color-bg-fill-magic-secondary-active);
}
}
60 changes: 52 additions & 8 deletions polaris-react/src/components/Frame/components/Toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, {useEffect} from 'react';
import {AlertCircleIcon, XSmallIcon} from '@shopify/polaris-icons';

import {classNames} from '../../../../utilities/css';
import {classNames, variationName} from '../../../../utilities/css';
import {Key} from '../../../../types';
import {Button} from '../../../Button';
import {Icon} from '../../../Icon';
Expand All @@ -24,6 +24,9 @@ export function Toast({
duration,
error,
action,
tone,
onClick,
icon,
}: ToastProps) {
useEffect(() => {
let timeoutDuration = duration || DEFAULT_TOAST_DURATION;
Expand Down Expand Up @@ -67,20 +70,61 @@ export function Toast({
</div>
) : null;

const leadingIconMarkup = error ? (
<div className={styles.LeadingIcon}>
<Icon source={AlertCircleIcon} tone="inherit" />
</div>
) : null;
let leadingIconMarkup = null;

if (error) {
leadingIconMarkup = (
<div className={styles.LeadingIcon}>
<Icon source={AlertCircleIcon} tone="inherit" />
</div>
);
} else if (icon) {
leadingIconMarkup = (
<div className={styles.LeadingIcon}>
<Icon source={icon} tone="inherit" />
</div>
);
}

const className = classNames(styles.Toast, error && styles.error);
const className = classNames(
styles.Toast,
error && styles.error,
tone && styles[variationName('tone', tone)],
);

if (!action && onClick) {
return (
<button
aria-live="assertive"
className={classNames(className, styles.WithActionOnComponent)}
type="button"
onClick={onClick}
>
<KeypressListener keyCode={Key.Escape} handler={onDismiss} />
{leadingIconMarkup}
<InlineStack gap="400" blockAlign="center">
<Text
as="span"
fontWeight="medium"
{...(tone === 'magic' && {tone: 'magic'})}
>
{content}
</Text>
</InlineStack>
</button>
);
}

return (
<div className={className} aria-live="assertive">
<KeypressListener keyCode={Key.Escape} handler={onDismiss} />
{leadingIconMarkup}
<InlineStack gap="400" blockAlign="center">
<Text as="span" fontWeight="medium">
<Text
as="span"
fontWeight="medium"
{...(tone === 'magic' && {tone: 'magic'})}
>
{content}
</Text>
</InlineStack>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React from 'react';
import {timer} from '@shopify/jest-dom-mocks';
import {mountWithApp} from 'tests/utilities';
import {CheckIcon} from '@shopify/polaris-icons';

import {Button} from '../../../../Button';
import {Toast} from '../Toast';
import type {ToastProps} from '../Toast';
import {Key} from '../../../../../types';
import {Icon} from '../../../../Icon';

interface HandlerMap {
[eventName: string]: any;
Expand Down Expand Up @@ -38,13 +40,25 @@ describe('<Toast />', () => {
});
});

it('renders a Toast with the magic tone when tone is "magic"', () => {
const message = mountWithApp(<Toast {...mockProps} tone="magic" />);
expect(message).toContainReactComponent('div', {
className: 'Toast toneMagic',
});
});

describe('dismiss button', () => {
it('renders by default', () => {
const message = mountWithApp(<Toast {...mockProps} />);
expect(message).toContainReactComponent('button');
});
});

it('renders a leading icon if an icon is provided', () => {
const message = mountWithApp(<Toast icon={CheckIcon} {...mockProps} />);
expect(message).toContainReactComponent(Icon, {source: CheckIcon});
});

describe('action', () => {
const mockAction = {
content: 'Do something',
Expand Down Expand Up @@ -193,6 +207,24 @@ describe('<Toast />', () => {
expect(spy).not.toHaveBeenCalled();
});
});

describe('onClick', () => {
it('wraps the toast in a button when provided', () => {
const spy = jest.fn();
const message = mountWithApp(<Toast {...mockProps} onClick={spy} />);

expect(message.find('button')).toContainReactText(mockProps.content);
});

it('fires the callback when the toast is clicked', () => {
const spy = jest.fn();
const message = mountWithApp(<Toast {...mockProps} onClick={spy} />);

message.find('button')?.trigger('onClick');

expect(spy).toHaveBeenCalledTimes(1);
});
});
});

function noop() {}
75 changes: 75 additions & 0 deletions polaris-react/src/components/Toast/Toast.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
BlockStack,
TextContainer,
} from '@shopify/polaris';
import {MagicIcon} from '@shopify/polaris-icons';

export default {
component: Toast,
Expand Down Expand Up @@ -217,3 +218,77 @@ export function InsideModal() {
</div>
);
}

export function Magic() {
const [active, setActive] = useState(false);

const toggleActive = useCallback(() => setActive((active) => !active), []);

const toastMarkup = active ? (
<Toast
content="Magic message"
onDismiss={toggleActive}
tone="magic"
duration={3000000}
/>
) : null;

return (
<div style={{height: '250px'}}>
<Frame>
<Page title="Default">
<Button onClick={toggleActive}>Show Magic Toast</Button>
{toastMarkup}
</Page>
</Frame>
</div>
);
}

export function WithOnClick() {
const [active, setActive] = useState(false);

const toggleActive = useCallback(() => setActive((active) => !active), []);

const toastMarkup = active ? (
<Toast content="Message Toast" onClick={toggleActive} duration={3000000} />
) : null;

return (
<div style={{height: '250px'}}>
<Frame>
<Page title="Default">
<Button onClick={toggleActive}>Show Magic Toast</Button>
{toastMarkup}
</Page>
</Frame>
</div>
);
}

export function MagicWithOnClick() {
const [active, setActive] = useState(false);

const toggleActive = useCallback(() => setActive((active) => !active), []);

const toastMarkup = active ? (
<Toast
content="Magic message"
tone="magic"
duration={3000000}
icon={MagicIcon}
onClick={toggleActive}
/>
) : null;

return (
<div style={{height: '250px'}}>
<Frame>
<Page title="Default">
<Button onClick={toggleActive}>Show Magic Toast</Button>
{toastMarkup}
</Page>
</Frame>
</div>
);
}
22 changes: 18 additions & 4 deletions polaris-react/src/utilities/frame/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {Action} from '../../types';
import type {Action, IconSource} from '../../types';

export interface Logo {
/** Provides a path for a logo used on a dark background */
Expand Down Expand Up @@ -53,7 +53,7 @@ export interface ContextualSaveBarProps {

// Toast

export interface ToastProps {
interface BaseToastProps {
/** The content that should appear in the toast message */
content: string;
/**
Expand All @@ -63,12 +63,26 @@ export interface ToastProps {
duration?: number;
/** Display an error toast. */
error?: boolean;
/** Callback when the dismiss icon is clicked */
onDismiss(): void;
/** Indicates the tone of the toast */
tone?: 'magic';
/** Icon prefix for the toast content */
icon?: IconSource;
}

interface ClickableToast {
/** Callback fired when the toast is clicked or keypressed */
onClick?(): void;
}

interface DismissableToast {
/** Adds an action next to the message */
action?: Action;
/** Callback when the dismiss icon is clicked */
onDismiss(): void;
}

export type ToastProps = BaseToastProps & ClickableToast & DismissableToast;

export interface ToastID {
id: string;
}
Expand Down

0 comments on commit f3fbabc

Please sign in to comment.