Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions design-log/147 - jay-html validation rules catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,21 @@ Package: `@jay-framework/a11y-validator` (monorepo, dev dependency)
| Rule | Severity | Element | WCAG | What it checks |
| ------------------------------ | -------- | ----------------------------------- | ----- | ------------------------------------------------------------------------------------- |
| Image missing alt | error | `<img>` | 1.1.1 | No `alt` attribute |
| Form input without label | error | `<input>`, `<select>`, `<textarea>` | 1.3.1 | No `<label for>`, no wrapping `<label>`, no `aria-label`/`aria-labelledby` |
| Form input without label | error | `<input>`, `<select>`, `<textarea>` | 1.3.1 | No `<label for>`, no wrapping `<label>`, no usable `aria-label`/`aria-labelledby` |
| Empty `aria-label` | error | labelable controls | 4.1.2 | `aria-label` present but empty/whitespace (DL#163) |
| Broken `aria-labelledby` | error | labelable controls | 1.3.1 | Empty `aria-labelledby` or id token(s) missing in file (DL#163) |
| Duplicate `id` | error | any | 4.1.1 | Same `id` value used more than once in the file (DL#163) |
| Button without accessible name | error | `<button>` | 4.1.2 | No text, no `aria-label`, no `aria-labelledby`, no child `<img alt>` |
| Media autoplay without muted | error | `<video>`, `<audio>` | 1.4.2 | `autoplay` attribute present without `muted` |
| Invalid ARIA role | error | any | 4.1.2 | `role` attribute value not in WAI-ARIA role list |
| Viewport disables zoom | error | `<meta>` | 1.4.4 | `user-scalable=no` or `maximum-scale` < 2 in viewport meta (via `ctx.head`) |
| Positive tabindex | warning | interactive + `[role]` | 2.4.3 | `tabindex` > 0 disrupts natural tab order |
| Focusable without role | warning | non-interactive | 4.1.2 | `<div tabindex="0">` or similar without `role` — screen readers don't know what it is |
| Multiple controls in `<label>` | warning | `<label>` | 1.3.1 | More than one labelable control nested in one label (DL#163) |
| Orphan `label[for]` | warning | `<label>` | 1.3.1 | `for` points to an `id` that does not exist in the file (DL#163) |
| Adjacent duplicate text | warning | any | — | Adjacent siblings with identical visible text (screen readers announce twice) |

The form label rule skips `type="hidden"`, `type="submit"`, `type="button"`, and `type="reset"` inputs.
The form label rule skips `type="hidden"`, `type="submit"`, `type="button"`, and `type="reset"` inputs. Labelable inputs include `checkbox` and `radio` (DL#163). Empty `aria-label` / unresolved `aria-labelledby` do not count as an accessible name.

## Rule Overlap

Expand Down
166 changes: 166 additions & 0 deletions design-log/163 - a11y form and label validation rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Design Log #163 — A11y Form and Label Validation Rules

## Background

`@jay-framework/a11y-validator` (DL#145, DL#147) already checks that text-like inputs, selects, and textareas have an associated label via `<label for>`, wrapping `<label>`, `aria-label`, or `aria-labelledby`.

Gaps remain for common form/accessibility mistakes that AI agents and designers make in jay-html:

- `checkbox` / `radio` are skipped by `LABELABLE_INPUTS`
- empty `aria-label=""` and unresolved `aria-labelledby` pass as “labeled”
- multiple form controls inside one `<label>` are accepted
- duplicate `id` values are not flagged
- orphan `<label for="...">` pointing at a missing `id` is not flagged

## Problem

Static validation under-reports WCAG 1.3.1 / 4.1.2 form naming issues. Agents self-correct only when findings include actionable `suggestion` text.

## Questions and Answers

### Q1: One PR or several?

**A:** One PR for the whole form/label scope. Nesting of interactive elements, `lang`, and runtime axe stay out of scope.

### Q2: Severity?

**A:**

| Finding | Severity |
| ------- | -------- |
| Missing label (incl. checkbox/radio) | error |
| Empty `aria-label` | error |
| `aria-labelledby` with missing/empty id refs | error |
| Duplicate `id` | error |
| Multiple labelable controls in one `<label>` | warning |
| Orphan `label[for]` (no matching `id`) | warning |

### Q3: Does wrapping `<label>` still count for checkbox/radio?

**A:** Yes — same association rules as other inputs. Explicit `for`/`id` preferred when multiple controls share a visual group; `fieldset`/`legend` is not required in this PR.

### Q4: How does `aria-labelledby` resolve?

**A:** Split on whitespace; every token must match an element `id` in the same jay-html file. Missing any token → error. Empty attribute → error.

### Q5: Version bump in the PR?

**A:** No. Maintainers bump `@jay-framework/a11y-validator` on release (currently `0.22.2`).

## Design

### Rules (additions / tightenings)

1. **Extend labelable inputs** — add `checkbox`, `radio` to `LABELABLE_INPUTS`. Still skip `hidden`, `submit`, `button`, `reset`.

2. **Empty `aria-label`** — if attribute is present and `trim()` is empty → error (do not treat as labeled).

3. **Broken `aria-labelledby`** — if present: empty / only whitespace → error; any id token not found in the document → error. If all ids resolve, treat as labeled (skip “no label” finding).

4. **Multiple controls in one `<label>`** — count labelable descendants (`input` except ignored types, `select`, `textarea`). If count > 1 → warning on `<label>`.

5. **Duplicate `id`** — collect all `id` attributes; any value used more than once → error per duplicate occurrence after the first (or one finding listing the id).

6. **Orphan `label[for]`** — if `for` is non-empty and no element has that `id` → warning.

### Implementation approach

Single pre-pass over the DOM:

- `Set` / `Map` of all `id` → count
- `Set` of existing ids for ARIA/`for` lookup
- for each `<label>`, count labelable descendants and check `for`

Then existing `walkElements` + tightened `checkLabel`.

### Examples

✅ Good:

```html
<label for="email">Email</label>
<input type="email" id="email" />

<label><input type="checkbox" /> Agree</label>

<input type="radio" id="a" aria-labelledby="opt-a" />
<span id="opt-a">Option A</span>
```

❌ Bad:

```html
<input type="checkbox" />
<input aria-label="" />
<input aria-labelledby="missing" />
<label>From <input type="date" /> To <input type="date" /></label>
<div id="x"></div>
<span id="x"></span>
<label for="nope">Name</label>
```

## Implementation Plan

### Phase 1: Design log + index

1. This document + `design-log/index.md` entry under validation/plugins.

### Phase 2: Tests

2. Vitest cases for each rule (pass + fail). No `toContain` on code files.

### Phase 3: Implementation

3. Update `a11y-validator.ts` helpers and `validate`.
4. Run package tests.

### Phase 4: Catalog + results

5. Append new rows to DL#147 a11y table.
6. Append Implementation Results here.

## Verification Criteria

1. checkbox/radio without association → error
2. `aria-label=""` → error; non-empty → passes label check
3. `aria-labelledby` missing target → error
4. two inputs in one label → warning
5. duplicate ids → error
6. `label for` without matching id → warning
7. Existing label/`alt`/button/tabindex tests still pass

## Trade-offs

| Decision | Benefit | Cost |
| -------- | ------- | ---- |
| Per-file id uniqueness only | Matches jay-html validation model | Won't catch cross-file collisions |
| No fieldset/legend rule yet | Keeps PR focused | Radio groups still weak without legend |
| Warning for multi-control label | Avoids hard break on legacy templates | Agents may ignore warnings |

## Out of Scope

- Nested interactive (`<a>` in `<a>`)
- `<html lang>`
- Runtime axe / focus / toast timing
- Color-only / contrast (design-system-validator)

## Implementation Results

### Phase 2–3: Tests + code

- Extended `LABELABLE_INPUTS` with `checkbox` / `radio`
- Tightened `checkLabel` for empty `aria-label` and resolved `aria-labelledby` tokens against file ids
- Pre-pass: duplicate `id` counts, orphan `label[for]`, multi-control `<label>`
- Tests added in `a11y-validator.test.ts`

### Test results

`packages/plugins/a11y-validator`: **54/54 passing**

### Catalog

Updated DL#147 a11y rules table with the new rows.

### Deviations from design

None material. Multi-control warning message wording uses “contains N form controls” (same intent as designed).
2 changes: 2 additions & 0 deletions design-log/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ Quick reference to find relevant design logs by topic. Design logs capture desig
| 160 | [deprecate editor packages](160%20-%20deprecate%20editor%20packages) | Move editor-client/protocol/server to \_deprecated; remove from stack-cli |
| 161 | [markdown image url resolution](161%20-%20markdown%20image%20url%20resolution) | Rewrite relative image URLs in markdown; copy media to public; CDN mapping |
| 162 | [structural headfull components](162%20-%20structural%20headfull%20components) | Allow headfull components without .ts code file; passthrough from contract |
| 163 | [a11y form and label validation rules](163%20-%20a11y%20form%20and%20label%20validation%20rules) | Extend a11y-validator: checkbox/radio, ARIA name integrity, duplicate ids, label hygiene |

---

Expand Down Expand Up @@ -164,6 +165,7 @@ Quick reference to find relevant design logs by topic. Design logs capture desig
| 153 | [npm create jay](153%20-%20npm%20create%20jay) | Interactive project scaffolding: name, plugin selection, agent-kit, setup banner |
| 158 | [staged npm publish](158%20-%20staged%20npm%20publish) | Two-phase publish: stage all packages without OTP, then bulk-approve with single OTP |
| 147 | [jay-html validation rules catalog](147%20-%20jay-html%20validation%20rules%20catalog) | Complete catalog of all validation rules across wix-media, SEO, and a11y |
| 163 | [a11y form and label validation rules](163%20-%20a11y%20form%20and%20label%20validation%20rules) | Extend a11y-validator: checkbox/radio, ARIA name integrity, duplicate ids, label hygiene |

---

Expand Down
Loading