@hdsydsvenskan/local-modal
v0.5.0
Published
A reusable modal dialog component, initially loosely based on the [WAI-ARIA dialog examples](https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/dialog.html), with added considerations for current screen reader behaviors (as far as can reasonab
Downloads
219
Maintainers
Keywords
Readme
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
- [x] Simple to use via custom elements
- [x] Well-documented in API and usage instructions
- [x] 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
- [x] Neutral with regards to styling
- [x] Prepared for async behaviors (animating in/out etc) and callbacks/events – somewhat undecided/unfinished
Feature set
- [x] Proper handling of focus (tab order, focusing inside modal when opened, focus back to opener when closed)
- [x] Handling replacing a modal
- [x] Handling opening up and closing nested modals (potentially with limitations)
- [x] Managing ARIA states for modality in an automated way
- [x] 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
andsinon-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
andkarma-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 nokarma-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 doBROWSER=firefox yarn test-e2e
. Supportsfirefox
andwebkit
env-var names, default is Chromium. - Starts a dev-server via Parcel, closes it when done.