Skip to content

Commit

Permalink
Feat(web): Introduce Toast JavaScript plugin #DS-1115
Browse files Browse the repository at this point in the history
  • Loading branch information
adamkudrna authored and pavelklibani committed Mar 11, 2024
1 parent 2e394dc commit b44a384
Show file tree
Hide file tree
Showing 10 changed files with 271 additions and 34 deletions.
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
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
72 changes: 70 additions & 2 deletions packages/web/src/scss/components/Toast/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ Toast is a composition of a few subcomponents:
- [Toast](#toast)
- [ToastBar](#toastbar)

## JavaScript Plugin

For full functionality, you need to provide Spirit JavaScript, which will handle toggling of the Toast component:

```html
<script src="node_modules/@lmc-eu/spirit-web/js/cjs/spirit-web.min.js" async></script>
```

You will find the [full documentation](#javascript-plugin-api) of the plugin below on this page.

Please consult the [main README][web-readme] for how to include JavaScript plugins.

Or, feel free to write the controlling script yourself.

## Toast

The Toast component is a container responsible for positioning the [ToastBar](#toastbar) component. It is capable of
Expand Down Expand Up @@ -187,6 +201,25 @@ For example:
</div>
```

### Opening the ToastBar

Use our JavaScript plugin to open a Toast **that is present in the DOM,** e.g.:

```html
<button
type="button"
class="Button Button--primary Button--medium"
data-spirit-toggle="toast"
data-spirit-target="#toast-example"
aria-controls="toast-example"
aria-expanded="false"
>
Open Modal
</button>
```

👉 Advanced toast queue control is not yet implemented.

### Dismissible ToastBar

To make the ToastBar dismissible, add the `ToastBar--dismissible` modifier class, a unique `id` attribute, and a close
Expand All @@ -201,6 +234,7 @@ button:
type="button"
class="Button Button--small Button--square Button--inverted"
data-spirit-dismiss="toast"
data-spirit-target="#my-dismissible-toast"
aria-controls="my-dismissible-toast"
aria-expanded="true"
>
Expand All @@ -214,8 +248,6 @@ button:

👉 Please keep in mind that the Button color should match the ToastBar color.

⚠️ The JavaScript functionality for dismissing the ToastBar is yet to be implemented.

## Full Example

```html
Expand Down Expand Up @@ -252,6 +284,42 @@ button:
<!-- Toast: end -->
```

## JavaScript Plugin API

| Method | Description |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getInstance` | _Static_ method which allows you to get the FileUploader instance associated with a DOM element. |
| `getOrCreateInstance` | _Static_ method which allows you to get the FileUploader instance associated with a DOM element, or create a new one in case it wasn’t initialized. |
| `hide` | Hides the toast element. Returns to the caller before the toast has actually been hidden (i.e. before the `hidden.toast` event occurs). |
| `show` | Reveals the toast element. **Returns to the caller before the toast has actually been shown** (i.e. before the `shown.toast` event occurs). |

```js
const toast = Toast.getInstance('#example'); // Returns a toast instance

toast.show();
```

### JavaScript Events

| Method | Description |
| -------------- | ------------------------------------------------------------------------------------- |
| `hidden.toast` | This event is fired when the `hide` instance has finished being hidden from the user. |
| `hide.toast` | This event is fired immediately when the `hide` instance method has been called. |
| `show.toast` | This event fires immediately when the `show` instance method is called. |
| `shown.toast` | This event is fired when the `show` instance has finished being shown to the user. |

```js
const myToastEl = document.getElementById('myToast');
const toast = Toast.getOrCreateInstance(myToastEl);

myToastEl.addEventListener('hidden.toast', () => {
// Do something…
});

toast.hide();
```

[web-readme]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web/README.md
[mdn-role-log]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/log_role
[mdn-aria-live]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-live
[dictionary-alignment]: https://github.com/lmc-eu/spirit-design-system/blob/main/docs/DICTIONARIES.md#alignment
Expand Down
Loading

0 comments on commit b44a384

Please sign in to comment.