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

component: Tag
summary: A short label for what something is about, like a topic or a picked value, that people can remove or follow as a link. It fires remove, and never removes itself.
standard: https://opencomponents.dev/docs/components/tag
element: span # no role of its own. Its link and its remove button carry theirs
props:
  label: { type: string } # needed when removable, since the remove button's name is built from it
  size: { type: [xs, sm, md, lg, xl], default: md }
  href: { type: string } # makes the label a link
  removable: { type: boolean, default: false }
  removeLabel: { type: function } # (label) => "Remove Bug"
  disabled: { type: boolean, default: false }
  readonly: { type: boolean, default: false }
slots:
  default: The label, when it's more than plain text, or a router's link, which the tag styles like its own.
  leading: An icon before the label, hidden from assistive technologies.
  remove: The remove button's icon. The button and its name stay.
events:
  remove: MouseEvent. Fires when the remove button is pressed. Never fires while disabled or readonly, and the tag doesn't remove itself.
states:
  link: ":has(> [data-slot=label] a)"
  removable: ":has(> [data-slot=remove])"
  hover: "[data-slot=label] a[href]:hover, [data-slot=remove]:hover, in @media (hover: hover)"
  focus: "[data-slot=label] a:focus-visible, drawn around the tag by its ::after, and [data-slot=remove]:focus-visible"
  disabled: "[data-disabled]"
  read-only: "[data-readonly]"
keyboard:
  Tab: Moves focus to the tag's link, then to its remove button. A plain or disabled tag isn't a stop.
  Enter: Follows the link, or fires remove on the remove button.
  Space: Fires remove on the remove button.
parts: [leading, label, remove] # data-slot, in this order. The label holds the link, when there's one
tokens:
  theme: ["--color--neutral-text", "--color--focus"]
  variables: [--tag--height, --tag--padding, --tag--gap, --tag--radius, --tag--font-size, --tag--background, --tag--color, --tag--border-color, --tag--icon--size, --tag--link--border-color, --tag--hover--background]
  layer: components # in Tailwind's order: theme, base, components, utilities
rules:
  - id: tag/sizes
    layer: ui
    level: should
    scope: component
    requirement: "It takes the Input's five size names, each as tall as the Select's tag at that size"
    basis: "Open Components"
    check: "Unit test, Visual regression test"
  - id: tag/target-size
    layer: ui
    level: must
    scope: component
    requirement: "The remove button and a link label each take a press over at least 24 × 24px, and at least 44px tall on a touch screen"
    basis: "WCAG 2.5.8 (AA), 2.5.5 (AAA)"
    check: "Emulation"
  - id: tag/contrast
    layer: ui
    level: must
    scope: both
    requirement: "The label has at least 4.5:1 against the tag's background, and the focus ring at least 3:1 against what's behind it"
    basis: "WCAG 1.4.3, 1.4.11 (AA)"
    check: "Contrast checker"
  - id: tag/focus-visible
    layer: ui
    level: must
    scope: component
    requirement: "The link and the remove button show an outline at least 2px thick on `:focus-visible`"
    basis: "WCAG 2.4.7 (AA)"
    check: "Keyboard"
  - id: tag/resizable
    layer: ui
    level: must
    scope: component
    requirement: "The tag is sized with a `min-height` in `rem`, so it grows with its text"
    basis: "Open Components, beyond WCAG 1.4.4 (AA)"
    check: "200% zoom"
  - id: tag/decorative-icons
    layer: ui
    level: must
    scope: component
    requirement: "The leading icon and the remove button's icon are hidden from assistive technologies"
    basis: "WCAG 1.1.1 (A)"
    check: "Unit test, ARIA snapshot"
  - id: tag/link-distinct
    layer: ui
    level: should
    scope: component
    requirement: "A link tag looks different from a plain one by more than its colour, like a border"
    basis: "Open Components, beyond WCAG 1.4.1 (A)"
    check: "Visual regression test"
  - id: tag/truncate
    layer: ui
    level: should
    scope: component
    requirement: "A long label stays on one line and ends in an ellipsis, while its full text stays in the DOM"
    basis: "Open Components"
    check: "Unit test, Visual regression test"
  - id: tag/stable-size
    layer: ui
    level: should
    scope: component
    requirement: "Hover, focus and being a link don't change the tag's size or position"
    basis: "Open Components"
    check: "Visual regression test"
  - id: tag/tokens
    layer: ui
    level: must
    scope: component
    requirement: "Each part sets its look through `--tag--…` token paths in the `components` layer"
    basis: "Open Components"
    check: "Stylelint"
  - id: tag/forced-colors
    layer: ui
    level: must
    scope: component
    requirement: "The tag's boundary, its focus ring and its disabled state stay visible in forced colours mode"
    basis: "Open Components"
    check: "Emulation"
  - id: tag/plain-text
    layer: ux
    level: must
    scope: component
    requirement: "A tag without a link or a remove button has no role and isn't focusable"
    basis: "WAI-ARIA"
    check: "Unit test, ARIA snapshot"
  - id: tag/link
    layer: ux
    level: must
    scope: component
    requirement: "With `href`, the label holds an `<a href>`"
    basis: "HTML"
    check: "Unit test"
  - id: tag/links-and-routers
    layer: ux
    level: should
    scope: component
    requirement: "A link in the default slot, like a router's, sits in the label, takes the label's look and hit area, and keeps its own attributes"
    basis: "Open Components"
    check: "Unit test, Visual regression test"
  - id: tag/remove-button
    layer: ux
    level: must
    scope: component
    requirement: "Removal is a native `<button type=\"button\">`, which `Enter` and `Space` press"
    basis: "HTML, WCAG 2.1.1 (A)"
    check: "Unit test, Keyboard"
  - id: tag/remove-name
    layer: ux
    level: must
    scope: both
    requirement: "The remove button's name includes the tag's text, like \"Remove Bug\""
    basis: "WCAG 2.4.6 (AA), 4.1.2 (A)"
    check: "Unit test, axe `button-name`"
  - id: tag/no-nesting
    layer: ux
    level: must
    scope: component
    requirement: "The link and the remove button are siblings, with neither inside the other"
    basis: "HTML"
    check: "Unit test, axe `nested-interactive`"
  - id: tag/disabled
    layer: ux
    level: must
    scope: component
    requirement: "`disabled` drops the remove button, and a link loses its `href` and takes `aria-disabled=\"true\"`"
    basis: "HTML, WAI-ARIA"
    check: "Unit test"
  - id: tag/read-only
    layer: ux
    level: must
    scope: component
    requirement: "`readonly` drops the remove button, and a link still works"
    basis: "Open Components"
    check: "Unit test"
  - id: tag/not-status
    layer: ux
    level: should
    scope: usage
    requirement: "A status or a count, like \"Draft\" or \"3\", is a Badge rather than a tag"
    basis: "Open Components"
    check: "Review"
  - id: tag/not-selectable
    layer: ux
    level: should
    scope: usage
    requirement: "A tag that toggles is a toggle button, and picking from a set is a Checkbox Group"
    basis: "Open Components"
    check: "Review"
  - id: tag/short-labels
    layer: ux
    level: should
    scope: usage
    requirement: "Labels are a word or two"
    basis: "Open Components"
    check: "Review"
  - id: tag/mirrors-in-rtl
    layer: ux
    level: should
    scope: both
    requirement: "It uses logical properties, so the remove button sits at the end edge in right-to-left layouts too"
    basis: "Open Components"
    check: "Review"
  - id: tag/shared-vocabulary
    layer: dx
    level: must
    scope: component
    requirement: "It takes `label`, `size`, `href`, `disabled` and `leading` like the Button, `readonly` and `removeLabel` like the Select, and names its own prop and event after what they do: `removable` and `remove`"
    basis: "Open Components"
    check: "Review"
  - id: tag/typed
    layer: dx
    level: must
    scope: component
    requirement: "Props, slots and the event are typed, with unions rather than `string`, so `size` only takes the five sizes"
    basis: "Open Components"
    check: "Type check"
  - id: tag/stateless
    layer: dx
    level: should
    scope: component
    requirement: "`remove` fires, and the tag stays until whatever owns it takes it away"
    basis: "Open Components"
    check: "Unit test"
  - id: tag/root-element
    layer: dx
    level: must
    scope: component
    requirement: "The root element is the tag itself, so attributes, listeners and refs land on it"
    basis: "Open Components"
    check: "Unit test"
  - id: tag/slots
    layer: dx
    level: should
    scope: component
    requirement: "Every part has a slot or tokens, and the remove button keeps its name when its icon is slotted"
    basis: "Open Components"
    check: "Unit test"
  - id: tag/dev-warnings
    layer: dx
    level: should
    scope: component
    requirement: "It warns in development when a tag is removable without a `label`"
    basis: "Open Components"
    check: "Unit test"
  - id: tag/role-and-name
    layer: ax
    level: must
    scope: component
    requirement: "It can be found by its parts' roles and names, with `getByRole(\"button\", { name: \"Remove Bug\" })` and, as a link, `getByRole(\"link\", { name: \"Bug\" })`"
    basis: "WCAG 4.1.2 (A)"
    check: "Unit test"
  - id: tag/deterministic-dom
    layer: ax
    level: must
    scope: component
    requirement: "The same props render the same markup, with no generated IDs. The remove button renders only when `removable` is set and the tag is neither disabled nor read-only, and the link only with `href` or a slotted link, while every other state changes attributes, never elements"
    basis: "Open Components"
    check: "Unit test"
  - id: tag/data-attributes
    layer: ax
    level: should
    scope: component
    requirement: "`size`, `disabled` and `readonly` are mirrored as `data-*`, and parts carry `data-slot`: `leading`, `label` and `remove`"
    basis: "Open Components"
    check: "Unit test"
