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): Introduce Toast JavaScript plugin #DS-1115 #1305

Merged
merged 2 commits into from
Mar 12, 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
34 changes: 17 additions & 17 deletions packages/web/src/js/Collapse.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import BaseComponent from './BaseComponent';
import {
ARIA_CONTROLS_ATTRIBUTE,
ARIA_EXPANDED_ATTRIBUTE,
CLASSNAME_OPEN,
CLASSNAME_TRANSITION,
NAME_DATA_TARGET,
NAME_DATA_TOGGLE,
ATTRIBUTE_ARIA_CONTROLS,
ATTRIBUTE_ARIA_EXPANDED,
ATTRIBUTE_DATA_TARGET,
ATTRIBUTE_DATA_TOGGLE,
CLASS_NAME_OPEN,
CLASS_NAME_TRANSITIONING,
} from './constants';
import EventHandler from './dom/EventHandler';
import SelectorEngine from './dom/SelectorEngine';
Expand Down Expand Up @@ -48,8 +48,8 @@ class Collapse extends BaseComponent {
};
this.state = {
open:
this.element.hasAttribute(ARIA_EXPANDED_ATTRIBUTE) &&
this.element.getAttribute(ARIA_EXPANDED_ATTRIBUTE) === 'true',
this.element.hasAttribute(ATTRIBUTE_ARIA_EXPANDED) &&
this.element.getAttribute(ATTRIBUTE_ARIA_EXPANDED) === 'true',
init: false,
};

Expand All @@ -74,7 +74,7 @@ class Collapse extends BaseComponent {
}

for (const item of children) {
const itemTrigger = SelectorEngine.findOne(`[${NAME_DATA_TOGGLE}="${NAME}"]`, item);
const itemTrigger = SelectorEngine.findOne(`[${ATTRIBUTE_DATA_TOGGLE}="${NAME}"]`, item);
const instance = Collapse.getInstance(itemTrigger);

if (instance?.state?.open && itemTrigger !== trigger) {
Expand Down Expand Up @@ -113,10 +113,10 @@ class Collapse extends BaseComponent {
}

updateTriggerElement(open: boolean = this.state.open) {
const triggers = SelectorEngine.findAll(`[${NAME_DATA_TARGET}="${this.meta.id}"]`);
const triggers = SelectorEngine.findAll(`[${ATTRIBUTE_DATA_TARGET}="${this.meta.id}"]`);
const updateElement = (element: Element | HTMLElement) => {
element.setAttribute(ARIA_CONTROLS_ATTRIBUTE, this.meta.id);
element.setAttribute(ARIA_EXPANDED_ATTRIBUTE, String(open));
element.setAttribute(ATTRIBUTE_ARIA_CONTROLS, this.meta.id);
element.setAttribute(ATTRIBUTE_ARIA_EXPANDED, String(open));
if (this.meta.hideOnCollapse && open) {
element.remove();
this.appendNodeToParent();
Expand All @@ -140,11 +140,11 @@ class Collapse extends BaseComponent {
}
this.adjustCollapsibleElementHeight(open);
if (this.state.init) {
this.target?.classList.add(CLASSNAME_TRANSITION);
this.target?.classList.add(CLASS_NAME_TRANSITIONING);
}
executeAfterTransition(this.target, () => {
this.target?.classList.remove(CLASSNAME_TRANSITION);
this.target?.classList.toggle(CLASSNAME_OPEN, open);
this.target?.classList.remove(CLASS_NAME_TRANSITIONING);
this.target?.classList.toggle(CLASS_NAME_OPEN, open);
if (open) {
this.target?.setAttribute('style', 'height: 100%');
} else {
Expand Down Expand Up @@ -188,7 +188,7 @@ class Collapse extends BaseComponent {
}

initEvents() {
const triggers = SelectorEngine.findAll(`[${NAME_DATA_TARGET}="${this.meta.id}"]`);
const triggers = SelectorEngine.findAll(`[${ATTRIBUTE_DATA_TARGET}="${this.meta.id}"]`);
if (triggers.length === 1) {
EventHandler.on(this.element, 'click', () => this.toggle());
} else {
Expand All @@ -197,7 +197,7 @@ class Collapse extends BaseComponent {
}

destroyEvents() {
const triggers = SelectorEngine.findAll(`[${NAME_DATA_TARGET}="${this.meta.id}"]`);
const triggers = SelectorEngine.findAll(`[${ATTRIBUTE_DATA_TARGET}="${this.meta.id}"]`);
if (triggers.length === 1) {
EventHandler.off(this.element, 'click', () => this.toggle());
} else {
Expand Down
103 changes: 103 additions & 0 deletions packages/web/src/js/Toast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import BaseComponent from './BaseComponent';
import {
ATTRIBUTE_ARIA_EXPANDED,
ATTRIBUTE_DATA_TARGET,
CLASS_NAME_HIDDEN,
CLASS_NAME_OPEN,
CLASS_NAME_TRANSITIONING,
} from './constants';
import { enableDismissTrigger, enableToggleTrigger, executeAfterTransition, SpiritConfig } from './utils';
import { EventHandler, SelectorEngine } from './dom';

const NAME = 'toast';
const DATA_KEY = `${NAME}`;
const EVENT_KEY = `.${DATA_KEY}`;

const EVENT_HIDE = `hide${EVENT_KEY}`;
const EVENT_HIDDEN = `hidden${EVENT_KEY}`;
const EVENT_SHOW = `show${EVENT_KEY}`;
const EVENT_SHOWN = `shown${EVENT_KEY}`;

class Toast extends BaseComponent {
isShown: boolean;
triggers: HTMLElement[];

static get NAME() {
return NAME;
}

constructor(element: SpiritElement, config?: SpiritConfig) {
super(element, config);

this.triggers = this.getTriggers();

this.isShown = this.checkShownState();
}

checkShownState() {
return this.element.classList.contains(CLASS_NAME_OPEN) || !this.element.classList.contains(CLASS_NAME_HIDDEN);
}

getTriggers() {
const id = this.element && (this.element.getAttribute('id') as string);

return SelectorEngine.findAll(`[${ATTRIBUTE_DATA_TARGET}="#${id}"]`);
}

show() {
if (this.isShown) {
return;
}

const showEvent = EventHandler.trigger(this.element, Toast.eventName(EVENT_SHOW)) as Event;
if (showEvent.defaultPrevented) {
return;
}

this.triggers.forEach((element) => {
element?.setAttribute(ATTRIBUTE_ARIA_EXPANDED, 'true');
});

this.element.classList.remove(CLASS_NAME_HIDDEN);
this.element.classList.add(CLASS_NAME_OPEN);
this.element.classList.add(CLASS_NAME_TRANSITIONING);

executeAfterTransition(this.element, () => {
EventHandler.trigger(this.element, Toast.eventName(EVENT_SHOWN));
this.element.classList.remove(CLASS_NAME_TRANSITIONING);
});

this.isShown = true;
}

hide() {
if (!this.isShown) {
return;
}

const hideEvent = EventHandler.trigger(this.element, Toast.eventName(EVENT_HIDE)) as Event;
if (hideEvent.defaultPrevented) {
return;
}

this.triggers.forEach((element) => {
element?.setAttribute(ATTRIBUTE_ARIA_EXPANDED, 'false');
});

this.element.classList.remove(CLASS_NAME_OPEN);
this.element.classList.add(CLASS_NAME_HIDDEN);
this.element.classList.add(CLASS_NAME_TRANSITIONING);

executeAfterTransition(this.element, () => {
EventHandler.trigger(this.element, Toast.eventName(EVENT_HIDDEN));
this.element.remove();
});

this.isShown = false;
}
}

enableToggleTrigger(Toast, 'show', 'target');
enableDismissTrigger(Toast, 'hide', 'target');

export default Toast;
12 changes: 6 additions & 6 deletions packages/web/src/js/__tests__/Collapse.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { clearFixture, getFixture } from '../../../tests/helpers/fixture';
import EventHandler from '../dom/EventHandler';
import Collapse from '../Collapse';
import { CLASSNAME_OPEN, CLASSNAME_TRANSITION } from '../constants';
import { CLASS_NAME_OPEN, CLASS_NAME_TRANSITIONING } from '../constants';

describe('Collapse', () => {
let fixtureEl: Element;
Expand Down Expand Up @@ -75,10 +75,10 @@ describe('Collapse', () => {
await collapse.show();

expect(element.getAttribute('aria-expanded')).toBe('true');
expect(target).toHaveClass(CLASSNAME_TRANSITION);
expect(target).toHaveClass(CLASS_NAME_TRANSITIONING);

EventHandler.trigger(target, 'transitionend');
expect(target).toHaveClass(CLASSNAME_OPEN);
expect(target).toHaveClass(CLASS_NAME_OPEN);
});
});

Expand Down Expand Up @@ -197,16 +197,16 @@ describe('Collapse', () => {
const collapse0 = new Collapse(element0);

expect(target0).toHaveClass('Collapse');
expect(target1).toHaveClass(CLASSNAME_OPEN);
expect(target1).toHaveClass(CLASS_NAME_OPEN);

await collapse0.show();

expect(target0).toHaveClass(CLASSNAME_TRANSITION);
expect(target0).toHaveClass(CLASS_NAME_TRANSITIONING);

EventHandler.trigger(target0, 'transitionend');
EventHandler.trigger(target1, 'transitionend');

expect(target0).toHaveClass(CLASSNAME_OPEN);
expect(target0).toHaveClass(CLASS_NAME_OPEN);
expect(target1).toHaveClass('Collapse');
});
});
Expand Down
112 changes: 112 additions & 0 deletions packages/web/src/js/__tests__/Toast.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { clearFixture, getFixture } from '../../../tests/helpers/fixture';
import Toast from '../Toast';
import {
ATTRIBUTE_ARIA_EXPANDED,
ATTRIBUTE_DATA_TARGET,
CLASS_NAME_HIDDEN,
CLASS_NAME_OPEN,
CLASS_NAME_TRANSITIONING,
} from '../constants';
import EventHandler from '../dom/EventHandler';

const testId = 'toast-test';

const toastHtmlClosed = `
<div id="${testId}" class="ToastBar ToastBar--success ToastBar--dismissible is-hidden">
<button type="button" data-spirit-target="#${testId}" aria-expanded="false">Close</button>
</div>
`;

const toastHtmlOpened = `
<div id="${testId}" class="ToastBar ToastBar--success ToastBar--dismissible is-open">
<button type="button" data-spirit-target="#${testId}" aria-expanded="true">Close</button>
</div>
`;

describe('Toast', () => {
let fixtureEl: Element;

beforeAll(() => {
fixtureEl = getFixture();
});

afterEach(() => {
clearFixture();
});

describe('NAME', () => {
it('should return plugin name', () => {
expect(Toast.NAME).toEqual(expect.any(String));
});
});

describe('DATA_KEY', () => {
it('should return plugin data key', () => {
expect(Toast.DATA_KEY).toBe('toast');
});
});

describe('EVENT_KEY', () => {
it('should return plugin event key', () => {
expect(Toast.EVENT_KEY).toBe('.toast');
});
});

describe('constructor', () => {
it('should construct a toast', () => {
fixtureEl.innerHTML = toastHtmlClosed;
const element = fixtureEl.querySelector('.ToastBar') as HTMLElement;
const toast = new Toast(element);

expect(toast.element).toEqual(element);
});
});

describe('show', () => {
it('should show a toast', async () => {
fixtureEl.innerHTML = toastHtmlClosed;

const element = fixtureEl.querySelector('.ToastBar') as HTMLElement;
const toast = new Toast(element);
const trigger = fixtureEl.querySelector(`[${ATTRIBUTE_DATA_TARGET}="#${testId}"]`) as HTMLButtonElement;

const showSpy = jest.spyOn(Toast.prototype, 'show');

await toast.show();

expect(showSpy).toHaveBeenCalled();
expect(trigger).toHaveAttribute(ATTRIBUTE_ARIA_EXPANDED, 'true');
expect(element).toHaveClass(CLASS_NAME_OPEN);
expect(element).toHaveClass(CLASS_NAME_TRANSITIONING);

EventHandler.trigger(element, 'transitionend');

expect(element).toHaveClass(CLASS_NAME_OPEN);
expect(element).not.toHaveClass(CLASS_NAME_HIDDEN);
expect(element).not.toHaveClass(CLASS_NAME_TRANSITIONING);
});
});

describe('hide', () => {
it('should hide a toast', async () => {
fixtureEl.innerHTML = toastHtmlOpened;

const element = fixtureEl.querySelector('.ToastBar') as HTMLElement;
const toast = new Toast(element);
const trigger = fixtureEl.querySelector(`[${ATTRIBUTE_DATA_TARGET}="#${testId}"]`) as HTMLButtonElement;

const hideSpy = jest.spyOn(Toast.prototype, 'hide');

await toast.hide();

expect(hideSpy).toHaveBeenCalled();
expect(trigger).toHaveAttribute(ATTRIBUTE_ARIA_EXPANDED, 'false');
expect(element).toHaveClass(CLASS_NAME_HIDDEN);
expect(element).toHaveClass(CLASS_NAME_TRANSITIONING);

EventHandler.trigger(element, 'transitionend');

expect(fixtureEl.querySelector('.ToastBar')).toBeNull();
});
});
});
17 changes: 9 additions & 8 deletions packages/web/src/js/constants.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
export const ARIA_EXPANDED_ATTRIBUTE = 'aria-expanded';
export const ARIA_CONTROLS_ATTRIBUTE = 'aria-controls';
export const ATTRIBUTE_ARIA_EXPANDED = 'aria-expanded';
export const ATTRIBUTE_ARIA_CONTROLS = 'aria-controls';
export const ATTRIBUTE_DATA_DISMISS = 'data-spirit-dismiss';
export const ATTRIBUTE_DATA_TARGET = 'data-spirit-target';
export const ATTRIBUTE_DATA_TOGGLE = 'data-spirit-toggle';

export const NAME_DATA_TOGGLE = 'data-spirit-toggle';
export const NAME_DATA_TARGET = 'data-spirit-target';

export const CLASSNAME_EXPANDED = 'is-expanded';
export const CLASSNAME_OPEN = 'is-open';
export const CLASSNAME_TRANSITION = 'is-transitioning';
export const CLASS_NAME_HIDDEN = 'is-hidden';
export const CLASS_NAME_OPEN = 'is-open';
export const CLASS_NAME_TRANSITIONING = 'is-transitioning';
export const CLASS_NAME_VISIBLE = 'is-visible';
1 change: 1 addition & 0 deletions packages/web/src/js/index.esm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { default as Offcanvas } from './Offcanvas';
export { default as Password } from './Password';
export { default as ScrollView } from './ScrollView';
export { default as Tabs } from './Tabs';
export { default as Toast } from './Toast';
export { default as Tooltip } from './Tooltip';
export * from './constants';
export * from './dom';
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/js/index.umd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Offcanvas from './Offcanvas';
import Password from './Password';
import ScrollView from './ScrollView';
import Tabs from './Tabs';
import Toast from './Toast';
import Tooltip from './Tooltip';
import * as constants from './constants';
import * as dom from './dom';
Expand All @@ -24,6 +25,7 @@ export default {
Password,
ScrollView,
Tabs,
Toast,
Tooltip,
constants,
dom,
Expand Down
Loading
Loading