# yaml-language-server: $schema=https://opencomponents.dev/schemas/contract.json
# The Dialog contract, with every rule from its checklist.
# The page explains why each one exists: https://opencomponents.dev/raw/docs/components/dialog.md

component: Dialog
summary: Asks for someone's full attention, on top of the page, until they answer or close it. Its title names it.
standard: https://opencomponents.dev/docs/components/dialog
element: dialog # opened with showModal(), which puts it in the top layer and makes the page inert
role: dialog # alertdialog, with role="alertdialog"
props:
  open: { type: boolean, default: false } # it never changes on its own
  id: { type: string } # required. Its title's and description's IDs start with it
  title: { type: string } # required
  description: { type: string }
  role: { type: [dialog, alertdialog], default: dialog }
  size: { type: [sm, md, lg], default: md }
  closeLabel: { type: string, default: Close }
slots:
  default: The content, like a message or a form, which scrolls when it doesn't fit.
  actions: The Buttons that answer the dialog, like Cancel and Save, in the order people read them.
events:
  close: "escape, close-button, backdrop or action, and the value of the button that sent a <form method=dialog>. Fires when people ask to close it, which it leaves to you. closed when it's already closed, like when the browser closed it, so open has to follow."
states:
  open: "[open], and :modal once showModal() ran"
  closed: ":not([open]), hidden by the browser's own styles"
keyboard:
  Tab: Moves focus between its controls, and never to the page behind it.
  Escape: Fires close, with the reason escape, in an alert dialog too.
parts: [header, title, close, description, body, actions] # data-slot, in this order. Only the header and the title are always there
tokens:
  theme: ["--color--neutral-text"]
  variables: [--dialog--width, --dialog--padding, --dialog--gap, --dialog--radius, --dialog--border-color, --dialog--background, --dialog--backdrop--background-color]
  layer: components # in Tailwind's order: theme, base, components, utilities
rules:
  - id: dialog/contrast
    layer: ui
    level: must
    scope: component
    requirement: "The title, the description and the content have a contrast of 4.5:1 against the panel"
    basis: "WCAG 1.4.3 (AA)"
    check: "axe `color-contrast`, contrast checker"
  - id: dialog/backdrop
    layer: ui
    level: should
    scope: component
    requirement: "A backdrop dims the page behind an open dialog, and a border sets the panel apart where the backdrop barely shows"
    basis: "Open Components"
    check: "Review"
  - id: dialog/fits-screen
    layer: ui
    level: must
    scope: component
    requirement: "It's never wider or taller than the screen, less a margin, down to 320px wide"
    basis: "WCAG 1.4.10 (AA)"
    check: "Emulation"
  - id: dialog/resizable
    layer: ui
    level: must
    scope: component
    requirement: "It's sized in `rem`, so it grows with its text, and its title wraps rather than getting cut off"
    basis: "Open Components, beyond WCAG 1.4.4, 1.4.12 (AA)"
    check: "200% zoom"
  - id: dialog/actions-in-view
    layer: ui
    level: should
    scope: component
    requirement: "The title and the actions stay in view, and only the body scrolls, when the content doesn't fit"
    basis: "Open Components"
    check: "Emulation"
  - id: dialog/fitting-size
    layer: ui
    level: should
    scope: usage
    requirement: "Its size fits its content, like `sm` for a confirmation and `lg` for a long form"
    basis: "Open Components"
    check: "Review"
  - id: dialog/reduced-motion
    layer: ui
    level: should
    scope: component
    requirement: "It only animates as it opens when `prefers-reduced-motion` is `no-preference`"
    basis: "WCAG 2.3.3 (AAA)"
    check: "Emulation"
  - id: dialog/forced-colors
    layer: ui
    level: must
    scope: component
    requirement: "The panel's border stays visible in forced colours mode"
    basis: "Open Components"
    check: "Emulation"
  - id: dialog/more-contrast
    layer: ui
    level: should
    scope: component
    requirement: "The panel's border is drawn at full strength when `prefers-contrast: more` is set"
    basis: "Open Components"
    check: "Emulation"
  - id: dialog/mirrors-in-rtl
    layer: ui
    level: should
    scope: component
    requirement: "It uses logical properties, so its close button and actions mirror in right-to-left languages"
    basis: "Open Components"
    check: "Review"
  - id: dialog/native-element
    layer: ux
    level: must
    scope: component
    requirement: "It's a native `<dialog>`, opened with `showModal()`, and never a `<div>` with `role=\"dialog\"`"
    basis: "Open Components, beyond WCAG 2.1.2, 4.1.2 (A)"
    check: "Unit test"
  - id: dialog/modal
    layer: ux
    level: must
    scope: component
    requirement: "While it's open, it's in the top layer and the rest of the page is inert, so nothing behind it can be focused, clicked or read"
    basis: "APG"
    check: "Keyboard, screen reader"
  - id: dialog/contain-focus
    layer: ux
    level: must
    scope: component
    requirement: "`Tab` and `Shift+Tab` never move focus to the page behind an open dialog"
    basis: "WCAG 2.4.3 (A), APG"
    check: "Keyboard"
  - id: dialog/named
    layer: ux
    level: must
    scope: component
    requirement: "It's named by its title, with `aria-labelledby`"
    basis: "WCAG 4.1.2 (A)"
    check: "Unit test"
  - id: dialog/visible-title
    layer: ux
    level: must
    scope: usage
    requirement: "Every dialog has a visible title that says what it's for, like \"Delete project?\""
    basis: "WCAG 1.3.1 (A), 2.4.6 (AA)"
    check: "Review"
  - id: dialog/described
    layer: ux
    level: should
    scope: both
    requirement: "A short description is linked with `aria-describedby`, while longer content, like a form, isn't"
    basis: "APG"
    check: "Unit test"
  - id: dialog/initial-focus
    layer: ux
    level: must
    scope: both
    requirement: "Opening it moves focus inside, to the control marked with `data-autofocus`, or else the first in its body, then its first action"
    basis: "WCAG 2.4.3 (A), APG"
    check: "Unit test"
  - id: dialog/safe-focus
    layer: ux
    level: must
    scope: both
    requirement: "An alert dialog's initial focus is on its least destructive action"
    basis: "APG"
    check: "Unit test, review"
  - id: dialog/return-focus
    layer: ux
    level: must
    scope: both
    requirement: "Closing it moves focus back to the control that opened it, or to a logical place when that control is gone"
    basis: "WCAG 2.4.3 (A), APG"
    check: "Unit test, keyboard"
  - id: dialog/escape
    layer: ux
    level: must
    scope: component
    requirement: "`Escape` asks to close every dialog, alert dialogs included, unless a control inside handles it first"
    basis: "APG"
    check: "Unit test"
  - id: dialog/close-button
    layer: ux
    level: must
    scope: component
    requirement: "A dialog, but not an alert dialog, has a visible close button, a Button named \"Close\""
    basis: "Open Components"
    check: "Unit test"
  - id: dialog/backdrop-dismiss
    layer: ux
    level: should
    scope: component
    requirement: "A press that starts and ends on the backdrop asks to close a dialog, but never an alert dialog"
    basis: "Open Components"
    check: "Unit test"
  - id: dialog/way-out
    layer: ux
    level: must
    scope: usage
    requirement: "Every alert dialog has an action that leaves things as they are, like \"Keep project\""
    basis: "Open Components"
    check: "Review"
  - id: dialog/outcome-names
    layer: ux
    level: should
    scope: usage
    requirement: "Its actions are named by what they do, like \"Delete project\", rather than \"Yes\" or \"OK\""
    basis: "Open Components"
    check: "Review"
  - id: dialog/keep-changes
    layer: ux
    level: should
    scope: usage
    requirement: "A dialog with unsaved changes asks before it closes, rather than losing them"
    basis: "Open Components"
    check: "Review"
  - id: dialog/scroll-lock
    layer: ux
    level: should
    scope: component
    requirement: "The page behind an open dialog doesn't scroll, and doesn't shift when its scrollbar goes"
    basis: "Open Components"
    check: "Emulation"
  - id: dialog/use-sparingly
    layer: ux
    level: should
    scope: usage
    requirement: "It's only used for what needs an answer before people go on, and never opens on its own, like on page load"
    basis: "Open Components"
    check: "Review"
  - id: dialog/one-at-a-time
    layer: ux
    level: should
    scope: usage
    requirement: "At most one dialog opens on top of another, and it closes first"
    basis: "Open Components"
    check: "Review"
  - id: dialog/shared-vocabulary
    layer: dx
    level: must
    scope: component
    requirement: "It uses the shared names: `open`, `size`, `actions` and `close` (`closeRequest` in Angular)"
    basis: "Open Components"
    check: "Review"
  - id: dialog/typed
    layer: dx
    level: must
    scope: component
    requirement: "Props, events and slots are typed, with unions rather than `string`, and `id` and `title` (`dialogTitle` in Angular) are required"
    basis: "Open Components"
    check: "Type check"
  - id: dialog/controllable
    layer: dx
    level: must
    scope: component
    requirement: "It opens and closes with `open`, and asks rather than closing itself: `Escape`, the close button and the backdrop fire `close`, with why, and when a browser closes it anyway, it fires `close` with `closed`"
    basis: "Open Components"
    check: "Unit test"
  - id: dialog/root-attributes
    layer: dx
    level: must
    scope: component
    requirement: "Attributes, classes and listeners land on the `<dialog>`"
    basis: "Open Components"
    check: "Unit test"
  - id: dialog/form-actions
    layer: dx
    level: should
    scope: component
    requirement: "A `<form method=\"dialog\">` inside it fires `close` with the reason `action` and its button's value, rather than closing it"
    basis: "HTML"
    check: "Unit test"
  - id: dialog/role-and-name
    layer: ax
    level: must
    scope: component
    requirement: "It can be found by its role and title alone, with `getByRole(\"dialog\", { name })`, or `\"alertdialog\"`"
    basis: "WCAG 4.1.2 (A)"
    check: "Unit test, ARIA snapshot"
  - id: dialog/hidden-when-closed
    layer: ax
    level: must
    scope: component
    requirement: "A closed dialog isn't in the accessibility tree, so neither agents nor screen readers find it"
    basis: "HTML"
    check: "Unit test"
  - id: dialog/deterministic-dom
    layer: ax
    level: must
    scope: component
    requirement: "The same props render the same markup, with its parts' IDs made from its own, and opening it changes attributes, never elements"
    basis: "Open Components"
    check: "Unit test"
  - id: dialog/data-attributes
    layer: ax
    level: should
    scope: component
    requirement: "The size is mirrored as `data-size`, and parts are marked with `data-slot`"
    basis: "Open Components"
    check: "Unit test"
  - id: dialog/dev-warnings
    layer: ax
    level: should
    scope: component
    requirement: "It warns during development when it has no title"
    basis: "Open Components"
    check: "Unit test"
