Skip to content

Latest commit

 

History

History
235 lines (181 loc) · 6.32 KB

CONTRIBUTING.md

File metadata and controls

235 lines (181 loc) · 6.32 KB

Contributing Guide

1. Anatomy of a UI Module

This section walks through the makeup-switch source code. Use it as a reference when creating a new class-based UI module.

Default Options

Every class will have a defaultOptions object.

const defaultOptions = {
    bem: {
        control: 'switch__control'
    },
    customElementMode: false
};
  • bem: If using different class names than Skin, you can specify the main hooks here.
  • customElementMode: Set this to true if using this as the model for the makeup-web-component.

Plus whatever options are relevant to that user interface module.

Constructor

Every class will have a constructor, for example:

constructor(el, selectedOptions) {
    this.options = Object.assign({}, defaultOptions, selectedOptions);

    this.el = el;

    this._onClickListener = this._onClick.bind(this);
    this._onKeyDownListener = this._onKeyDown.bind(this);
    this._onMutationListener = this._onMutation.bind(this);

    if (!this.options.customElementMode) {
        this._mutationObserver = new MutationObserver(this._onMutationListener);
        this._observeMutations();
        this._observeEvents();
    }
}

First we always merge any given options with the default options and set the element reference.

Next we cache any event listener references and bind/scope the handlers to the class instance.

Next, if not being used as the model for a web component, we must initialise our mutation observer and event observers. A web component would initialise the event observers (via _observeEvents()) during its connectedCallback() routine.

Mutation Observer

Our mutation observer is essentially going to mimic the attributeChangedCallback() life cycle method of a web component. This is a handy way of letting any observers know when important properties of the class have changed.

_observeMutations() {
    if (!this.options.customElementMode) {
        this._mutationObserver.observe(this._focusableElement, {
            attributes: true,
            childList: false,
            subtree: false
        });
    }
}

_unobserveMutations() {
    if (!this.options.customElementMode) {
        this._mutationObserver.disconnect();
    }
}

Mutation Handler

The mutation handler creates an abstraction around the mutation, dispatching a custom event with the detail of the changed attribute(s).

_onMutation(mutationsList) {
    for (const mutation of mutationsList) {
        if (mutation.type === 'attributes') {
            this.el.dispatchEvent(new CustomEvent('makeup-switch-mutation', {
                detail: {
                    attributeName: mutation.attributeName
                }
            }));
        }
    }
}

Event Listeners

This section is the setup and tear down for our event listeners (i.e. any interesting device interactions on the ui element itself - click, focus, keydown, etc).

_observeEvents() {
    this._focusableElement.addEventListener('click', this._onClickListener);
    this._focusableElement.addEventListener('keydown', this._onKeyDownListener);
}

_unobserveEvents() {
    this._focusableElement.removeEventListener('click', this._onClickListener);
    this._focusableElement.removeEventListener('keydown', this._onKeyDownListener);
}

Event Handlers

Use private methods (remember, they should be bound/scoped to the class instance) for event handler routines.

_onKeyDown(e) {
    switch (e.keyCode) {
        case 32:
            e.preventDefault();
            this.toggle();
            break;
        case 37:
            this.checked = false;
            break;
        case 39:
            this.checked = true;
            break;
        default:
            break;
    }
}

_onClick() {
    if (!this.disabled) {
        this.toggle();
    }
}

Getters & Setters

Every class will expose its property API via getters and setters.

In order to prevent an issue with any web component that might be using this class, we must unobserve all mutations during any transaction that changes state (i.e. setters).

get _focusableElement() {
    return this.el.querySelector(`.${this.options.bem.control}`);
}

set checked(isChecked) {
    this._unobserveMutations();
    this._focusableElement.setAttribute('aria-checked', isChecked.toString());
    this.el.dispatchEvent(new CustomEvent('makeup-switch-toggle', {
        composed: true,
        detail: {
            on: this.checked
        }
    }));
    this._observeMutations();
}

get checked() {
    return this._focusableElement.getAttribute('aria-checked') === 'true';
}

set disabled(isDisabled) {
    this._unobserveMutations();
    this._focusableElement.setAttribute('aria-disabled', isDisabled.toString());
    this._focusableElement.setAttribute('tabindex', isDisabled ? '-1' : '0');
    this._observeMutations();
}

get disabled() {
    return this._focusableElement.getAttribute('aria-disabled') === 'true';
}

set labelledby(theId) {
    this._unobserveMutations();
    this._focusableElement.setAttribute('aria-labelledby', theId);

    // customElementMode a11y workaround
    // aria-labelledby cannot resolve element id references that live outside of the Shadow DOM
    // as a workaround we can use aria-label
    if (this.options.customElementMode) {
        const labellingEl = document.getElementById(this.labelledby);

        if (labellingEl && labellingEl.innerText !== '') {
            this.label = labellingEl.innerText;
        }
    }

    this._observeMutations();
}

get labelledby() {
    return this._focusableElement.getAttribute('aria-labelledby');
}

get label() {
    return this._focusableElement.getAttribute('aria-label');
}

set label(theLabel) {
    this._unobserveMutations();
    this._focusableElement.setAttribute('aria-label', theLabel);
    this._observeMutations();
}

Methods

Any methods unique to the class go here.

toggle() {
    this.checked = !(this.checked);
}

Destroy

Finally, a destroy method allows the class and all of its references to be cleanly removed from memory.

_destroy() {
    this._unobserveMutations();
    this._unobserveEvents();
    this._onClickListener = null;
    this._onKeyDownListener = null;
    this._onMutationListener = null;
}

2. Anatomy of a Core Module

(TODO)