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(pie-toast): DSW-2204 implement auto-dismiss / persistent feature #1862

Merged
merged 26 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c990841
feat(pie-toast): DSW-2204 updated lockfile
thejfreitas Aug 13, 2024
3840581
feat(pie-toast): DSW-2204 fixed wrong description
thejfreitas Aug 13, 2024
c19a159
feat(pie-toast): DSW-2204 added variants documentation in storybook
thejfreitas Aug 13, 2024
f76a0e9
feat(pie-toast): DSW-2204 implemented auto-dismiss logic
thejfreitas Aug 13, 2024
8c46289
feat(pie-toast): DSW-2204 implemented animations
thejfreitas Sep 11, 2024
03f2609
feat(pie-toast): DSW-2204 adjusted visual tests
thejfreitas Sep 11, 2024
9f0c5ed
Merge branch 'main' into dsw-2204-pie-toast-auto-dismiss-persistent-feat
thejfreitas Sep 13, 2024
fe4984d
feat(pie-toast): DSW-2204 avoid toast to dismiss if user focus on but…
thejfreitas Sep 13, 2024
2aecbc3
feat(pie-toast): DSW-2204 updated lockfile
thejfreitas Sep 13, 2024
cefd7a0
feat(pie-toast): DSW-2204 lint styles
thejfreitas Sep 13, 2024
7ad08d9
feat(pie-toast): DSW-2204 added changeset
thejfreitas Sep 13, 2024
196d0ea
Merge branch 'main' into dsw-2204-pie-toast-auto-dismiss-persistent-feat
thejfreitas Sep 20, 2024
b88f95e
feat(pie-toast): DSW-2204 addressed typos
thejfreitas Sep 20, 2024
2965081
feat(pie-toast): DSW-2204 replaced hardcoded animations in favor of t…
thejfreitas Sep 20, 2024
95b48c3
feat(pie-toast): DSW-2204 adapted tests to behave better with animations
thejfreitas Sep 23, 2024
b3b47d9
feat(pie-toast): DSW-2204 used defaultDuration into defaultProps
thejfreitas Sep 23, 2024
79ef477
Merge branch 'main' into dsw-2204-pie-toast-auto-dismiss-persistent-feat
thejfreitas Sep 23, 2024
1d2d9b7
Merge branch 'main' into dsw-2204-pie-toast-auto-dismiss-persistent-feat
thejfreitas Sep 24, 2024
787c4b3
Merge branch 'main' into dsw-2204-pie-toast-auto-dismiss-persistent-feat
thejfreitas Sep 27, 2024
cf69f20
feat(pie-toast): DSW-2204 fixed typo and css variables
thejfreitas Sep 27, 2024
0eab87b
feat(pie-toast): DSW-2204 fixed typo
thejfreitas Sep 27, 2024
89a3e97
feat(pie-toast): DSW-2204 making isOpen reactive to storybook
thejfreitas Oct 1, 2024
0b0378f
feat(pie-toast): DSW-2204 remove never type from setAutoDismiss
thejfreitas Oct 1, 2024
f960cbf
Merge branch 'main' into dsw-2204-pie-toast-auto-dismiss-persistent-feat
thejfreitas Oct 1, 2024
5e32574
Merge branch 'main' into dsw-2204-pie-toast-auto-dismiss-persistent-feat
thejfreitas Oct 2, 2024
d1c248b
feat(pie-toast): DSW-2204 added comments in updated lifecycle method …
thejfreitas Oct 2, 2024
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
44 changes: 44 additions & 0 deletions apps/pie-storybook/stories/pie-toast-docs/variants.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as ToastStories from '../pie-toast.stories.ts';
import { ComponentStatus } from '../../docblocks/component-status.jsx';

<ComponentStatus component="pie-toast" />

<Meta of={ToastStories} />

### Neutral

<Canvas of={ToastStories.Neutral} />

### Info

<Canvas of={ToastStories.Info} />

### Info Strong

<Canvas of={ToastStories.InfoStrong} />

### Warning

<Canvas of={ToastStories.Warning} />

### Warning Strong

<Canvas of={ToastStories.WarningStrong} />

### Success

<Canvas of={ToastStories.Success} />

### Success Strong

<Canvas of={ToastStories.SuccessStrong} />

### Error

<Canvas of={ToastStories.Error} />

### Error Strong

<Canvas of={ToastStories.ErrorStrong} />

30 changes: 22 additions & 8 deletions apps/pie-storybook/stories/pie-toast.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { html } from 'lit';
import { ToastProps, defaultProps, variants } from '@justeattakeaway/pie-toast';
import { action } from '@storybook/addon-actions';
import { type StoryMeta } from '../types';
import { createStory } from '../utilities';
import { createStory, TemplateFunction } from '../utilities';
thejfreitas marked this conversation as resolved.
Show resolved Hide resolved

type ToastStoryMeta = StoryMeta<ToastProps>;

Expand All @@ -13,6 +13,7 @@ const defaultArgs: ToastProps = {
text: 'Confirm',
ariaLabel: 'Descriptive confirmation text',
},
duration: null,
};

const toastStoryMeta: ToastStoryMeta = {
Expand All @@ -27,7 +28,7 @@ const toastStoryMeta: ToastStoryMeta = {
},
},
variant: {
description: 'Set the variant of the notification.',
description: 'Set the variant of the toast.',
control: 'select',
options: variants,
defaultValue: {
Expand Down Expand Up @@ -63,6 +64,10 @@ const toastStoryMeta: ToastStoryMeta = {
description: 'The leading action configuration for the toast.',
control: 'object',
},
duration: {
description: 'It set the duration of the toast in milliseconds before it auto-dismiss.',
thejfreitas marked this conversation as resolved.
Show resolved Hide resolved
control: 'number',
},
},
args: defaultArgs,
parameters: {
Expand All @@ -76,34 +81,43 @@ const toastStoryMeta: ToastStoryMeta = {
export default toastStoryMeta;

const pieToastLeadingActionClick = action('pie-toast-leading-action-click');
const pieToastSupportingActionClick = action('pie-toast-supporting-action-click');
const pieToastClose = action('pie-toast-close');
const pieToastOpen = action('pie-toast-open');

// TODO: remove the eslint-disable rule when props are added
// eslint-disable-next-line no-empty-pattern
const Template = ({
const Template : TemplateFunction<ToastProps> = ({
isOpen,
isDismissible,
message,
leadingAction,
isMultiline,
isStrong,
variant,
duration,
}: ToastProps) => html`
<pie-toast
?isOpen="${isOpen}"
?isDismissible="${isDismissible}"
?isStrong="${isStrong}"
variant="${variant}"
message="${message}"
.duration="${duration}"
thejfreitas marked this conversation as resolved.
Show resolved Hide resolved
?isMultiline="${isMultiline}"
.leadingAction="${leadingAction}"
@pie-toast-leading-action-click="${pieToastLeadingActionClick}"
@pie-toast-supporting-action-click="${pieToastSupportingActionClick}"
@pie-toast-close="${pieToastClose}"
@pie-toast-open="${pieToastOpen}"
/></pie-toast>
`;

export const Default = createStory<ToastProps>(Template, defaultArgs)();
const createToastStory = createStory<ToastProps>(Template, defaultArgs);

export const Neutral = createToastStory();
export const Info = createToastStory({ variant: 'info' });
export const InfoStrong = createToastStory({ variant: 'info', isStrong: true });
export const Warning = createToastStory({ variant: 'warning' });
export const WarningStrong = createToastStory({ variant: 'warning', isStrong: true });
export const Success = createToastStory({ variant: 'success' });
export const SuccessStrong = createToastStory({ variant: 'success', isStrong: true });
export const Error = createToastStory({ variant: 'error' });
export const ErrorStrong = createToastStory({ variant: 'error', isStrong: true });
export const AutoDismiss = createToastStory({ duration: 3000, message: 'Closing in three seconds' });
11 changes: 10 additions & 1 deletion packages/components/pie-toast/src/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { type ComponentDefaultProps } from '@justeattakeaway/pie-webc-core';

export const variants = ['neutral', 'info', 'warning', 'success', 'error'] as const;

export const defaultDuration = 5000;
thejfreitas marked this conversation as resolved.
Show resolved Hide resolved

export type ActionProps = {
/**
* The text to display inside the button.
Expand Down Expand Up @@ -44,6 +46,13 @@ export interface ToastProps {
* The leading action for the toast.
*/
leadingAction?: ActionProps;

/**
* It set the duration of the toast in milliseconds before it auto-dismiss
thejfreitas marked this conversation as resolved.
Show resolved Hide resolved
* If the value is null auto-dismiss is disabled
* If the value is not provided it auto-dismiss after 5 seconds (5000 milliseconds)
thejfreitas marked this conversation as resolved.
Show resolved Hide resolved
*/
duration?: number | null;
thejfreitas marked this conversation as resolved.
Show resolved Hide resolved
}

export const componentSelector = 'pie-toast';
Expand All @@ -70,7 +79,7 @@ export const ON_TOAST_OPEN_EVENT = `${componentSelector}-open`;
*/
export const ON_TOAST_LEADING_ACTION_CLICK_EVENT = `${componentSelector}-leading-action-click`;

export type DefaultProps = ComponentDefaultProps<ToastProps, keyof Omit<ToastProps, 'message' | 'leadingAction'>>;
export type DefaultProps = ComponentDefaultProps<ToastProps, keyof Omit<ToastProps, 'message' | 'leadingAction' | 'duration'>>;

export const defaultProps: DefaultProps = {
isOpen: true,
Expand Down
123 changes: 117 additions & 6 deletions packages/components/pie-toast/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
ON_TOAST_LEADING_ACTION_CLICK_EVENT,
defaultProps,
variants,
defaultDuration,
} from './defs';

// Valid values available to consumers
Expand Down Expand Up @@ -64,15 +65,67 @@ export class PieToast extends RtlMixin(LitElement) implements ToastProps {
@property({ type: Object })
public leadingAction: ToastProps['leadingAction'];

@property()
public duration: ToastProps['duration'];

@query('pie-button') actionButton?: HTMLElement;

@query('pie-icon-button') closeButton?: HTMLElement;

private _actionButtonOffset = 0;

private _messageAreaMaxWidth = 0;

private _timeoutId: NodeJS.Timeout | null = null;

private _abortController: AbortController | null = null;

// Renders a `CSSResult` generated from SCSS by Vite
static styles = unsafeCSS(styles);

/**
* Create a timeout function and set its id into a private attribute.
*
* @private
*/
private setAutoDismiss () {
this._timeoutId = setTimeout(() => {
this.closeToastComponent();
}, this.setAutoDismissDuration());
}

/**
* It gets the duration of the timeout in milliseconds.
* If the duration is undefined it returns 5000 which is the default value.
* If the duration is an arbitrary number provided by the user, it returns the number itself.
*
* @returns number
* @private
*/
private setAutoDismissDuration (): number {
switch (typeof this.duration) {
case 'undefined':
return defaultDuration;
case 'number':
return this.duration as number;
default:
return 0 as never;
}
}

/**
* If the _abortController is set, it aborts all event
* listeners in this controller and the controller turns into null.
*
* @private
*/
private abortAndCleanEventListeners () {
if (this._abortController) {
this._abortController.abort();
this._abortController = null;
thejfreitas marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
* Calculates and returns the width of the message based on the toast properties.
*
Expand Down Expand Up @@ -111,6 +164,52 @@ export class PieToast extends RtlMixin(LitElement) implements ToastProps {
return toastMaxWidthWithoutPadding - offset;
}

/**
* Adds event listeners to the specified element for handling the auto dismiss behavior.
*
* @param {typeof this | HTMLElement | undefined} element - The element to which the listeners will be added. It can be the current instance, an HTMLElement, or undefined.
* @param {keyof HTMLElementEventMap} inEvent - The event type to listen for when entering the element. (e.g., 'mouseenter', 'focusin').
* @param {keyof HTMLElementEventMap} outEvent - The event type to listen for when leaving the element. (e.g., 'mouseleave', 'focusout').
* @param {AddEventListenerOptions['signal']} abortSignal - An AbortSignal that can be used to remove the event listeners.
*
* @private
*/
private addListenersToElement (
element: typeof this | HTMLElement | undefined,
inEvent: keyof HTMLElementEventMap,
outEvent: keyof HTMLElementEventMap,
abortSignal: AddEventListenerOptions['signal'],
) {
if (element) {
element.addEventListener(inEvent, () => {
if (this._timeoutId) {
clearTimeout(this._timeoutId);
}
}, { signal: abortSignal });
element.addEventListener(outEvent, () => {
this.setAutoDismiss();
}, { signal: abortSignal });
}
}

/**
* It created all event listeners to handle the auto-dismiss capability
thejfreitas marked this conversation as resolved.
Show resolved Hide resolved
* as well the controller responsible to remove the events when needed.
*
* @private
*/
private createAutoDismissEventListeners () {
this._abortController = new AbortController();

this.setAutoDismiss();

const { signal } = this._abortController;

this.addListenersToElement(this.actionButton, 'focus', 'focusout', signal);
this.addListenersToElement(this.closeButton, 'focus', 'focusout', signal);
this.addListenersToElement(this, 'mouseover', 'mouseout', signal);
}

/**
* Lifecycle method executed when component is updated.
* It dispatches an event if toast is opened.
Expand All @@ -119,6 +218,14 @@ export class PieToast extends RtlMixin(LitElement) implements ToastProps {
protected async updated (_changedProperties: PropertyValues<this>) {
if (_changedProperties.has('isOpen') && this.isOpen) {
dispatchCustomEvent(this, ON_TOAST_OPEN_EVENT, { targetNotification: this });

if (this.duration !== null) {
this.createAutoDismissEventListeners();
}
thejfreitas marked this conversation as resolved.
Show resolved Hide resolved
}

if (_changedProperties.has('isOpen') && !this.isOpen && this._abortController) {
thejfreitas marked this conversation as resolved.
Show resolved Hide resolved
this.abortAndCleanEventListeners();
}

await this.updateComplete;
Expand All @@ -137,7 +244,8 @@ export class PieToast extends RtlMixin(LitElement) implements ToastProps {
_changedProperties.has('message') ||
_changedProperties.has('isDismissible') ||
_changedProperties.has('isMultiline') ||
_changedProperties.has('leadingAction')) {
_changedProperties.has('leadingAction') ||
_changedProperties.has('duration')) {
thejfreitas marked this conversation as resolved.
Show resolved Hide resolved
this.requestUpdate();
}
}
Expand Down Expand Up @@ -234,6 +342,7 @@ export class PieToast extends RtlMixin(LitElement) implements ToastProps {
private closeToastComponent () {
this.isOpen = false;
dispatchCustomEvent(this, ON_TOAST_CLOSE_EVENT, { targetNotification: this });
this.abortAndCleanEventListeners();
}

/**
Expand Down Expand Up @@ -288,14 +397,12 @@ export class PieToast extends RtlMixin(LitElement) implements ToastProps {
_messageAreaMaxWidth,
} = this;

if (!isOpen) {
return nothing;
}

const componentWrapperClasses = {
[componentClass]: true,
[`${componentClass}--${variant}`]: true,
[`${componentClass}--strong`]: isStrong,
[`${componentClass}--animate-in`]: isOpen,
[`${componentClass}--animate-out`]: !isOpen,
};

const messageAreaClasses = {
Expand All @@ -304,7 +411,11 @@ export class PieToast extends RtlMixin(LitElement) implements ToastProps {
};

return html`
<div data-test-id="${componentSelector}" class="${classMap(componentWrapperClasses)}">
<div
data-test-id="${componentSelector}"
class="${classMap(componentWrapperClasses)}"
aria-live="${variant === 'error' ? 'assertive' : 'polite'}"
>
<div class="${componentClass}-contentArea">
<div class="${classMap(messageAreaClasses)}">
${this.variantHasIcon(variant) ? this.getVariantIcon() : nothing}
Expand Down
Loading
Loading