# dialog
[![tests](https://img.shields.io/github/actions/workflow/status/substrate-system/dialog/nodejs.yml?style=flat-square)](https://github.com/substrate-system/dialog/actions/workflows/nodejs.yml)
[![types](https://img.shields.io/npm/types/@substrate-system/dialog?style=flat-square)](README.md)
[![module](https://img.shields.io/badge/module-ESM%2FCJS-blue?style=flat-square)](README.md)
[![semantic versioning](https://img.shields.io/badge/semver-2.0.0-blue?logo=semver&style=flat-square)](https://semver.org/)
[![Common Changelog](https://nichoth.github.io/badge/common-changelog.svg)](https://common-changelog.org)
[![install size](https://flat.badgen.net/packagephobia/install/@substrate-system/dialog)](https://packagephobia.com/result?p=@substrate-system/dialog)
[![gzip size](https://flat.badgen.net/bundlephobia/minzip/@substrate-system/dialog)](https://bundlephobia.com/package/@substrate-system/dialog)
[![license](https://img.shields.io/badge/license-Big_Time-blue?style=flat-square)](LICENSE)


Modal/dialog window.

See [smashingmagazine.com article](https://www.smashingmagazine.com/2022/04/cta-modal-build-web-component/) and [nathansmith/cta-modal](https://github.com/nathansmith/cta-modal/tree/main).


<details><summary><h2>Contents</h2></summary>

<!-- toc -->

- [Install](#install)
- [Use](#use)
  * [FOUCE](#fouce)
  * [CSS](#css)
    + [Import](#import)
    + [CSS Variables](#css-variables)
  * [Bundler](#bundler)
  * [HTML only](#html-only)
- [API](#api)
  * [Attributes](#attributes)
    + [`active`](#active)
    + [`closable`](#closable)
    + [`no-icon`](#no-icon)
    + [`animated`](#animated)
    + [`noclick`](#noclick)
    + [`close`](#close)
  * [Methods](#methods)
    + [`open()`](#open)
    + [`close()`](#close)
  * [Events](#events)
    + [`close`](#close-1)
    + [`open`](#open)
- [Accessibility](#accessibility)
  * [Things You Need To Do](#things-you-need-to-do)
  * [Include a heading](#include-a-heading)
  * [Adding a description](#adding-a-description)
- [Example](#example)
- [Credits](#credits)

<!-- tocstop -->

</details>


## Install

```sh
npm i -S @substrate-system/dialog
```

## Use

### FOUCE

>
> [!NOTE]
> You should prevent the flash of undefined custom elements, because
> the modal content should be hidden from the user until it is opened.
> See [abeautifulsite.net](https://www.abeautifulsite.net/posts/revisiting-fouce).
>

This is how I do it in the example page:

```ts
import { ModalWindow } from '@substrate-system/dialog'

(async () => {
    await Promise.race([
        // Load all custom elements
        Promise.allSettled([
            customElements.whenDefined(ModalWindow.TAG),  // modal-window
        ]),
        // Resolve after two seconds
        new Promise(resolve => setTimeout(resolve, 2000))
    ])

    // Remove the class, showing the page content
    document.body.classList.remove('reduce-fouce')
})()
```

And the HTML has a class `reduce-fouce`:

```html
<body class="reduce-fouce">
```

The CSS:

```css
body {
    &.reduce-fouce {
        opacity: 0;
    }
}
```

### CSS

#### Import

If you are using a bundler:

```js
import '@substrate-system/dialog/css'
```

Or via CSS imports, for example with
[lightningcss](https://lightningcss.dev/bundling.html):

```css
@import url("@substrate-system/dialog/css");
```

#### CSS Variables

Customize the CSS with some variables.

```css
:root {
    /* Overlay */
    --modal-overlay-z-index: 100000;
    --modal-overlay-background-color: rgba(0, 0, 0, 0.5);
    --modal-overlay-padding-top: 20px;
    --modal-overlay-padding-left: 20px;
    --modal-overlay-padding-right: 20px;
    --modal-overlay-padding-bottom: 20px;

    /* Dialog */
    --modal-dialog-background-color: #fff;
    --modal-dialog-border-radius: 0;
    --modal-dialog-box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.5);
    --modal-dialog-padding: 1rem;
    --modal-dialog-width: 500px;

    /* Close button */
    --modal-close-color: #fff;
    --modal-close-background-color: #000;
    --modal-close-border-radius: 50%;
    --modal-close-box-shadow: 0 0 0 1px #fff;
    --modal-close-display: block;
    --modal-close-font-family: 'Arial', sans-serif;
    --modal-close-font-size: 23px;
    --modal-close-line-height: 26px;
    --modal-close-width: 26px;

    /* Close button hover/focus */
    --modal-close-color-hover: #000;
    --modal-close-background-color-hover: #fff;
    --modal-close-box-shadow-hover: 0 0 0 1px #000;
}
```

### Bundler

Just import. This calls the global function `window.customElements.define`.

```js
import '@substrate-system/dialog'
```

Then use the tag in HTML:

```html
<button id="open-modal" type="button">Open modal</button>

<modal-window id="my-modal">
    <h2>Modal Title</h2>
    <p>This is the modal content.</p>
    <p>Click the X, press Escape, or click outside to close.</p>
</modal-window>
```

Open/close via JavaScript:

```js
const modal = document.getElementById('my-modal')

document.getElementById('open-modal').addEventListener('click', () => {
    modal.open()
})
```

### HTML only

First copy the file to a location accessible to your web server.

```sh
cp ./node_modules/@substrate-system/dialog/dist/index.min.js ./public/dialog.js
```

Then link to the file in HTML
```html
<body>
    <p>...content...</p>
    <script type="module" src="/dialog.js"></script>
</body>
```

## API

### Attributes

#### `active`

Controls whether the modal is open. Set to `"true"` to open, `"false"` or remove the attribute to close.

```js
modal.setAttribute('active', 'true')   // open
modal.setAttribute('active', 'false')  // close
modal.removeAttribute('active')        // close
```

#### `closable`

Set to `"false"` to prevent the modal from being closed via the close button, Escape key, or clicking outside. You must close it programmatically. Defaults to `true`.

```html
<modal-window closable="false">
    <h2>Unclosable Modal</h2>
    <p>This modal cannot be closed with the X button, Escape key, or clicking outside.</p>
    <button id="close-btn" type="button">Close this modal</button>
</modal-window>
```

```js
document.getElementById('close-btn').addEventListener('click', () => {
    modal.close()
})
```

#### `no-icon`

Hides the close button icon. Useful when you want to provide your own close UI.

```html
<modal-window no-icon>
    <header>
        <button type="button" id="cancel">Cancel</button>
        <h3>Edit profile</h3>
        <button type="button" id="save">Save</button>
    </header>
    <div>...form content...</div>
</modal-window>
```

#### `animated`

Controls whether open/close animations are used. Set to `"false"` to disable. Defaults to `true`. Animations also respect `prefers-reduced-motion`.

```html
<modal-window animated="false">
    <p>No animation</p>
</modal-window>
```

#### `noclick`

When present, clicking the backdrop does not close the modal.
The Escape key and close button still work.

```html
<modal-window noclick>
    <p>Clicking the backdrop won't close this</p>
</modal-window>
```

#### `close`

Sets the title/aria-label for the close button. Defaults to "Close".

```html
<modal-window close="Dismiss">
    <p>Close button will have title "Dismiss"</p>
</modal-window>
```

### Methods

#### `open()`

Opens the modal and focuses it.

```js
const modal = document.querySelector('modal-window')
modal.open()
```

#### `close()`

Closes the modal and returns focus to the previously focused element.

```js
modal.close()
```

### Events

Follow the pattern in [web-component](https://github.com/substrate-system/web-component#event-namestringstring).
Events are namespaced with the prefix `modal-window:`. Listen for them by
calling `ModalWindow.event('name')`.

#### `close`

```ts
import { ModalWindow } from '@substrate-system/dialog'

const el = document.querySelector('modal-window')
el.addEventListener(ModalWindow.event('close'), () => {
    console.log('closed')
})
```

#### `open`

```ts
import { ModalWindow } from '@substrate-system/dialog'

const el = document.querySelector('modal-window')
el.addEventListener(ModalWindow.event('open'), () => {
    console.log('is open')
})
```

## Accessibility

Things handled by this library:

* `role="dialog"` and `aria-modal="true"` on the dialog
* Focus trapping (Tab cycles within modal)
* Escape key closes the modal (when `closable`)
* Focus returns to the trigger element on close
* Close button has `aria-label`
* Respects `prefers-reduced-motion`

### Things You Need To Do

### Include a heading

The component extracts text from the first heading (h1-h6) to use as the
dialog's `aria-label`. Always include a descriptive heading:

```html
<!-- Good: heading text becomes the aria-label -->
<modal-window>
    <h2>Edit Profile</h2>
    <p>Update your information below.</p>
</modal-window>

<!-- Avoid: no heading results in aria-label="modal" -->
<modal-window>
    <p>Some content without a heading...</p>
</modal-window>
```

### Adding a description

For modals with important supplementary text (like warnings), you can
add `aria-describedby`. This library will handle an `aria-describedby` attribute
correctly, meaning that it will be forwarded to the correct element.

```html
<modal-window aria-describedby="delete-warning" id="confirm-delete">
    <h2>Delete Account</h2>
    <p id="delete-warning">
        This action cannot be undone.
    </p>
    <button type="button">Cancel</button>
    <button type="button">Delete</button>
</modal-window>
```

## Example

[See `./example`](./example/).

## Credits

Thanks
[@nathansmith](https://github.com/nathansmith) and
[Smashing Magazine](https://www.smashingmagazine.com/2022/04/cta-modal-build-web-component/)
for publishing this originally.
