0.5.0 • Published 7 months ago

@hdsydsvenskan/local-modal v0.5.0

Weekly downloads
-
License
UNLICENSED
Repository
github
Last release
7 months ago

local-modal

A reusable modal dialog component, initially loosely based on the WAI-ARIA dialog examples, with added considerations for current screen reader behaviors (as far as can reasonably be accomodated) – and, most notably, falling back to focusing the dialog element itself if a suitable first focus point cannot be established.

In general, this implementation has a few different parts:

  • A modal dialog implementation
  • ...which implements focus management via a focus trap implementation
  • ...with optionally, backdrop behavior implementation
  • ...and, also optionally, a scroll-locking implementation
  • A ”modal controller“ which opens, replaces or closes modals on the page, managing the overall state.
  • A data-attribute-API, which allows for easily building controls to open/close/replace modals when interacting with buttons etc.

These parts are intended to be as loosely coupled as possible.

Current status

Basically functional, but needs testing

This is what we’re aiming for in a 1.0 release:

Design goals

  • Simple to use via custom elements
  • Well-documented in API and usage instructions
  • Data-attribute api for bits outside modal itself
  • Optional dependency injection, in order to re-use e.g. already existing polyfills or helpers - somewhat undecided/unfinished
  • Neutral with regards to styling
  • Prepared for async behaviors (animating in/out etc) and callbacks/events – somewhat undecided/unfinished

Feature set

  • Proper handling of focus (tab order, focusing inside modal when opened, focus back to opener when closed)
  • Handling replacing a modal
  • Handling opening up and closing nested modals (potentially with limitations)
  • Managing ARIA states for modality in an automated way
  • Handling of backdrop element
  • Having (and passing) a robust set of unit- and end-to-end tests, in several browsers. – has reasonable coverage, needs more e2e-tests
  • Compatibility tested with at least 2 popular browser/screen reader combos, on 2 different platforms – performs reasonably in Safari+VoiceOver/Mac so far

Installation and usage instructions

Installing

Install the component using yarn add @hdsydsvenskan/local-modal or npm install @hdsydsvenskan/local-modal.

Importing

You can use the component and its pieces in a few different ways. The easiest way:

import '@hdsydsvenskan/local-modal';

...imports the index file, which sets up the modal custom element along with its controller and data--attribute API.

This modal is as small as possible, and does not include a backdrop implementation or a scroll lock implementation.


Please note that this may demand more of how you implement e.g. styling in order to make the modal accessible & usable.


What it does give your is the following:

  • The ability to use <local-modal>-elements in your code and automatically have them act as modal dialogs
  • A delegated click handler which will allow you to control any modal on the page via the data-local-modal API.

Kitchen sink example

For full control over imports and which pieces get used, you can create your own setup, preferrably in its own file like this:

import modalDialogFactory from '@hdsydsvenskan/local-modal/src/modal-dialog';
import ModalController from '@hdsydsvenskan/local-modal/src/modal-controller';
import { FocusTrap } from '@hdsydsvenskan/local-modal/src/focus-trap';
import Backdrop from '@hdsydsvenskan/local-modal/src/backdrop';
import ScrollLock from '@hdsydsvenskan/local-modal/src/scroll-lock';

export const ModalDialog = modalDialogFactory({
  FocusTrap,
  BackDrop,
  ScrollLock
});

export const controller = new ModalController();

Then, your can import your modal class elswhere, and define your element name as well as set up the data- api.

import { ModalDialog } from './my-modal';
import { dataApiSetup } from '@hdsydsvenskan/local-modal/src/data-attr-api';

dataApiSetup();

window.customElements.define('my-local-modal', ModalDialog);

Using the modal

Custom element markup requirements

The following attributes are required when using the custom element:

  • A unique ID in the id attribute
  • The hidden boolean attribute

The following patterns are strongly recommended:

  • The aria-labelledby attribute, with an IDRef value pointing to a heading element inside the modal of the same ID.
  • A heading element (visible, or accessible only by screen readers) as early as possible in the modal contents, describing the purpose of the dialog.

Example markup:

<local-modal id="modal-example-a" hidden aria-labelledby="modal-example-a-heading">
  <h2>Here’s what this modal does</h2>
  <p>Further text content</p>
  <div>
    <label for="modal-a-text">Enter your name:</label>
    <input type="text" id="modal-a-text" name="name">
  </div>
  <button type="submit">Save</button>
</local-modal>

Data-attribute API

To control the modal, you need to trigger custom events that get picked up by the modal controller instance. The modal controller then opens, closes or replaces the relevant modals.

In order to help you trigger these events, a simple data--attribute API is provided. It works something like this:

<button data-local-modal="open modal-example-a">Update settings</button>

...where the value of the attribute is split by spaces and converted to data sent via the event.

Clicking this button will trigger an event for the controller to open the modal with ID modal-example-a.

If you want to close a modal, put a button inside the modal and change the value to close:

<button data-local-modal="close">Cancel</button>

If there is a button inside an already open modal and you want to replace it with another, use the following syntax:

<button data-local-modal="replace modal-example-a">Show help text</button>

There is a small algorithm for what gains focus when the modal is opened. If you want to tell it to focus a specific element when opening, pass the id of that element as the third bit of the value.

<button data-local-modal="open modal-example-a modal-a-text">Update settings</button>

...which would then automatically focus the input field when the modal opens.

Finally, you can adress where focus should go when the modal is closed – by default, the opening element receives focus when the modal closes, but if you pass another ID, the modal will find that element and configure itself to focus it once the modal closes:

<button data-local-modal="open modal-example-a modal-a-text some-id-to-focus-after-closing">Update settings</button>

Note This edge case is a bit clunky, and currently depends on all pieces of the data-API parameters being filled in. That said, it could be useful if the button somehow is inaccessible after the modal is closed.


Custom event API

The same type of events triggered via the data-attribute declarative API can also be triggered programatically.

See the src/data-attr-api.js file for examples, but here's an example of how to open a certain modal:

const id = 'my-element-id';
const focusAfter = 'some-element-id';
const focusFirst = 'some-other-element-id';

const controllerEvent = new CustomEvent(`lcl-modal:open`, {
    detail: {
      id,
      focusAfter, // optional
      focusFirst // optional
    }
  });
  document.dispatchEvent(controllerEvent);

Tests

Unit tests

  • Tests are run on Mocha via the Karma test-runner (and karma-mocha plugin).
  • JS code is bundled before tests via Parcel (karma-parcel plugin).
  • Mocha has chai, chai-as-expected (expectations, async), sinon and sinon-chai (test sandbox stuff) integrations.
  • Karma launches headless browsers via Playwright – similar (very much so) to Puppeteer, but running on both Chromium, Firefox and WebKit (karma-launcher-webkit and karma-launcher-firefox plugins).
  • Coverage is reported via Istanbul (and the karma-coverage plugin), in console and to the .coverage dir.
  • NOTE: due to a problem with how Parcel (mis-)reads advanced features of .babelrc files, there is a small CLI utility to backup, change and restore the .babelrc when running tests. Not ideal, but works, for now. The issue is reportedly fixed in Parcel@v2, but so far there is no karma-parcel plugin compatible with that.

End-to-end tests

  • Tests are run on Mocha/chai/sinon, which launches headless browsers via Playwright
  • Currently only runs one browser (currently Chromium) but can easily be configured to run more via BROWSER env var. For example, if you want to run the e2e-tests via Firefox, you could do BROWSER=firefox yarn test-e2e. Supports firefox and webkit env-var names, default is Chromium.
  • Starts a dev-server via Parcel, closes it when done.
0.5.0

7 months ago

0.4.0

2 years ago

0.3.0

2 years ago