# Modal

Modal dialog with a focus trap attachment for accessible keyboard navigation.

## Example code

```html
<!DOCTYPE html>
<html>

<head>
  <title>Modal</title>
  <meta name="description"
    content="Modal dialog with a focus trap attachment for accessible keyboard navigation.">
  <script type="importmap">
    {
      "imports": {
        "nanotags": "https://esm.sh/nanotags@latest",
        "nanostores": "https://esm.sh/nanostores@latest"
      }
    }
  </script>
</head>

<body>
  <div id="description">Modal dialog with a focus trap <a href='cookbook#attachments'>attachment</a> for accessible keyboard navigation.</div>

  <div data-type="html">

    <x-dialog>
      <button data-ref="trigger">Open Dialog</button>
      <dialog data-ref="dialog">
        <h3>Confirm Action</h3>
        <p>This dialog traps focus using an attachment pattern. Try pressing Tab to cycle through focusable elements.
        </p>
        <div class="actions">
          <button data-ref="cancel">Cancel</button>
          <button data-ref="confirm" class="primary">Confirm</button>
        </div>
      </dialog>
    </x-dialog>
  </div>

  <script type="module" data-type="javascript">
    import { define } from "nanotags";
    import { atom } from "nanostores";
    import { attachFocusTrap } from "attachFocusTrap.js";

    define("x-dialog")
      .withRefs(({ one }) => ({
        trigger: one("button"),
        dialog: one("dialog"),
        cancel: one("button"),
        confirm: one("button"),
      }))
      .setup((ctx) => {
        const $open = atom(false);

        function open() {
          $open.set(true);
        }

        function close() {
          $open.set(false);
        }

        attachFocusTrap(ctx, ctx.refs.dialog);

        ctx.on(ctx.refs.trigger, "click", open);
        ctx.on(ctx.refs.cancel, "click", close);
        ctx.on(ctx.refs.confirm, "click", () => {
          console.log("Confirmed!");
          close();
        });
        ctx.on(ctx.refs.dialog, "cancel", (e) => {
          e.preventDefault();
          close();
        });


        ctx.effect($open, (open) => {
          if (open) {
            ctx.refs.dialog.showModal();
          } else {
            ctx.refs.dialog.close();
          }
        });

        return { open, close };
      });
  </script>

  <script type="module" data-type="javascript" data-name="attachFocusTrap.js">
    const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';

    export function attachFocusTrap(ctx, container) {
      ctx.on(container, "keydown", (e) => {
        if (e.key !== "Tab") return;

        const focusable = [...container.querySelectorAll(FOCUSABLE)];
        if (focusable.length === 0) return;

        const first = focusable[0];
        const last = focusable[focusable.length - 1];

        if (e.shiftKey && document.activeElement === first) {
          e.preventDefault();
          last.focus();
        } else if (!e.shiftKey && document.activeElement === last) {
          e.preventDefault();
          first.focus();
        }
      });
    }
  </script>

  <style data-type="css">
    dialog {
      border: none;
      border-radius: 12px;
      padding: 24px;
      max-width: 400px;
      background: var(--surface-hover);
      box-shadow: 0 20px 60px light-dark(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.6));
    }

    dialog::backdrop {
      background: light-dark(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.6));
    }

    dialog h3 {
      margin: 0 0 12px;
    }

    dialog p {
      color: var(--text-muted);
      margin: 0 0 20px;
    }

    dialog .actions {
      display: flex;
      justify-content: flex-end;
      gap: 8px;
    }

    dialog button.primary {
      background: var(--accent);
      color: white;
      border-color: var(--accent);
    }

    dialog button.primary:hover {
      background: var(--accent-hover);
    }
  </style>
</body>

</html>

```
