Skip to content

reecelucas/svelte-accessible-dialog

master
Switch branches/tags
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
src
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

svelte-accessible-dialog

An accessible dialog component for Svelte apps. Demo.

Coverage Status Build Status npm bundle size npm GitHub

Installation

npm install svelte-accessible-dialog

Usage

Basic

<script>
  import { DialogOverlay, DialogContent } from 'svelte-accessible-dialog';

  let isOpen;

  const open = () => {
    isOpen = true;
  };

  const close = () => {
    isOpen = false;
  };
</script>

<button on:click={open}>Open Dialog</button>

<DialogOverlay {isOpen} onDismiss={close}>
  <DialogContent aria-label="Announcement">
    <button on:click={close}>Close</button>
    <p>I am a dialog</p>
  </DialogContent>
</DialogOverlay>

Setting Initial Focus

By default, the first focusable element will receive focus when the dialog opens, but you can provide an element to focus instead.

<script>
  import { DialogOverlay, DialogContent } from 'svelte-accessible-dialog';

  let isOpen;
  let initialFocusElement;

  const open = () => {
    isOpen = true;
  };

  const close = () => {
    isOpen = false;
  };
</script>

<button on:click={open}>Open Dialog</button>

<DialogOverlay {isOpen} {initialFocusElement} onDismiss={close}>
  <DialogContent aria-label="Announcement">
    <button on:click={close}>Close</button>
    <label>
      Name: <input type="text" bind:this={initialFocusElement} />
    </label>
    <p>I am a dialog</p>
  </DialogContent>
</DialogOverlay>

Setting Return Focus

By default, the element that invoked the dialog will receive focus when the dialog closes, but you can provide an element to focus instead.

See the WAI-ARIA authoring practices for more detail about when you might want to do this.

<script>
  import { DialogOverlay, DialogContent } from 'svelte-accessible-dialog';

  let isOpen;
  let returnFocusElement;

  const open = () => {
    isOpen = true;
  };

  const close = () => {
    isOpen = false;
  };
</script>

<button on:click={open}>Open Dialog</button>
<button bind:this={returnFocusElement}>I focus on close</button>

<DialogOverlay {isOpen} {returnFocusElement} onDismiss={close}>
  <DialogContent aria-label="Announcement">
    <button on:click={close}>Close</button>
    <p>I am a dialog</p>
  </DialogContent>
</DialogOverlay>

Legacy Support for aria-modal

DialogContent has the aria-modal attribute. This indicates to screen readers that only content contained within the dialog should be accessible to the user. Modern screen readers respect this attribute, but you can enable a legacy workaround if you require deeper support.

See the WAI-ARIA authoring practices for more detail.

<script>
  import { DialogOverlay, DialogContent } from 'svelte-accessible-dialog';

  let isOpen;

  const open = () => {
    isOpen = true;
  };

  const close = () => {
    isOpen = false;
  };
</script>

<button on:click={open}>Open Dialog</button>

<DialogOverlay {isOpen} ariaModalLegacy={true} onDismiss={close}>
  <DialogContent aria-label="Announcement">
    <button on:click={close}>Close</button>
    <p>I am a dialog</p>
  </DialogContent>
</DialogOverlay>

Styling

:global

<style>
  :global([data-svelte-dialog-overlay].overlay) {
    z-index: 10;
  }

  :global([data-svelte-dialog-content].content) {
    border: 2px solid #000;
  }
</style>

<DialogOverlay class="overlay">
  <DialogContent class="content">
    <p>I am a dialog</p>
  </DialogContent>
</DialogOverlay>

Inline Styles

<DialogOverlay style="z-index: 10">
  <DialogContent style="border: 2px solid #000">
    <p>I am a dialog</p>
  </DialogContent>
</DialogOverlay>

Props

DialogOverlay

Must render DialogContent. Any props not listed below will be spread onto the underlying div.

Prop Type Required Description
isOpen Boolean Yes Controls whether the dialog is open or not.
onDismiss () => void Yes This function is called whenever the user hits "Escape" or clicks outside the dialog. The dialog must be closed on onDismiss.
initialFocusElement HTMLElement No The element that will receive focus when the dialog is open. Defaults to the first focusable element.
returnFocusElement HTMLElement No The element that will receive focus when the dialog closes. Defaults to the element that invoked the dialog.
ariaModalLegacy Boolean No Enables a fallback for the aria-modal attribute. When true, all content outside of the active dialog will have the aria-hidden and inert attributes set to "true".

DialogContent

Must be a child of DialogOverlay. Element props will be spread onto the underlying div.

Accessibility

WAI-ARIA: https://www.w3.org/TR/wai-aria-practices-1.2/#dialog_modal

Keyboard Accessibility

key action
Escape Dismisses the dialog

Tabbable Elements

It's recommended to have at least one tabbable element in the DialogContent. Ideally, the first element in the dialog should be a close button. If no tabbable elements are found, the dialog content element itself will receive focus.

Hiding Page Content from Screen Readers

Until fairly recently, keeping a screen reader within an active dialog was difficult. A focus trap prevents focus from leaving a dialog, but does nothing to stop a wandering virtual cursor. A common solution to this problem was to set the aria-hidden and inert attributes on all elements outside of the active dialog.

ARIA 1.1 introduced the aria-modal attribute. aria-modal indicates to screen readers that only content contained within a dialog with aria-modal="true" should be accessible to the user. Modern screen readers respect this attribute, so svelte-accessible-dialog does not implement the legacy workaround by default.

If support for aria-modal is inadequate for your app, you can pass ariaModalLegacy={true} to DialogOverlay to enable the legacy workaround.

Labelling

A dialog needs to be properly labelled to provide context for users that rely on assistive technology. If a dialog is announced to the user without a label, it can be confusing and difficult to navigate.

There are two general approaches to labelling: aria-label and aria-labelledby. If the text is visible on the screen, you should provide the label's HTML element with a unique id attribute. That id is then given to an aria-labelledby attribute on the DialogContent. With this context, the screen reader will announce whatever text is nested inside that ID'd markup as the title for the Dialog.

If a design doesn't include a visible label on the screen, you need to provide an aria-label prop on the DialogContent instead.

aria-label

<DialogContent aria-label="Cookie notice">
  <p>We use cookies to improve your website experience</p>
  <button>Not interested</button>
  <button>Ok, thanks</button>
</DialogContent>

aria-labelledby

<DialogContent aria-labelledby="cookie-dialog-title">
  <h2 id="cookie-dialog-title">Cookie Notice</h2>
  <p>We use cookies to improve your website experience</p>
  <button>Not interested</button>
  <button>Ok, thanks</button>
</DialogContent>

Z-index

DialogOverlay does not set a z-index. It depends on DOM order to be on top of the page content (it's inserted at the end of the document when it's opened). If you're fighting z-index wars, make sure to add a z-index to DialogOverlay.

Configuring webpack

If you're using webpack with svelte-loader, make sure to add "svelte" to resolve.mainFields in your webpack config. This ensures that webpack imports the uncompiled components (src/index.js) rather than the compiled version (dist/index.mjs), which is more efficient.

If you're using Rollup with rollup-plugin-svelte, this will happen automatically.

Tests

Tests use Jest and svelte-testing-library.

git clone git@github.com:reecelucas/svelte-accessible-dialog.git
cd svelte-accessible-dialog
yarn
yarn test

LICENSE

MIT