diff --git a/.changeset/tiny-eels-play.md b/.changeset/tiny-eels-play.md new file mode 100644 index 0000000000..67002a14d6 --- /dev/null +++ b/.changeset/tiny-eels-play.md @@ -0,0 +1,9 @@ +--- +"@justeattakeaway/pie-toast-provider": minor +"@justeattakeaway/pie-toast": minor +"@justeattakeaway/pie-webc": minor +"pie-storybook": minor +"pie-monorepo": minor +--- + +[Added] - priority order for the toast provider diff --git a/apps/pie-storybook/data/tag-variants-to-statuses-map.ts b/apps/pie-storybook/data/tag-variants-to-statuses-map.ts index 13ff8f6892..6f0d5e70f7 100644 --- a/apps/pie-storybook/data/tag-variants-to-statuses-map.ts +++ b/apps/pie-storybook/data/tag-variants-to-statuses-map.ts @@ -1,9 +1,9 @@ import { type TagVariantToStatusMap } from '../interfaces/tag-variant-to-status-map'; export const tagVariantToStatusMap: TagVariantToStatusMap = { - alpha: 'yellow', - beta: 'yellow', - deprecated: 'red', - removed: 'red', - stable: 'green', + alpha: 'brand-02', + beta: 'brand-02', + deprecated: 'error', + removed: 'error', + stable: 'information', }; diff --git a/apps/pie-storybook/stories/pie-toast-provider.stories.ts b/apps/pie-storybook/stories/pie-toast-provider.stories.ts index f7e026cc80..2608058173 100644 --- a/apps/pie-storybook/stories/pie-toast-provider.stories.ts +++ b/apps/pie-storybook/stories/pie-toast-provider.stories.ts @@ -1,19 +1,47 @@ import { html } from 'lit'; import { type Meta } from '@storybook/web-components'; +import { action } from '@storybook/addon-actions'; -import '@justeattakeaway/pie-toast-provider'; -import { type ToastProviderProps } from '@justeattakeaway/pie-toast-provider'; +import { toaster } from '@justeattakeaway/pie-toast-provider'; +import { type ToastProviderProps, defaultProps } from '@justeattakeaway/pie-toast-provider'; +import '@justeattakeaway/pie-button'; +import '@justeattakeaway/pie-tag'; import { createStory } from '../utilities'; type ToastProviderStoryMeta = Meta; -const defaultArgs: ToastProviderProps = {}; +const onQueueUpdate = (event: CustomEvent) => { + action('pie-toast-provider-queue-update')(event.detail); + + const queueLengthTag = document.querySelector('#queue-length-tag') as HTMLElement; + if (queueLengthTag) { + queueLengthTag.textContent = `Toast Queue Length: ${event.detail.length}`; + } +}; +const defaultArgs: ToastProviderProps = { + ...defaultProps, + options: { + duration: 3000, + isDismissible: true, + onPieToastOpen: action('onPieToastOpen'), + onPieToastClose: action('onPieToastClose'), + onPieToastLeadingActionClick: action('onPieToastLeadingActionClick'), + }, +}; const toastProviderStoryMeta: ToastProviderStoryMeta = { title: 'Toast Provider', component: 'pie-toast-provider', - argTypes: {}, + argTypes: { + options: { + description: 'Global options to configure default toast behavior.', + control: 'object', + defaultValue: { + summary: defaultProps.options, + }, + }, + }, args: defaultArgs, parameters: { design: { @@ -25,10 +53,70 @@ const toastProviderStoryMeta: ToastProviderStoryMeta = { export default toastProviderStoryMeta; -// TODO: remove the eslint-disable rule when props are added -// eslint-disable-next-line no-empty-pattern -const Template = ({}: ToastProviderProps) => html` - +const Template = ({ options }: ToastProviderProps) => html` + + + + + Toast Queue Length: 0 + + +
+ { + toaster.create({ + message: 'Low Priority Info Toast', + variant: 'info', + duration: null, + leadingAction: { + text: 'Confirm', + }, + }); +}}> + Trigger Info Toast (Low Priority) + + + { + toaster.create({ + message: 'Medium Priority Warning Toast', + variant: 'warning', + }); +}}> + Trigger Warning Toast (Medium Priority) + + + { + toaster.create({ + message: 'High Priority Error Toast', + variant: 'error', + }); +}}> + Trigger Error Toast (High Priority) + + + { + toaster.create({ + message: 'Actionable Info Toast', + variant: 'info', + leadingAction: { text: 'Retry' }, + }); +}}> + Trigger Actionable Info Toast + + + { + toaster.clearAll(); +}}> + Clear All Toasts + +
`; export const Default = createStory(Template, defaultArgs)(); diff --git a/packages/components/pie-toast-provider/package.json b/packages/components/pie-toast-provider/package.json index b19ba8a56b..b1a28326dd 100644 --- a/packages/components/pie-toast-provider/package.json +++ b/packages/components/pie-toast-provider/package.json @@ -41,6 +41,7 @@ "cem-plugin-module-file-extensions": "0.0.5" }, "dependencies": { + "@justeattakeaway/pie-toast": "0.5.0", "@justeattakeaway/pie-webc-core": "0.24.2" }, "volta": { diff --git a/packages/components/pie-toast-provider/src/defs.ts b/packages/components/pie-toast-provider/src/defs.ts index e9a5c00337..5989db056d 100644 --- a/packages/components/pie-toast-provider/src/defs.ts +++ b/packages/components/pie-toast-provider/src/defs.ts @@ -1,3 +1,50 @@ -// TODO - please remove the eslint disable comment below when you add props to this interface -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ToastProviderProps {} +import { type ToastProps } from '@justeattakeaway/pie-toast'; + +export const PRIORITY_ORDER: { [x: string]: number } = { + 'error-actionable': 1, + error: 2, + 'warning-actionable': 3, + 'positive-actionable': 4, + 'info-actionable': 5, + 'neutral-actionable': 6, + warning: 7, + positive: 8, + info: 9, + neutral: 10, +}; + +export interface ExtendedToastProps extends ToastProps { + /** + * Callback for when the toast is closed. + */ + onPieToastClose?: () => void; + + /** + * Callback for when the toast is opened. + */ + onPieToastOpen?: () => void; + + /** + * Callback for when the leading action is clicked. + */ + onPieToastLeadingActionClick?: (event: Event) => void; +} + +export interface ToastProviderProps { + /** + * Default options for all toasts. + */ + options?: Partial; +} + +export const defaultProps: ToastProviderProps = { + options: {}, +}; + +/** + * Event name for when the chip is closed. + * + * @constant + */ + +export const ON_TOAST_PROVIDER_QUEUE_UPDATE_EVENT = 'pie-toast-provider-queue-update'; diff --git a/packages/components/pie-toast-provider/src/index.ts b/packages/components/pie-toast-provider/src/index.ts index b9bc7e79fa..00d1276d82 100644 --- a/packages/components/pie-toast-provider/src/index.ts +++ b/packages/components/pie-toast-provider/src/index.ts @@ -1,8 +1,25 @@ -import { LitElement, html, unsafeCSS } from 'lit'; -import { RtlMixin, defineCustomElement } from '@justeattakeaway/pie-webc-core'; - +import { + LitElement, + html, + nothing, + unsafeCSS, + type PropertyValues, +} from 'lit'; +import { state, property } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { + RtlMixin, + defineCustomElement, + dispatchCustomEvent, +} from '@justeattakeaway/pie-webc-core'; import styles from './toast-provider.scss?inline'; -import { type ToastProviderProps } from './defs'; +import { + defaultProps, + PRIORITY_ORDER, + type ToastProviderProps, + type ExtendedToastProps, + ON_TOAST_PROVIDER_QUEUE_UPDATE_EVENT, +} from './defs'; // Valid values available to consumers export * from './defs'; @@ -11,20 +28,154 @@ const componentSelector = 'pie-toast-provider'; /** * @tagname pie-toast-provider + * @event {CustomEvent} pie-toast-provider-queue-update - when a toast is added or removed from the queue. */ export class PieToastProvider extends RtlMixin(LitElement) implements ToastProviderProps { - render () { - return html`

Hello world!

`; - } + @state() + private _toasts: ExtendedToastProps[] = []; + + @state() + private _currentToast: ExtendedToastProps | null = null; + + @property({ type: Object }) + public options = defaultProps.options; + + updated (changedProperties: PropertyValues): void { + if (changedProperties.has('_toasts' as keyof PieToastProvider)) { + this._dispatchQueueUpdateEvent(); + } + } + + private _dispatchQueueUpdateEvent () : void { + dispatchCustomEvent( + this, ON_TOAST_PROVIDER_QUEUE_UPDATE_EVENT, + this._toasts, + ); + } + + /** + * Get the priority for a toast. + * @param {string} type - The variant type of the toast. + * @param {boolean} hasAction - Whether the toast has an action. + * @returns {number} - The priority based on the variant and action. + */ + private getPriority (type: ExtendedToastProps['variant'], hasAction: boolean): number { + const key = `${type}${hasAction ? '-actionable' : ''}`; + return PRIORITY_ORDER[key]; + } + + /** + * Handles the dismissal of the current toast and displays the next one in the queue (if any). + */ + private _dismissToast () { + this._currentToast?.onPieToastClose?.(); + this._currentToast = null; + requestAnimationFrame(() => { this._showNextToast(); }); + } + + /** + * Displays the next toast in the queue, if available. + */ + private _showNextToast () { + if (this._toasts.length > 0) { + const [nextToast, ...remainingToasts] = this._toasts; + this._currentToast = nextToast; + this._toasts = remainingToasts; + } else { + this._currentToast = null; + } + } + + /** + * Adds a new toast to the queue and triggers its display if no toast is currently active. + * @param {ToastProps} toast - The toast props to display. + */ + public createToast (toast: ExtendedToastProps) { + const newToast = { ...this.options, ...toast }; + + this._toasts = [...this._toasts, newToast].sort((a, b) => { + const priorityB = this.getPriority(b.variant, !!b.leadingAction?.text); + const priorityA = this.getPriority(a.variant, !!a.leadingAction?.text); + + return priorityA - priorityB; + }); + + if (!this._currentToast) { + this._showNextToast(); + } + } - // Renders a `CSSResult` generated from SCSS by Vite - static styles = unsafeCSS(styles); + /** + * + * Clears all toasts from the queue and dismisses the currently visible toast. + */ + public clearToasts () { + this._toasts = []; + this._currentToast = null; + } + + render () { + const { _currentToast, _dismissToast } = this; + + return html` +
+ ${_currentToast + ? html` + + + ` + : nothing} +
+ `; + } + + // Renders a `CSSResult` generated from SCSS by Vite + static styles = unsafeCSS(styles); } defineCustomElement(componentSelector, PieToastProvider); declare global { - interface HTMLElementTagNameMap { - [componentSelector]: PieToastProvider; - } + interface HTMLElementTagNameMap { + [componentSelector]: PieToastProvider; + } } + +/** + * Singleton toaster interface for global access. + */ +export const toaster = { + _getToastProvider (): PieToastProvider | null { + const toastProvider = document.querySelector(componentSelector) as PieToastProvider; + + if (!toastProvider) { + console.error('ToastProvider is not initialized.'); + return null; + } + + return toastProvider; + }, + create (toast: ExtendedToastProps) { + const toastProvider = this._getToastProvider(); + if (!toastProvider) return; + + toastProvider.createToast(toast); + }, + clearAll () { + const toastProvider = this._getToastProvider(); + if (!toastProvider) return; + + toastProvider.clearToasts(); + }, +}; + diff --git a/packages/components/pie-toast-provider/src/toast-provider.scss b/packages/components/pie-toast-provider/src/toast-provider.scss index 6ffaedad64..fa5c53d152 100644 --- a/packages/components/pie-toast-provider/src/toast-provider.scss +++ b/packages/components/pie-toast-provider/src/toast-provider.scss @@ -1 +1,15 @@ @use '@justeattakeaway/pie-css/scss' as p; +@use '@justeattakeaway/pie-css/scss/settings' as *; + + +.c-toast-provider { + --toast-provider-offset: var(--dt-spacing-d); + + position: absolute; + inset-inline-start: var(--toast-provider-offset); + inset-block-end: var(--toast-provider-offset); + + @include media('>md') { + --toast-offset: var(--dt-spacing-e); + } +} \ No newline at end of file diff --git a/packages/components/pie-toast/src/toast.scss b/packages/components/pie-toast/src/toast.scss index 050e162fdc..46b3324060 100644 --- a/packages/components/pie-toast/src/toast.scss +++ b/packages/components/pie-toast/src/toast.scss @@ -1,5 +1,4 @@ @use '@justeattakeaway/pie-css/scss' as p; -@use '@justeattakeaway/pie-css/scss/settings' as *; .c-toast { --toast-border-radius: var(--dt-radius-rounded-b); @@ -8,13 +7,9 @@ --toast-font-size: #{p.font-size(--dt-font-body-s-size)}; --toast-line-height: #{p.line-height(--dt-font-body-s-line-height)}; --toast-icon-fill: var(--dt-color-content-default); - --toast-offset: var(--dt-spacing-d); --toast-translate-start: -100%; --toast-translate-end: 0; - position: absolute; - inset-inline-start: var(--toast-offset); - inset-block-end: var(--toast-offset); display: flex; flex-direction: column; justify-content: center; @@ -33,10 +28,6 @@ transition-property: all; transition-duration: var(--dt-motion-timing-100); transition-timing-function: var(--dt-motion-easing-in); - - @include media('>md') { - --toast-offset: var(--dt-spacing-e); - } } .c-toast--rtl { diff --git a/yarn.lock b/yarn.lock index ece1109852..ddd61cd514 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5169,6 +5169,7 @@ __metadata: "@custom-elements-manifest/analyzer": 0.9.0 "@justeattakeaway/pie-components-config": 0.18.0 "@justeattakeaway/pie-css": 0.13.1 + "@justeattakeaway/pie-toast": 0.5.0 "@justeattakeaway/pie-webc-core": 0.24.2 cem-plugin-module-file-extensions: 0.0.5 languageName: unknown