-
Notifications
You must be signed in to change notification settings - Fork 8
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(pie-toast-provider): DSW-2222 toast provider basic functionality #2098
base: main
Are you sure you want to change the base?
Changes from 6 commits
ff8133c
6881f24
5633655
37e17ff
6e96ada
f667474
7b0a454
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,54 @@ | ||
// 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'; | ||
|
||
import { type ComponentDefaultProps } from '@justeattakeaway/pie-webc-core'; | ||
|
||
export const PRIORITY_ORDER: { [x: string]: number } = { | ||
'error-actionable': 1, | ||
error: 2, | ||
'warning-actionable': 3, | ||
'success-actionable': 4, | ||
'info-actionable': 5, | ||
'neutral-actionable': 6, | ||
warning: 7, | ||
success: 8, | ||
info: 9, | ||
neutral: 10, | ||
}; | ||
|
||
export interface ExtendedToastProps extends ToastProps { | ||
/** | ||
* Triggered when the user interacts with the close icon or when the toast auto dismiss. | ||
*/ | ||
onPieToastClose?: () => void; | ||
|
||
/** | ||
* Triggered when the toast is opened. | ||
*/ | ||
onPieToastOpen?: () => void; | ||
|
||
/** | ||
* Triggered when the user interacts with the leading action. | ||
*/ | ||
onPieToastLeadingActionClick?: (event: Event) => void; | ||
} | ||
|
||
export interface ToastProviderProps { | ||
/** | ||
* Default options for all toasts; accepts all toast props. | ||
*/ | ||
options?: Partial<ExtendedToastProps>; | ||
} | ||
|
||
export type DefaultProps = ComponentDefaultProps<ToastProviderProps>; | ||
|
||
export const defaultProps: DefaultProps = { | ||
options: {}, | ||
}; | ||
|
||
/** | ||
* Event name for when the toast provider queue is updated. | ||
* | ||
* @constant | ||
*/ | ||
|
||
export const ON_TOAST_PROVIDER_QUEUE_UPDATE_EVENT = 'pie-toast-provider-queue-update'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,144 @@ | ||
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 '@justeattakeaway/pie-toast'; | ||
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'; | ||
export { toaster } from './toaster'; | ||
|
||
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 { | ||
@state() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens when a consumer mistakenly adds a second Toast Provider to an app? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We currently assume only one toast provider, rendered at the root level of the consumer app, and toasts are displayed in the body as specified in design specs here However, this might change as designers plan to support rendering toasts within modals (sub-containers) and this will be addressed once we receive the complete design requirements and will probably require the usage of multiple providers either identified by ids or querying the closest provider in the DOM tree when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand that is expected to have a single provider at the moment, that's why I ask what happens if the user does a mistake. Everything still works perfectly when more than one provider is in the DOM? Should we help the user to avoid this mistake? |
||
private _toasts: ExtendedToastProps[] = []; | ||
|
||
@state() | ||
private _currentToast: ExtendedToastProps | null = null; | ||
|
||
@property({ type: Object }) | ||
public options = defaultProps.options; | ||
|
||
updated (changedProperties: PropertyValues<this>): 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(); | ||
} | ||
} | ||
|
||
/** | ||
* | ||
* Clears all toasts from the queue and dismisses the currently visible toast. | ||
*/ | ||
public clearToasts () { | ||
this._toasts = []; | ||
this._currentToast = null; | ||
} | ||
|
||
render () { | ||
return html`<h1 data-test-id="pie-toast-provider">Hello world!</h1>`; | ||
const { _currentToast, _dismissToast } = this; | ||
|
||
return html` | ||
<div class="c-toast-provider" data-test-id="pie-toast-provider"> | ||
${_currentToast | ||
? html` | ||
<pie-toast | ||
message="${_currentToast.message}" | ||
variant="${ifDefined(_currentToast.variant)}" | ||
?isStrong="${_currentToast.isStrong}" | ||
?isDismissible="${_currentToast.isDismissible}" | ||
?isMultiline="${_currentToast.isMultiline}" | ||
.leadingAction="${_currentToast.leadingAction}" | ||
.duration="${typeof _currentToast.duration === 'undefined' ? nothing : _currentToast.duration}" | ||
@pie-toast-close="${_dismissToast}" | ||
@pie-toast-open="${_currentToast.onPieToastOpen}" | ||
@pie-toast-leading-action-click="${_currentToast.onPieToastLeadingActionClick}"> | ||
</pie-toast> | ||
` | ||
: nothing} | ||
</div> | ||
raoufswe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
`; | ||
} | ||
|
||
// Renders a `CSSResult` generated from SCSS by Vite | ||
|
@@ -28,3 +152,4 @@ declare global { | |
[componentSelector]: PieToastProvider; | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be ok to move the indentation of these buttons click handlers content?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's auto-formatted on save for me 🙄 are you getting the same thing locally?
Screen.Recording.2024-12-02.at.13.06.43.mov