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

component: Tooltip
summary: Shows a short text hint next to its trigger, on hover and on keyboard focus. Text only, and never the only place its text is.
standard: https://opencomponents.dev/docs/components/tooltip
element: span # popover="manual", right after its trigger, and in the top layer while it's open
role: tooltip # with type="description". With type="label", it's aria-hidden, with no role
props:
  label: { type: string } # required, and plain text only
  type: { type: [label, description], default: description }
  delay: { type: number, default: 500 } # in milliseconds, for the pointer only
  placement: { type: [top, bottom, start, end], default: top }
  arrow: { type: boolean, default: true }
slots:
  default: The trigger, which binds the props it's given, like aria-describedby and the listeners, on its focusable element.
states:
  closed: "[data-state=closed]"
  open: "[data-state=open]"
keyboard:
  Tab: Focus on the trigger opens it at once, and focus leaving closes it.
  Escape: Closes it while it's open, keeping focus on the trigger, and stops there.
parts: [arrow] # data-slot, after the text, unless arrow is false
tokens:
  theme: ["--color--neutral", "--color--neutral-contrast"]
  variables: [--tooltip--background, --tooltip--color, --tooltip--font-size, --tooltip--padding, --tooltip--radius, --tooltip--max-width, --tooltip--offset, --tooltip--arrow--size]
  layer: components # in Tailwind's order: theme, base, components, utilities
rules:
  - id: tooltip/text-only
    layer: ui
    level: must
    scope: component
    requirement: "It holds plain text, with no links, buttons or anything else to focus or click"
    basis: "Open Components"
    check: "Type check"
  - id: tooltip/contrast
    layer: ui
    level: must
    scope: both
    requirement: "Its text has a contrast of 4.5:1 against its fill"
    basis: "WCAG 1.4.3 (AA)"
    check: "Contrast checker"
  - id: tooltip/wraps
    layer: ui
    level: should
    scope: component
    requirement: "Long text wraps at a maximum width set in `ch`, and is never cut off or scrolled"
    basis: "WCAG 1.4.4, 1.4.10 (AA)"
    check: "200% zoom"
  - id: tooltip/top-layer
    layer: ui
    level: must
    scope: component
    requirement: "It opens in the top layer, so no `overflow` or `z-index` around its trigger clips or covers it"
    basis: "HTML"
    check: "Visual regression test"
  - id: tooltip/flips
    layer: ui
    level: must
    scope: component
    requirement: "It opens beside its trigger, flips to the opposite side when there's no room and stays inside the viewport"
    basis: "Open Components"
    check: "Visual regression test"
  - id: tooltip/points-at-trigger
    layer: ui
    level: must
    scope: component
    requirement: "An arrow, when there is one, sits on the edge facing the trigger and points at its centre, or as close as the rounded corners allow, after a flip too"
    basis: "Open Components"
    check: "Visual regression test"
  - id: tooltip/fallback-position
    layer: ui
    level: should
    scope: component
    requirement: "Browsers without CSS anchor positioning get the same placement from a script"
    basis: "Open Components"
    check: "Visual regression test"
  - id: tooltip/reduced-motion
    layer: ui
    level: must
    scope: component
    requirement: "With `prefers-reduced-motion: reduce`, it fades in without growing or moving"
    basis: "WCAG 2.3.3 (AAA), Open Components"
    check: "Emulation"
  - id: tooltip/forced-colors
    layer: ui
    level: must
    scope: component
    requirement: "In forced colours mode, a border in the system's text colour marks its edge"
    basis: "Open Components"
    check: "Emulation"
  - id: tooltip/focus-opens
    layer: ux
    level: must
    scope: component
    requirement: "Keyboard focus on its trigger opens it at once"
    basis: "WCAG 2.1.1 (A)"
    check: "Unit test, keyboard"
  - id: tooltip/hover-delay
    layer: ux
    level: should
    scope: component
    requirement: "The pointer rests on its trigger for a delay, about 500ms, before it opens"
    basis: "Open Components"
    check: "Unit test"
  - id: tooltip/skip-delay
    layer: ux
    level: should
    scope: component
    requirement: "Moving the pointer straight on to the next trigger opens that tooltip without waiting"
    basis: "Open Components"
    check: "Unit test"
  - id: tooltip/hoverable
    layer: ux
    level: must
    scope: component
    requirement: "The pointer can cross from the trigger onto the tooltip, within a short grace period, without closing it"
    basis: "WCAG 1.4.13 (AA)"
    check: "Unit test"
  - id: tooltip/persistent
    layer: ux
    level: must
    scope: component
    requirement: "It stays open while its trigger has hover or focus, and never hides on a timer"
    basis: "WCAG 1.4.13 (AA)"
    check: "Unit test"
  - id: tooltip/dismiss-with-escape
    layer: ux
    level: must
    scope: component
    requirement: "`Escape` dismisses it without moving focus or the pointer"
    basis: "WCAG 1.4.13 (AA)"
    check: "Unit test, keyboard"
  - id: tooltip/contain-escape
    layer: ux
    level: must
    scope: component
    requirement: "A key press that closes it stops there, so a dialog or popover around it stays open, while one with no tooltip open reaches them as usual"
    basis: "Open Components"
    check: "Unit test"
  - id: tooltip/one-at-a-time
    layer: ux
    level: must
    scope: component
    requirement: "Opening a tooltip closes any other one that's open"
    basis: "Open Components"
    check: "Unit test"
  - id: tooltip/no-touch
    layer: ux
    level: must
    scope: component
    requirement: "A tap never opens it, even when the tap moves focus to its trigger"
    basis: "Open Components"
    check: "Unit test"
  - id: tooltip/not-the-only-place
    layer: ux
    level: must
    scope: usage
    requirement: "Nothing people need is only in a tooltip, since touch screen users can't open one"
    basis: "Open Components"
    check: "Review"
  - id: tooltip/focusable-trigger
    layer: ux
    level: must
    scope: usage
    requirement: "Its trigger is focusable, and a disabled one uses `aria-disabled` rather than `disabled`"
    basis: "Open Components, beyond WCAG 2.1.1 (A)"
    check: "Keyboard"
  - id: tooltip/short
    layer: ux
    level: should
    scope: usage
    requirement: "Its text is a few words long, a hint rather than instructions"
    basis: "Open Components"
    check: "Review"
  - id: tooltip/no-title
    layer: ux
    level: should
    scope: usage
    requirement: "Its trigger has no `title` attribute, which would show a second, browser-drawn tooltip"
    basis: "Open Components"
    check: "Review"
  - id: tooltip/trigger-props
    layer: dx
    level: must
    scope: component
    requirement: "The trigger is the element you render, which binds the props the Tooltip hands it, with no wrapper element"
    basis: "Open Components"
    check: "Unit test"
  - id: tooltip/label-prop
    layer: dx
    level: should
    scope: component
    requirement: "Its text comes from `label`, the name every component uses for plain text"
    basis: "Open Components"
    check: "Type check"
  - id: tooltip/attributes-on-tooltip
    layer: dx
    level: should
    scope: component
    requirement: "Classes and attributes you pass land on the tooltip, so a class can set its variables"
    basis: "Open Components"
    check: "Unit test"
  - id: tooltip/describes
    layer: ax
    level: must
    scope: component
    requirement: "With `type=\"description\"`, it's `role=\"tooltip\"`, and its trigger points to its `id` with `aria-describedby`"
    basis: "WAI-ARIA"
    check: "Unit test, screen reader"
  - id: tooltip/shows-label
    layer: ax
    level: must
    scope: component
    requirement: "With `type=\"label\"`, it's `aria-hidden`, nothing points to it, and the trigger keeps its own name"
    basis: "WCAG 4.1.2 (A), Open Components"
    check: "Unit test"
  - id: tooltip/never-labelledby
    layer: ax
    level: must
    scope: both
    requirement: "No trigger is named by its tooltip through `aria-labelledby`"
    basis: "Open Components"
    check: "Unit test, review"
  - id: tooltip/named-trigger
    layer: ax
    level: must
    scope: usage
    requirement: "Its trigger has an accessible name of its own, like an icon-only Button's hidden label"
    basis: "WCAG 4.1.2 (A)"
    check: "ARIA snapshot"
  - id: tooltip/in-the-dom
    layer: ax
    level: must
    scope: component
    requirement: "It stays in the DOM while it's closed, so its trigger's description is always there"
    basis: "WAI-ARIA"
    check: "Unit test"
  - id: tooltip/data-state
    layer: ax
    level: should
    scope: component
    requirement: "Its open state shows as `data-state`, `open` or `closed`, so tests and agents can read it"
    basis: "Open Components"
    check: "Unit test"
