codebyplan 1.5.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -5
- package/dist/cli.js +4578 -2709
- package/package.json +5 -1
- package/templates/.gitkeep +0 -0
- package/templates/README.md +20 -0
- package/templates/agents/cbp-cc-executor.md +213 -0
- package/templates/agents/cbp-database-agent.md +229 -0
- package/templates/agents/cbp-improve-claude.md +245 -0
- package/templates/agents/cbp-improve-round.md +284 -0
- package/templates/agents/cbp-mechanical-edits.md +111 -0
- package/templates/agents/cbp-research.md +282 -0
- package/templates/agents/cbp-round-executor.md +604 -0
- package/templates/agents/cbp-security-agent.md +134 -0
- package/templates/agents/cbp-task-check.md +213 -0
- package/templates/agents/cbp-task-planner.md +582 -0
- package/templates/agents/cbp-test-e2e-agent.md +363 -0
- package/templates/agents/cbp-testing-qa-agent.md +400 -0
- package/templates/context/mcp-docs.md +139 -0
- package/templates/hooks/README.md +236 -0
- package/templates/hooks/cbp-auto-test-hooks.sh +44 -0
- package/templates/hooks/cbp-lint-format-on-edit.sh +159 -0
- package/templates/hooks/cbp-maestro-yaml-validate.sh +100 -0
- package/templates/hooks/cbp-mcp-migration-guard.sh +32 -0
- package/templates/hooks/cbp-mcp-round-sync.sh +79 -0
- package/templates/hooks/cbp-mcp-worktree-inject.sh +76 -0
- package/templates/hooks/cbp-notify.sh +68 -0
- package/templates/hooks/cbp-plugin-dispatch.sh +29 -0
- package/templates/hooks/cbp-pre-commit-quality-gate.sh +204 -0
- package/templates/hooks/cbp-statusline.sh +347 -0
- package/templates/hooks/cbp-subagent-statusline.sh +182 -0
- package/templates/hooks/cbp-test-coverage-gate.sh +144 -0
- package/templates/hooks/cbp-test-hooks.sh +320 -0
- package/templates/hooks/hooks.json +85 -0
- package/templates/hooks/validate-context-usage.sh +59 -0
- package/templates/hooks/validate-git-commit.sh +78 -0
- package/templates/hooks/validate-git-stash-deny.sh +32 -0
- package/templates/hooks/validate-structure-lengths.sh +57 -0
- package/templates/hooks/validate-structure-lib.sh +104 -0
- package/templates/hooks/validate-structure-patterns.sh +54 -0
- package/templates/hooks/validate-structure-scope.sh +33 -0
- package/templates/hooks/validate-structure-smoke.sh +95 -0
- package/templates/hooks/validate-structure-templates.sh +34 -0
- package/templates/hooks/validate-structure.sh +69 -0
- package/templates/rules/.gitkeep +0 -0
- package/templates/rules/README.md +47 -0
- package/templates/rules/context-file-loading.md +52 -0
- package/templates/rules/scope-vocabulary.md +64 -0
- package/templates/rules/todo-backend.md +109 -0
- package/templates/settings.project.base.json +55 -0
- package/templates/settings.user.base.json +25 -0
- package/templates/skills/cbp-build-cc-agent/SKILL.md +139 -0
- package/templates/skills/cbp-build-cc-agent/examples/read-only-reviewer.md +32 -0
- package/templates/skills/cbp-build-cc-agent/examples/with-hooks.md +41 -0
- package/templates/skills/cbp-build-cc-agent/examples/with-skills-preload.md +25 -0
- package/templates/skills/cbp-build-cc-agent/reference/cbp-quality.md +153 -0
- package/templates/skills/cbp-build-cc-agent/reference/frontmatter-fields.md +37 -0
- package/templates/skills/cbp-build-cc-agent/reference/permission-modes.md +18 -0
- package/templates/skills/cbp-build-cc-agent/scripts/validate-agent.sh +67 -0
- package/templates/skills/cbp-build-cc-agent/templates/agent.md +66 -0
- package/templates/skills/cbp-build-cc-claude-file/SKILL.md +178 -0
- package/templates/skills/cbp-build-cc-claude-file/examples/minimal-project.md +33 -0
- package/templates/skills/cbp-build-cc-claude-file/examples/monorepo-with-imports.md +39 -0
- package/templates/skills/cbp-build-cc-claude-file/reference/imports.md +72 -0
- package/templates/skills/cbp-build-cc-claude-file/reference/what-belongs.md +39 -0
- package/templates/skills/cbp-build-cc-claude-file/templates/project-claude-md.md +48 -0
- package/templates/skills/cbp-build-cc-claude-file/templates/user-claude-md.md +22 -0
- package/templates/skills/cbp-build-cc-memory/SKILL.md +201 -0
- package/templates/skills/cbp-build-cc-memory/examples/feedback-memory.md +11 -0
- package/templates/skills/cbp-build-cc-memory/examples/project-memory.md +11 -0
- package/templates/skills/cbp-build-cc-memory/examples/reference-memory.md +13 -0
- package/templates/skills/cbp-build-cc-memory/examples/user-memory.md +14 -0
- package/templates/skills/cbp-build-cc-memory/reference/memory-types.md +59 -0
- package/templates/skills/cbp-build-cc-memory/reference/when-to-save.md +62 -0
- package/templates/skills/cbp-build-cc-memory/templates/MEMORY-index.md +4 -0
- package/templates/skills/cbp-build-cc-memory/templates/memory-entry.md +15 -0
- package/templates/skills/cbp-build-cc-mode/SKILL.md +99 -0
- package/templates/skills/cbp-build-cc-rule/SKILL.md +176 -0
- package/templates/skills/cbp-build-cc-rule/examples/global-rule.md +19 -0
- package/templates/skills/cbp-build-cc-rule/examples/scoped-rule.md +41 -0
- package/templates/skills/cbp-build-cc-rule/reference/paths-patterns.md +48 -0
- package/templates/skills/cbp-build-cc-rule/templates/rule.md +32 -0
- package/templates/skills/cbp-build-cc-settings/SKILL.md +220 -0
- package/templates/skills/cbp-build-cc-settings/examples/hooks-config.json +64 -0
- package/templates/skills/cbp-build-cc-settings/examples/permissions-config.json +34 -0
- package/templates/skills/cbp-build-cc-settings/examples/sandbox-config.json +42 -0
- package/templates/skills/cbp-build-cc-settings/reference/cbp-conventions.md +104 -0
- package/templates/skills/cbp-build-cc-settings/reference/permission-rules.md +61 -0
- package/templates/skills/cbp-build-cc-settings/reference/scope-precedence.md +73 -0
- package/templates/skills/cbp-build-cc-settings/reference/settings-fields.md +166 -0
- package/templates/skills/cbp-build-cc-settings/templates/settings.json +23 -0
- package/templates/skills/cbp-build-cc-settings/templates/settings.local.json +10 -0
- package/templates/skills/cbp-build-cc-skill/SKILL.md +154 -0
- package/templates/skills/cbp-build-cc-skill/examples/dynamic-context.md +31 -0
- package/templates/skills/cbp-build-cc-skill/examples/fork-skill.md +22 -0
- package/templates/skills/cbp-build-cc-skill/examples/knowledge-skill.md +25 -0
- package/templates/skills/cbp-build-cc-skill/examples/task-skill.md +29 -0
- package/templates/skills/cbp-build-cc-skill/reference/cbp-quality.md +157 -0
- package/templates/skills/cbp-build-cc-skill/reference/frontmatter-fields.md +35 -0
- package/templates/skills/cbp-build-cc-skill/reference/string-substitutions.md +60 -0
- package/templates/skills/cbp-build-cc-skill/scripts/validate-skill.sh +90 -0
- package/templates/skills/cbp-build-cc-skill/templates/skill.md +51 -0
- package/templates/skills/cbp-checkpoint-check/SKILL.md +156 -0
- package/templates/skills/cbp-checkpoint-complete/SKILL.md +109 -0
- package/templates/skills/cbp-checkpoint-create/SKILL.md +287 -0
- package/templates/skills/cbp-checkpoint-end/SKILL.md +241 -0
- package/templates/skills/cbp-checkpoint-update/SKILL.md +115 -0
- package/templates/skills/cbp-frontend-a11y/SKILL.md +109 -0
- package/templates/skills/cbp-frontend-a11y/reference/aria-roles-states.md +130 -0
- package/templates/skills/cbp-frontend-a11y/reference/contrast-visual.md +122 -0
- package/templates/skills/cbp-frontend-a11y/reference/keyboard-patterns.md +154 -0
- package/templates/skills/cbp-frontend-a11y/reference/semantic-html.md +111 -0
- package/templates/skills/cbp-frontend-design/SKILL.md +145 -0
- package/templates/skills/cbp-frontend-design/reference/nextjs-scss.md +118 -0
- package/templates/skills/cbp-frontend-design/reference/rn-expo.md +101 -0
- package/templates/skills/cbp-frontend-design/reference/tauri-react.md +82 -0
- package/templates/skills/cbp-frontend-ui/SKILL.md +262 -0
- package/templates/skills/cbp-frontend-ui/reference/ui-label-maps.md +42 -0
- package/templates/skills/cbp-frontend-ui/reference/ui-layout-patterns.md +105 -0
- package/templates/skills/cbp-frontend-ui/reference/variant-defaults.md +149 -0
- package/templates/skills/cbp-frontend-ux/SKILL.md +181 -0
- package/templates/skills/cbp-git-branch-feat-create/SKILL.md +115 -0
- package/templates/skills/cbp-git-commit/SKILL.md +278 -0
- package/templates/skills/cbp-git-worktree-create/SKILL.md +226 -0
- package/templates/skills/cbp-git-worktree-remove/SKILL.md +145 -0
- package/templates/skills/cbp-merge-main/SKILL.md +228 -0
- package/templates/skills/cbp-round-check/SKILL.md +104 -0
- package/templates/skills/cbp-round-end/SKILL.md +183 -0
- package/templates/skills/cbp-round-end/reference/findings-presentation.md +44 -0
- package/templates/skills/cbp-round-end/reference/inline-fallback.md +35 -0
- package/templates/skills/cbp-round-execute/SKILL.md +211 -0
- package/templates/skills/cbp-round-execute/reference/inline-fallback.md +59 -0
- package/templates/skills/cbp-round-input/SKILL.md +165 -0
- package/templates/skills/cbp-round-start/SKILL.md +222 -0
- package/templates/skills/cbp-round-update/SKILL.md +163 -0
- package/templates/skills/cbp-session-end/SKILL.md +187 -0
- package/templates/skills/cbp-session-start/SKILL.md +155 -0
- package/templates/skills/cbp-ship/SKILL.md +332 -0
- package/templates/skills/cbp-ship/reference/changesets-overview.md +120 -0
- package/templates/skills/cbp-ship/reference/eas-cli-overview.md +60 -0
- package/templates/skills/cbp-ship/reference/gh-cli-overview.md +135 -0
- package/templates/skills/cbp-ship/reference/gh-cli-shipment-commands.md +283 -0
- package/templates/skills/cbp-ship/reference/npm-publish-monorepo.md +252 -0
- package/templates/skills/cbp-ship/reference/npm-publish-oidc-trusted.md +157 -0
- package/templates/skills/cbp-ship/reference/npm-publish-overview.md +171 -0
- package/templates/skills/cbp-ship/reference/preflight-checklist.md +88 -0
- package/templates/skills/cbp-ship/reference/railway-nestjs-deployment.md +169 -0
- package/templates/skills/cbp-ship/reference/railway-overview.md +120 -0
- package/templates/skills/cbp-ship/reference/railway-troubleshooting.md +168 -0
- package/templates/skills/cbp-ship/reference/release-please-overview.md +99 -0
- package/templates/skills/cbp-ship/reference/surface-expo-eas.md +155 -0
- package/templates/skills/cbp-ship/reference/surface-npm.md +180 -0
- package/templates/skills/cbp-ship/reference/surface-railway.md +152 -0
- package/templates/skills/cbp-ship/reference/surface-supabase.md +178 -0
- package/templates/skills/cbp-ship/reference/surface-tauri.md +138 -0
- package/templates/skills/cbp-ship/reference/surface-vercel.md +124 -0
- package/templates/skills/cbp-ship/reference/surface-vscode-ext.md +144 -0
- package/templates/skills/cbp-ship/reference/surfaces.md +60 -0
- package/templates/skills/cbp-ship/reference/testflight-automation.md +215 -0
- package/templates/skills/cbp-ship/reference/testflight-internal-vs-external.md +69 -0
- package/templates/skills/cbp-ship/reference/testflight-overview.md +98 -0
- package/templates/skills/cbp-ship/reference/versioning.md +116 -0
- package/templates/skills/cbp-ship/scripts/detect-surfaces.sh +217 -0
- package/templates/skills/cbp-ship/scripts/verify-expo-eas.sh +35 -0
- package/templates/skills/cbp-ship/scripts/verify-npm.sh +21 -0
- package/templates/skills/cbp-ship/scripts/verify-railway.sh +41 -0
- package/templates/skills/cbp-ship/scripts/verify-supabase.sh +19 -0
- package/templates/skills/cbp-ship/scripts/verify-tauri.sh +24 -0
- package/templates/skills/cbp-ship/scripts/verify-vercel.sh +32 -0
- package/templates/skills/cbp-ship/scripts/verify-vscode-ext.sh +25 -0
- package/templates/skills/cbp-ship/templates/eas.json +66 -0
- package/templates/skills/cbp-ship/templates/railway.toml +15 -0
- package/templates/skills/cbp-ship/templates/release-please-config.json +17 -0
- package/templates/skills/cbp-ship/templates/vercel.json +19 -0
- package/templates/skills/cbp-ship/templates/vscodeignore +21 -0
- package/templates/skills/cbp-ship/templates/workflow-changesets.yml +41 -0
- package/templates/skills/cbp-ship/templates/workflow-eas-submit.yml +53 -0
- package/templates/skills/cbp-ship/templates/workflow-npm-publish.yml +36 -0
- package/templates/skills/cbp-ship/templates/workflow-release-please.yml +21 -0
- package/templates/skills/cbp-ship/templates/workflow-tauri-release.yml +69 -0
- package/templates/skills/cbp-ship/templates/workflow-vsce-publish.yml +31 -0
- package/templates/skills/cbp-ship-configure/SKILL.md +296 -0
- package/templates/skills/cbp-ship-configure/reference/expo-mobile.md +204 -0
- package/templates/skills/cbp-ship-configure/reference/npm-package.md +165 -0
- package/templates/skills/cbp-ship-configure/reference/railway-backend.md +199 -0
- package/templates/skills/cbp-ship-configure/reference/supabase.md +200 -0
- package/templates/skills/cbp-ship-configure/reference/tauri-desktop.md +181 -0
- package/templates/skills/cbp-ship-configure/reference/vercel.md +117 -0
- package/templates/skills/cbp-ship-configure/reference/vscode-ext.md +155 -0
- package/templates/skills/cbp-ship-main/SKILL.md +65 -0
- package/templates/skills/cbp-supabase-branch-check/SKILL.md +337 -0
- package/templates/skills/cbp-supabase-branch-check/reference/dag-steps.md +29 -0
- package/templates/skills/cbp-supabase-migrate/SKILL.md +314 -0
- package/templates/skills/cbp-supabase-migrate/reference/advisor-triage.md +70 -0
- package/templates/skills/cbp-supabase-migrate/reference/cli-fallback.md +87 -0
- package/templates/skills/cbp-supabase-migrate/reference/preflight-dry-run.md +58 -0
- package/templates/skills/cbp-supabase-setup/SKILL.md +239 -0
- package/templates/skills/cbp-supabase-setup/reference/branching-setup.md +121 -0
- package/templates/skills/cbp-supabase-setup/reference/cli-fallback.md +109 -0
- package/templates/skills/cbp-task-check/SKILL.md +166 -0
- package/templates/skills/cbp-task-complete/SKILL.md +206 -0
- package/templates/skills/cbp-task-complete/reference/checkpoint-done-branching.md +48 -0
- package/templates/skills/cbp-task-complete/reference/next-step-heuristic.md +56 -0
- package/templates/skills/cbp-task-create/SKILL.md +167 -0
- package/templates/skills/cbp-task-start/SKILL.md +239 -0
- package/templates/skills/cbp-task-testing/SKILL.md +277 -0
- package/templates/skills/cbp-todo/SKILL.md +97 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
scope: org-shared
|
|
3
|
+
name: cbp-checkpoint-update
|
|
4
|
+
description: Update checkpoint state (activate, update context, etc.)
|
|
5
|
+
argument-hint: [checkpoint-number]
|
|
6
|
+
effort: high
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Checkpoint Update Command
|
|
10
|
+
|
|
11
|
+
Update checkpoint status, context, or other fields.
|
|
12
|
+
|
|
13
|
+
## Instructions
|
|
14
|
+
|
|
15
|
+
### Step 0.5: Parse `$ARGUMENTS`
|
|
16
|
+
|
|
17
|
+
Parse the argument:
|
|
18
|
+
|
|
19
|
+
| Shape | Regex | Resolves to |
|
|
20
|
+
|-------|-------|-------------|
|
|
21
|
+
| `{chk}` (e.g. `108`) | `^[0-9]+$` | Target CHK-{chk} |
|
|
22
|
+
| _(empty)_ | — | Use MCP `get_current_task` to find the active checkpoint |
|
|
23
|
+
|
|
24
|
+
Anything else is malformed — surface this error and stop:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
checkpoint-update: invalid argument `{value}`. Expected:
|
|
28
|
+
108 → CHK-108
|
|
29
|
+
(empty) → active checkpoint
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
#### Worked examples
|
|
33
|
+
|
|
34
|
+
- `checkpoint-update 108` → CHK-108
|
|
35
|
+
- `checkpoint-update` (no arg) → active checkpoint via `get_current_task`
|
|
36
|
+
- `checkpoint-update abc` → error: malformed
|
|
37
|
+
- `checkpoint-update 108-1` → error: malformed (that is task-start's shape)
|
|
38
|
+
|
|
39
|
+
### Step 1: Get Current Checkpoint
|
|
40
|
+
|
|
41
|
+
Given the parse from Step 0.5:
|
|
42
|
+
|
|
43
|
+
| Parse | Resolution path |
|
|
44
|
+
|-------|-----------------|
|
|
45
|
+
| `{chk}` | MCP `get_checkpoints(repo_id)` → filter `number === {chk}` (must exist). |
|
|
46
|
+
| _(empty)_ | MCP `get_current_task` with repo_id (and worktree_id resolved via `npx codebyplan resolve-worktree`) to find the active checkpoint. If no active checkpoint, use MCP `get_checkpoints` filtered by `pending` status to find pending ones. |
|
|
47
|
+
|
|
48
|
+
### Step 1.5: Detect Entry Context (from `/cbp-task-complete` expand path)
|
|
49
|
+
|
|
50
|
+
When invoked with a preamble naming `Triggered from /cbp-task-complete with intent: expand`, the user just completed the last task in the checkpoint and chose Option B "Expand checkpoint with more tasks" per `task-complete/reference/checkpoint-done-branching.md`.
|
|
51
|
+
|
|
52
|
+
In that case, lead with explicit guidance:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
You're expanding [CHK-NNN]: [title]
|
|
56
|
+
The checkpoint has no remaining tasks; you chose to add more before shipping.
|
|
57
|
+
|
|
58
|
+
Recommended sequence:
|
|
59
|
+
1. (here) update context — record any new decisions / discoveries / scope clarifications driving the expansion
|
|
60
|
+
2. (next) `/cbp-task-create` — add the specific tasks the expansion calls for
|
|
61
|
+
|
|
62
|
+
You can skip the context update (Steps 2–3) if the expansion is purely additive with no scope change. Run `/cbp-task-create` directly in that case.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
When invoked WITHOUT this preamble (direct user invocation), skip the guidance and proceed normally.
|
|
66
|
+
|
|
67
|
+
### Step 2: Determine Update
|
|
68
|
+
|
|
69
|
+
Ask user via AskUserQuestion what to update:
|
|
70
|
+
- **Activate**: Change pending → active (starts development)
|
|
71
|
+
- **Update context**: Add decisions, discoveries, constraints
|
|
72
|
+
- **Update goal**: Refine the checkpoint goal
|
|
73
|
+
- **Update deadline**: Change the deadline
|
|
74
|
+
|
|
75
|
+
When the entry context is "expand from task-complete", `Update context` is the recommended default.
|
|
76
|
+
|
|
77
|
+
### Step 3: Apply Update
|
|
78
|
+
|
|
79
|
+
Use MCP `update_checkpoint` with the appropriate fields.
|
|
80
|
+
|
|
81
|
+
**For activation:**
|
|
82
|
+
```
|
|
83
|
+
update_checkpoint(checkpoint_id, status: "active")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**For context updates:**
|
|
87
|
+
Read current checkpoint to get existing context, merge new data, then:
|
|
88
|
+
```
|
|
89
|
+
update_checkpoint(checkpoint_id, context: merged_context)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Step 4: Show Result and Route
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
## Checkpoint Updated
|
|
96
|
+
|
|
97
|
+
**CHK-[NNN]**: [Title]
|
|
98
|
+
**Status**: [new status]
|
|
99
|
+
**Updated**: [what changed]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
When the entry context was "expand from task-complete", append:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
**Next:**
|
|
106
|
+
Run `/cbp-task-create` to add the new task(s) under this checkpoint.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Otherwise, no follow-up directive — the user is back in control.
|
|
110
|
+
|
|
111
|
+
## Integration
|
|
112
|
+
|
|
113
|
+
- **Reads**: MCP `get_current_task`, `get_checkpoints`
|
|
114
|
+
- **Writes**: MCP `update_checkpoint`
|
|
115
|
+
- **Triggered by**: User directly, OR `/cbp-task-complete` Step 9c (expand path) — see `task-complete/reference/checkpoint-done-branching.md`
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
scope: org-shared
|
|
3
|
+
name: cbp-frontend-a11y
|
|
4
|
+
description: Pre-implementation accessibility playbook loaded BEFORE writing UI / styling code. Produces a per-component checklist of WCAG 2.1 AA obligations from semantic HTML, ARIA roles/states, keyboard patterns, and contrast requirements.
|
|
5
|
+
effort: xhigh
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Frontend Accessibility (Pre-Implementation Playbook)
|
|
9
|
+
|
|
10
|
+
Loaded by `round-executor` Step 2.6 BEFORE any UI or styling file is written, AFTER `frontend-design` has committed to an aesthetic direction. Produces a concrete pre-write checklist — not a post-implementation audit.
|
|
11
|
+
|
|
12
|
+
## When this skill fires
|
|
13
|
+
|
|
14
|
+
Invoked when the wave's `skill_preloads[]` contains `"frontend-a11y"` (set by planner Phase 5.6 when `wave.files[]` includes UI-bearing paths). See `rules/frontend-accessibility-invocation.md` for the trigger gate.
|
|
15
|
+
|
|
16
|
+
If none of the wave files are UI-bearing, skip — proceed to Step 3.
|
|
17
|
+
|
|
18
|
+
## Phase 1: Read tokens and sibling components
|
|
19
|
+
|
|
20
|
+
1. Identify design tokens from the plan's context or `frontend-design` output — colour tokens are needed for contrast checks.
|
|
21
|
+
2. Glob for sibling components in the same directory as files being authored. Read 1-2 examples to understand existing `aria-*` usage, `role` attributes, keyboard handlers, focus management patterns.
|
|
22
|
+
3. Note the established a11y posture: does the codebase already use `role="dialog"` patterns? Does it trap focus in modals? Are there custom keyboard handlers?
|
|
23
|
+
|
|
24
|
+
## Phase 2: Detect the stack
|
|
25
|
+
|
|
26
|
+
Read the planner's `test_strategy.platform` or grep for signal files:
|
|
27
|
+
|
|
28
|
+
| Signal | Stack |
|
|
29
|
+
|--------|-------|
|
|
30
|
+
| `next.config.ts` | Next.js (App Router) |
|
|
31
|
+
| `expo` in deps | React Native / Expo |
|
|
32
|
+
| `tauri.conf.json` | Tauri web view |
|
|
33
|
+
|
|
34
|
+
Load the matching reference doc from `reference/` (relative to this SKILL.md):
|
|
35
|
+
|
|
36
|
+
- Next.js / web: read `reference/semantic-html.md`, `reference/aria-roles-states.md`, `reference/keyboard-patterns.md`, `reference/contrast-visual.md`
|
|
37
|
+
- React Native: read `reference/aria-roles-states.md`, `reference/contrast-visual.md` (semantic HTML n/a for RN)
|
|
38
|
+
- Tauri web view: same as Next.js / web
|
|
39
|
+
|
|
40
|
+
## Phase 3: Load reference docs
|
|
41
|
+
|
|
42
|
+
For the detected stack, read EACH applicable reference doc in sequence. Do not skip — the pre-write checklist is derived from their combined content.
|
|
43
|
+
|
|
44
|
+
Reference docs (paths relative to this SKILL.md):
|
|
45
|
+
|
|
46
|
+
- `reference/semantic-html.md` — landmark roles, heading hierarchy, element semantics, anti-patterns
|
|
47
|
+
- `reference/aria-roles-states.md` — role table, aria-label vs aria-labelledby, live regions, expanded/pressed/hidden
|
|
48
|
+
- `reference/keyboard-patterns.md` — focus management, tab order, Esc, arrow keys, focus traps, type-ahead
|
|
49
|
+
- `reference/contrast-visual.md` — WCAG 2.1 AA ratios, focus-visible, colour-only state, reduced-motion, touch targets
|
|
50
|
+
|
|
51
|
+
## Phase 4: Commit to per-component obligations
|
|
52
|
+
|
|
53
|
+
For each component or element type in `wave.files[]`, derive explicit obligations from the reference docs. Group by component:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Component: <ComponentName> (<path>)
|
|
57
|
+
- Semantic: [e.g. "use <button> not <div onClick>"]
|
|
58
|
+
- ARIA: [e.g. "aria-expanded on disclosure trigger"]
|
|
59
|
+
- Keyboard: [e.g. "Esc closes; focus returns to trigger"]
|
|
60
|
+
- Contrast: [e.g. "border token needs 3:1 vs background — verify"]
|
|
61
|
+
- Touch: [e.g. "min 44x44px tap target on mobile"]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Unknown component types get the universal checklist from Phase 5.
|
|
65
|
+
|
|
66
|
+
## Phase 5: Universal guidelines (applied to every component)
|
|
67
|
+
|
|
68
|
+
Regardless of component type, every UI component must satisfy:
|
|
69
|
+
|
|
70
|
+
1. Every interactive element is keyboard-reachable and activatable (Enter/Space for buttons, Enter for links)
|
|
71
|
+
2. Focus-visible is never removed via `outline: none` without a custom indicator meeting 3:1 contrast
|
|
72
|
+
3. No state conveyed via colour alone — icon + text + colour
|
|
73
|
+
4. `prefers-reduced-motion` media query wraps any animation
|
|
74
|
+
5. Touch targets ≥ 44×44 CSS px
|
|
75
|
+
6. Images have `alt` text (decorative images use `alt=""`)
|
|
76
|
+
7. Form inputs have associated `<label htmlFor>` or `aria-label`
|
|
77
|
+
|
|
78
|
+
## Phase 6: Output pre-write checklist
|
|
79
|
+
|
|
80
|
+
Produce a flat checklist in `round.context.frontend_a11y_checklist`:
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
frontend_a11y_checklist:
|
|
84
|
+
- component: "<ComponentName>"
|
|
85
|
+
file: "<path>"
|
|
86
|
+
items:
|
|
87
|
+
- "[Semantic] use <button> not <div onClick>"
|
|
88
|
+
- "[ARIA] aria-expanded on trigger; aria-controls referencing panel id"
|
|
89
|
+
- "[Keyboard] Esc closes panel; focus returns to trigger"
|
|
90
|
+
- "[Contrast] verify focus ring token meets 3:1 vs background"
|
|
91
|
+
- "[Touch] min 44x44px on mobile breakpoint"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The executor consults this checklist when authoring each component in Step 3.
|
|
95
|
+
|
|
96
|
+
## Output back to round-executor
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
round.context.frontend_a11y_loaded: true
|
|
100
|
+
round.context.frontend_a11y_checklist: [per-component checklist items]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Integration
|
|
104
|
+
|
|
105
|
+
- **Invoked by**: `round-executor` Step 2.6 (when `"frontend-a11y"` in `wave.skill_preloads[]`)
|
|
106
|
+
- **Reads**: `reference/semantic-html.md`, `reference/aria-roles-states.md`, `reference/keyboard-patterns.md`, `reference/contrast-visual.md`
|
|
107
|
+
- **Output consumed by**: `round-executor` Step 3 (implementation guidance)
|
|
108
|
+
- **Pairs with**: `frontend-design` (invoked first in Step 2.6), `frontend-ui` + `frontend-ux` (post-implementation Step 3.8)
|
|
109
|
+
- **Trigger rule**: `rules/frontend-accessibility-invocation.md`
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# ARIA Roles, States, and Properties Reference
|
|
2
|
+
|
|
3
|
+
**Golden rule**: Prefer native HTML semantics. ARIA fills gaps — it does not replace semantic elements. An `<input type="checkbox">` is always better than `<div role="checkbox">`.
|
|
4
|
+
|
|
5
|
+
## When to Add a Role
|
|
6
|
+
|
|
7
|
+
Use `role` ONLY when no native HTML element maps to the semantic need:
|
|
8
|
+
|
|
9
|
+
| Pattern | Native element | ARIA role (fallback) |
|
|
10
|
+
|---------|---------------|----------------------|
|
|
11
|
+
| Dialog / modal | — | `role="dialog"` + `aria-modal="true"` |
|
|
12
|
+
| Alerting status message | — | `role="status"` or `role="alert"` |
|
|
13
|
+
| Tab list | — | `role="tablist"`, `role="tab"`, `role="tabpanel"` |
|
|
14
|
+
| Custom listbox | — | `role="listbox"`, `role="option"` |
|
|
15
|
+
| Progress indicator | `<progress>` | `role="progressbar"` if `<progress>` unstyled |
|
|
16
|
+
| Tooltip | `title` attr (limited) | `role="tooltip"` + `aria-describedby` |
|
|
17
|
+
|
|
18
|
+
## aria-label vs aria-labelledby vs aria-describedby
|
|
19
|
+
|
|
20
|
+
| Attribute | Use when | Priority |
|
|
21
|
+
|-----------|----------|----------|
|
|
22
|
+
| `aria-labelledby` | A visible text element names this element | Highest — overrides aria-label and native label |
|
|
23
|
+
| `aria-label` | No visible text label exists (icon buttons, close buttons) | Medium |
|
|
24
|
+
| `aria-describedby` | Additional descriptive text supplements the label | Lowest — supplementary, not a primary name |
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
```jsx
|
|
28
|
+
// Icon button — no visible label
|
|
29
|
+
<button aria-label="Close dialog">
|
|
30
|
+
<Icon name="x" aria-hidden="true" />
|
|
31
|
+
</button>
|
|
32
|
+
|
|
33
|
+
// Element labelled by visible heading
|
|
34
|
+
<section aria-labelledby="billing-heading">
|
|
35
|
+
<h2 id="billing-heading">Billing</h2>
|
|
36
|
+
</section>
|
|
37
|
+
|
|
38
|
+
// Field with hint text
|
|
39
|
+
<input id="password" aria-describedby="password-hint" />
|
|
40
|
+
<span id="password-hint">Must be at least 8 characters</span>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Live Regions
|
|
44
|
+
|
|
45
|
+
Announce dynamic content changes to screen readers without moving focus.
|
|
46
|
+
|
|
47
|
+
| Role / Attribute | Urgency | Use for |
|
|
48
|
+
|-----------------|---------|---------|
|
|
49
|
+
| `aria-live="polite"` | Waits for user to be idle | Status messages, form validation summaries, search result counts |
|
|
50
|
+
| `aria-live="assertive"` | Interrupts immediately | Critical errors, session timeout warnings |
|
|
51
|
+
| `role="status"` | Same as `aria-live="polite"` | Status messages (shorthand) |
|
|
52
|
+
| `role="alert"` | Same as `aria-live="assertive"` | Error alerts (shorthand) |
|
|
53
|
+
|
|
54
|
+
Anti-pattern: using `role="alert"` for non-urgent messages — screen reader interruptions are disruptive. Reserve for true emergencies.
|
|
55
|
+
|
|
56
|
+
```jsx
|
|
57
|
+
// Status (polite)
|
|
58
|
+
<div role="status" aria-live="polite">{statusMessage}</div>
|
|
59
|
+
|
|
60
|
+
// Error (assertive)
|
|
61
|
+
<div role="alert">{errorMessage}</div>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Common State Attributes
|
|
65
|
+
|
|
66
|
+
| Attribute | Values | Use for |
|
|
67
|
+
|-----------|--------|---------|
|
|
68
|
+
| `aria-expanded` | `true` / `false` | Disclosure triggers (accordion, dropdown, menu) |
|
|
69
|
+
| `aria-pressed` | `true` / `false` / `"mixed"` | Toggle buttons (bold, like, mute) |
|
|
70
|
+
| `aria-hidden` | `true` | Decorative elements, icon-only images — removes from accessibility tree |
|
|
71
|
+
| `aria-checked` | `true` / `false` / `"mixed"` | Custom checkbox/radio when not using `<input type="checkbox">` |
|
|
72
|
+
| `aria-disabled` | `true` | Visually disabled but still focusable (use sparingly) |
|
|
73
|
+
| `aria-selected` | `true` / `false` | Selected tab, selected option in listbox |
|
|
74
|
+
| `aria-current` | `"page"` / `"step"` / `true` | Current item in nav, current step in wizard |
|
|
75
|
+
|
|
76
|
+
```jsx
|
|
77
|
+
// Accordion trigger
|
|
78
|
+
<button
|
|
79
|
+
aria-expanded={isOpen}
|
|
80
|
+
aria-controls="panel-1"
|
|
81
|
+
>
|
|
82
|
+
Section title
|
|
83
|
+
</button>
|
|
84
|
+
<div id="panel-1" hidden={!isOpen}>...</div>
|
|
85
|
+
|
|
86
|
+
// Toggle button
|
|
87
|
+
<button
|
|
88
|
+
aria-pressed={isBold}
|
|
89
|
+
onClick={() => setIsBold(!isBold)}
|
|
90
|
+
>
|
|
91
|
+
Bold
|
|
92
|
+
</button>
|
|
93
|
+
|
|
94
|
+
// Decorative icon
|
|
95
|
+
<svg aria-hidden="true" focusable="false">...</svg>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## aria-hidden Usage Rules
|
|
99
|
+
|
|
100
|
+
- `aria-hidden="true"` removes element and ALL descendants from accessibility tree
|
|
101
|
+
- Never place `aria-hidden="true"` on a focusable element (keyboard users still reach it)
|
|
102
|
+
- Common correct uses: decorative icons, background images, duplicate visible text (when the accessible name comes from aria-label)
|
|
103
|
+
|
|
104
|
+
```jsx
|
|
105
|
+
// Correct: icon inside labelled button
|
|
106
|
+
<button aria-label="Delete">
|
|
107
|
+
<TrashIcon aria-hidden="true" />
|
|
108
|
+
</button>
|
|
109
|
+
|
|
110
|
+
// Wrong: aria-hidden on focusable element
|
|
111
|
+
<button aria-hidden="true">...</button> // users can still Tab to it
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Dialog Pattern
|
|
115
|
+
|
|
116
|
+
```jsx
|
|
117
|
+
<dialog
|
|
118
|
+
role="dialog"
|
|
119
|
+
aria-modal="true"
|
|
120
|
+
aria-labelledby="dialog-title"
|
|
121
|
+
aria-describedby="dialog-description"
|
|
122
|
+
>
|
|
123
|
+
<h2 id="dialog-title">Confirm deletion</h2>
|
|
124
|
+
<p id="dialog-description">This action cannot be undone.</p>
|
|
125
|
+
<button onClick={onConfirm}>Delete</button>
|
|
126
|
+
<button onClick={onClose}>Cancel</button>
|
|
127
|
+
</dialog>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Focus must move INTO the dialog on open and RETURN to the trigger on close. See `keyboard-patterns.md` for focus trap implementation.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Contrast and Visual Accessibility Reference
|
|
2
|
+
|
|
3
|
+
## WCAG 2.1 AA Contrast Requirements
|
|
4
|
+
|
|
5
|
+
All text and UI components must meet these minimum contrast ratios:
|
|
6
|
+
|
|
7
|
+
| Content type | Minimum ratio | Standard |
|
|
8
|
+
|-------------|--------------|---------|
|
|
9
|
+
| Normal text (< 18pt / < 14pt bold) | **4.5:1** | WCAG 2.1 AA SC 1.4.3 |
|
|
10
|
+
| Large text (≥ 18pt / ≥ 14pt bold) | **3:1** | WCAG 2.1 AA SC 1.4.3 |
|
|
11
|
+
| UI components (borders, icons, form controls) | **3:1** | WCAG 2.1 AA SC 1.4.11 |
|
|
12
|
+
| Graphical objects (data chart elements, icons conveying meaning) | **3:1** | WCAG 2.1 AA SC 1.4.11 |
|
|
13
|
+
|
|
14
|
+
**Decorative** elements (purely ornamental, no meaning) are EXEMPT.
|
|
15
|
+
|
|
16
|
+
### Verification
|
|
17
|
+
|
|
18
|
+
Use design tokens from `packages/design-tokens/` to derive hex values, then verify with a contrast checker:
|
|
19
|
+
|
|
20
|
+
- Browser DevTools Accessibility panel
|
|
21
|
+
- `npx @accessibility-checker/cli` against the rendered component
|
|
22
|
+
- Design-tool plugins (Figma Contrast, Stark)
|
|
23
|
+
|
|
24
|
+
If a token pair is new, record the ratio in a comment in the SCSS: `/* contrast: 5.2:1 vs --color-surface */`.
|
|
25
|
+
|
|
26
|
+
## Focus Visible
|
|
27
|
+
|
|
28
|
+
Focus indicators must NEVER be suppressed via `outline: none` or `outline: 0` without providing a replacement that meets **3:1 contrast** against adjacent colour:
|
|
29
|
+
|
|
30
|
+
```scss
|
|
31
|
+
// Anti-pattern — removes all visible focus indicator
|
|
32
|
+
&:focus {
|
|
33
|
+
outline: none; // WCAG failure
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Correct — custom focus ring that meets 3:1 contrast
|
|
37
|
+
&:focus-visible {
|
|
38
|
+
outline: 2px solid var(--color-focus-ring); // 3:1 minimum
|
|
39
|
+
outline-offset: 2px;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Use `:focus-visible` (not `:focus`) for the custom ring — `:focus-visible` suppresses the ring for mouse clicks while preserving it for keyboard navigation.
|
|
44
|
+
|
|
45
|
+
The WCAG 2.2 enhanced criterion (SC 2.4.11, AAA) requires 3:1 contrast + 2px outline area. Target this for new components.
|
|
46
|
+
|
|
47
|
+
## Colour-Only State Communication
|
|
48
|
+
|
|
49
|
+
State conveyed through colour alone is a WCAG 2.1 AA failure (SC 1.4.1). Always pair colour with at least one of: icon, text label, pattern, or shape.
|
|
50
|
+
|
|
51
|
+
| Anti-pattern | Fix |
|
|
52
|
+
|-------------|-----|
|
|
53
|
+
| Red border = error (colour only) | Red border + error icon + "Invalid email" text |
|
|
54
|
+
| Green = online (colour only) | Green dot + "Online" text label |
|
|
55
|
+
| Yellow = warning (colour only) | Yellow background + warning icon + descriptive text |
|
|
56
|
+
| Active nav item is blue (colour only) | Blue + `aria-current="page"` + underline or bold weight |
|
|
57
|
+
|
|
58
|
+
## prefers-reduced-motion
|
|
59
|
+
|
|
60
|
+
Users with vestibular disorders may configure `prefers-reduced-motion: reduce` in their OS. Honour it:
|
|
61
|
+
|
|
62
|
+
```scss
|
|
63
|
+
@keyframes slideIn {
|
|
64
|
+
from { transform: translateX(-100%); }
|
|
65
|
+
to { transform: translateX(0); }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.panel {
|
|
69
|
+
animation: slideIn 300ms ease-out;
|
|
70
|
+
|
|
71
|
+
@media (prefers-reduced-motion: reduce) {
|
|
72
|
+
animation: none;
|
|
73
|
+
// Provide instant appearance or a fade (no translate/scale/spin)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For JavaScript-driven animations:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
82
|
+
if (!prefersReduced) {
|
|
83
|
+
// run animation
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Transitions that solely affect `opacity` (fade) are generally safe — opacity changes do not trigger vestibular responses. Transforms (`translate`, `scale`, `rotate`) and large motion sweeps are the primary triggers.
|
|
88
|
+
|
|
89
|
+
## Touch Target Size
|
|
90
|
+
|
|
91
|
+
Interactive elements must have a minimum tap target of **44 × 44 CSS px** (Apple HIG / WCAG 2.5.5 AAA, de-facto standard):
|
|
92
|
+
|
|
93
|
+
```scss
|
|
94
|
+
.icon-button {
|
|
95
|
+
min-width: 44px;
|
|
96
|
+
min-height: 44px;
|
|
97
|
+
display: flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
justify-content: center;
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
When the visible icon is smaller (e.g. 24px), expand the tap target with padding:
|
|
104
|
+
|
|
105
|
+
```scss
|
|
106
|
+
.icon-button {
|
|
107
|
+
padding: 10px; // 24px icon + 2×10px padding = 44px touch target
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
On mobile breakpoints specifically, verify that adjacent interactive elements have at least 8px spacing between tap target edges to prevent accidental activation.
|
|
112
|
+
|
|
113
|
+
## Text Resizing
|
|
114
|
+
|
|
115
|
+
Users who increase browser text size up to 200% must be able to read and use all content without horizontal scroll (WCAG 1.4.4). Use relative units:
|
|
116
|
+
|
|
117
|
+
- `font-size`: `rem` (relative to browser root, respects user preference)
|
|
118
|
+
- `line-height`: unitless (e.g. `1.5`) or `em`
|
|
119
|
+
- Container widths: `max-width` in `ch` or `%`, never fixed `px` for reading columns
|
|
120
|
+
- Spacing: `em` for component-internal spacing; `rem` for layout spacing
|
|
121
|
+
|
|
122
|
+
Avoid `px` for font sizes on text elements that users may need to scale.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Keyboard Interaction Patterns Reference
|
|
2
|
+
|
|
3
|
+
Every interactive component must be fully operable with a keyboard alone. Mouse-only patterns are WCAG 2.1 AA failures (Success Criterion 2.1.1).
|
|
4
|
+
|
|
5
|
+
## Focus Management
|
|
6
|
+
|
|
7
|
+
### On Mount (Dialog / Modal / Drawer)
|
|
8
|
+
|
|
9
|
+
When a dialog, modal, or drawer opens:
|
|
10
|
+
|
|
11
|
+
1. Move focus to the FIRST interactive element inside (or to the dialog container if no interactive element exists — ensure `tabindex="0"` on the container in that case)
|
|
12
|
+
2. For modals with a clear primary action: move focus to the primary action button
|
|
13
|
+
3. For forms: move focus to the first form field
|
|
14
|
+
|
|
15
|
+
```jsx
|
|
16
|
+
// Using useEffect + ref
|
|
17
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (isOpen) {
|
|
21
|
+
// Focus the first focusable element inside
|
|
22
|
+
const firstFocusable = modalRef.current?.querySelector<HTMLElement>(
|
|
23
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
24
|
+
);
|
|
25
|
+
firstFocusable?.focus();
|
|
26
|
+
}
|
|
27
|
+
}, [isOpen]);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### On Close (Dialog / Modal / Drawer)
|
|
31
|
+
|
|
32
|
+
When a dialog, modal, or drawer closes:
|
|
33
|
+
|
|
34
|
+
1. Return focus to the element that TRIGGERED the opening (save a ref to it before opening)
|
|
35
|
+
2. If the trigger no longer exists (deleted row), focus the nearest logical element
|
|
36
|
+
|
|
37
|
+
```jsx
|
|
38
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
39
|
+
|
|
40
|
+
const handleClose = () => {
|
|
41
|
+
setIsOpen(false);
|
|
42
|
+
// Return focus to trigger
|
|
43
|
+
triggerRef.current?.focus();
|
|
44
|
+
};
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Tab Order
|
|
48
|
+
|
|
49
|
+
- Tab order must follow the logical reading/interaction order — typically top-left to bottom-right
|
|
50
|
+
- Never use `tabindex > 0` — it creates a separate tab order before natural DOM order, confusing all keyboard users
|
|
51
|
+
- `tabindex="0"` adds a non-interactive element to natural tab order (use sparingly — prefer interactive elements)
|
|
52
|
+
- `tabindex="-1"` removes from natural tab order but allows programmatic `.focus()` (correct for modal containers)
|
|
53
|
+
|
|
54
|
+
## Esc Key
|
|
55
|
+
|
|
56
|
+
Any overlay, popover, dropdown, or modal MUST close on `Escape`:
|
|
57
|
+
|
|
58
|
+
```jsx
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
61
|
+
if (e.key === 'Escape') {
|
|
62
|
+
onClose();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
if (isOpen) {
|
|
66
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
67
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
68
|
+
}
|
|
69
|
+
}, [isOpen, onClose]);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Arrow Key Navigation
|
|
73
|
+
|
|
74
|
+
For composite widgets (menu, listbox, radio group, tabs, grid), arrow keys navigate BETWEEN items — Tab moves focus OUT of the widget entirely.
|
|
75
|
+
|
|
76
|
+
| Widget | Keys |
|
|
77
|
+
|--------|------|
|
|
78
|
+
| Menu / dropdown | `↑`/`↓` between items, `Home`/`End` to first/last |
|
|
79
|
+
| Tabs | `←`/`→` between tabs (horizontal) or `↑`/`↓` (vertical) |
|
|
80
|
+
| Listbox | `↑`/`↓` between options, `Home`/`End` |
|
|
81
|
+
| Grid | `↑`/`↓`/`←`/`→` between cells |
|
|
82
|
+
| Radio group | `↑`/`↓` or `←`/`→` between radios; selection follows focus |
|
|
83
|
+
|
|
84
|
+
## Focus Traps (Modal)
|
|
85
|
+
|
|
86
|
+
While a modal is open, Tab and Shift+Tab must cycle WITHIN the modal — not escape to the page behind:
|
|
87
|
+
|
|
88
|
+
```jsx
|
|
89
|
+
const FOCUSABLE_SELECTORS =
|
|
90
|
+
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
91
|
+
|
|
92
|
+
const handleTabKey = (e: KeyboardEvent) => {
|
|
93
|
+
const focusableEls = Array.from(
|
|
94
|
+
modalRef.current?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS) ?? []
|
|
95
|
+
);
|
|
96
|
+
const first = focusableEls[0];
|
|
97
|
+
const last = focusableEls[focusableEls.length - 1];
|
|
98
|
+
|
|
99
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
last.focus();
|
|
102
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
first.focus();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Library alternative: `focus-trap-react` handles this pattern with accessibility compliance. If it is already in `package.json`, use it.
|
|
110
|
+
|
|
111
|
+
## Type-Ahead (Listbox / Menu)
|
|
112
|
+
|
|
113
|
+
When a user types a character while focus is inside a listbox or menu, focus jumps to the first item whose label starts with that character. Implement only when `role="listbox"` or `role="menu"` is used with custom keyboard handling — native `<select>` has this built in.
|
|
114
|
+
|
|
115
|
+
## Enter and Space Activation
|
|
116
|
+
|
|
117
|
+
| Element | Enter | Space |
|
|
118
|
+
|---------|-------|-------|
|
|
119
|
+
| `<button>` | Activates | Activates |
|
|
120
|
+
| `<a href>` | Follows link | Scrolls page (native browser) |
|
|
121
|
+
| `<input type="checkbox">` | n/a | Toggles |
|
|
122
|
+
| `role="button"` | Must activate | Must activate |
|
|
123
|
+
| `role="menuitem"` | Activates | Activates |
|
|
124
|
+
|
|
125
|
+
For custom `role="button"` on a non-button element:
|
|
126
|
+
```jsx
|
|
127
|
+
<div
|
|
128
|
+
role="button"
|
|
129
|
+
tabIndex={0}
|
|
130
|
+
onKeyDown={(e) => {
|
|
131
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
handleActivate();
|
|
134
|
+
}
|
|
135
|
+
}}
|
|
136
|
+
onClick={handleActivate}
|
|
137
|
+
>
|
|
138
|
+
Custom button
|
|
139
|
+
</div>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Prefer `<button>` — it handles Enter/Space natively and avoids this boilerplate.
|
|
143
|
+
|
|
144
|
+
## Skip Links
|
|
145
|
+
|
|
146
|
+
For pages with repeated navigation (header nav present on every page), provide a skip-to-main-content link as the first focusable element:
|
|
147
|
+
|
|
148
|
+
```jsx
|
|
149
|
+
<a href="#main-content" className={styles.skipLink}>
|
|
150
|
+
Skip to main content
|
|
151
|
+
</a>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Visible on focus (via CSS), hidden otherwise. The `<main id="main-content">` is the target.
|