Skip to content

feat(a11y): WCAG 2.1 AA scanning, baseline ratchet, and remediation - #2011

Open
paustint wants to merge 201 commits into
mainfrom
chore/wcag-a11y-scanning
Open

feat(a11y): WCAG 2.1 AA scanning, baseline ratchet, and remediation#2011
paustint wants to merge 201 commits into
mainfrom
chore/wcag-a11y-scanning

Conversation

@paustint

@paustint paustint commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

A customer agreement requires commercially reasonable efforts toward WCAG 2.1 AA
and an accessibility conformance report (VPAT) on written request. This branch
adds the scanning infrastructure to know where we stand, remediates what it
found across the web app, desktop app, browser extension, landing site and docs,
and guards against regressions. Program docs, the findings log (the canonical
tracker: C#, X#, L#, M#, R#, V# references below) and the draft VPAT live in
docs/accessibility/; a public summary is at /accessibility on the docs site.

Infrastructure and gates

  • Playwright axe scans of every app route plus key interactive states, gated by
    a ratchet baseline (a11y-baseline.json): existing violations pass, anything
    new fails, and the file only shrinks as findings are fixed. Scan evidence
    uploads as CI artifacts (VPAT inputs). pnpm a11y:scan-urls covers landing/docs.
  • axeScan() for component specs (asserts on its own; filters Floating UI's
    focus-guard noise), with an axeScan() requirement for new libs/ui specs.
  • pnpm a11y:lint-ratchet (pre-commit + CI lint job): per-file baseline of
    warn-tier jsx-a11y hits that may only shrink, every swept APP_ROUTES
    entry must be baselined; five jsx-a11y rules promoted to error.
  • /a11y-review skill and a definition of done for interactive UI in CLAUDE.md.

Remediation — axe baseline went from 89 serious/critical entries to 3
(the one open nested-interactive pattern), zero critical violations across
49 scans, plus two full keyboard/VoiceOver manual passes over every surface.
The recurring fixes, all through shared primitives in @jetstream/ui:

  • Names, roles and states: combobox/picklist option roles were on the wrong
    element (silent arrowing), icon-only buttons had no names, radio groups
    shared no name, form controls now carry aria-invalid and described-by help.
  • Composite keyboard patterns: roving tabindex for lists, field lists, tabs,
    trees, accordions, menus and the grid (one page tab stop each, arrows inside);
    ARIA 1.2 combobox; TimePicker rebuilt as a type-to-filter combobox.
  • Focus management: modals/popovers/panels/menus return focus to their opener,
    self-disabling buttons keep focus (ariaDisabledButtonProps), controls that
    remove themselves hand focus to a neighbour, route changes move focus to main.
  • Announcements: AssistiveStatus / useAnnouncer for long-running actions,
    counts, save/deploy/test outcomes; toasts are live regions (errors assertive).
  • Escape layering (useEscapeToCloseLayer): one press closes only the innermost
    layer; code editors keep Escape while autocomplete/find/hover is open.
  • Monaco editors: Escape / Shift+Escape leave the editor and the editor's name
    says so; Settings gains "Optimize code editors for screen readers".

Behavior changes to eyeball

  • Org cards drag from a dedicated grip handle; card-body dragging no longer
    initiates a drag (the card body is full of buttons).
  • Landing CTAs on pricing/desktop-app/goodbye darkened cyan-500 → cyan-700.
  • Grid rows are 29px (were 28.5px) so row borders render on whole pixels; the
    record-actions column header now reads "Actions".
  • A closed combobox opens on Enter; Picklist no longer reopens after choosing
    with Enter/Space; menus activate with Space and open with ArrowDown; the
    download modals open on Enter without downloading at once.
  • Cmd/Ctrl+Enter saves the record modal; page-level Cmd/Ctrl+Enter shortcuts
    stay quiet while a modal is open.
  • Escape leaves a Monaco editor (after closing any open editor widget).
  • Cookie banner buttons are "Accept cookies" / "Reject cookies"; Debug Levels
    rows are real buttons; the feedback widget sits below overlay layers.
  • Checkbox lists dropped listbox/option roles (options cannot contain
    interactive children) — E2E page objects assert on the row checkbox instead.

Deliberately not done (all logged in the findings "Still open" table):
nested-interactive on list rows with trailing popover triggers (3 pages) stays
baselined pending a design decision; 14 VPAT criteria remain TBD until their
evidence passes are recorded; the route sweep covers top-level routes only.

Review history: three code-review rounds (2026-09-01, 09-07, 09-13) with
every finding either fixed as a fixup of the commit that introduced it or
logged; the 09-13 round added browser-verified fixes for popover clipping of
filter dropdowns and Escape-vs-autocomplete in the SOQL popovers.

Copilot AI lite review requested due to automatic review settings August 27, 2026 02:04
Comment thread apps/jetstream-e2e/src/tests/a11y/page-sweep.spec.ts Fixed
Comment thread apps/jetstream-e2e/src/tests/a11y/page-sweep.spec.ts Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR establishes an end-to-end WCAG 2.1 AA accessibility program for Jetstream: automated scanning (Playwright + axe-core) with a “baseline ratchet” that prevents regressions, plus targeted UI remediations and supporting program documentation (findings log + draft ACR/VPAT).

Changes:

  • Added Playwright axe-core page sweep + interactive-state scans that write evidence artifacts and gate against a11y-baseline.json (ratchet behavior).
  • Added component-test axe scanning via axeScan() (@jetstream/test-utils) and began applying it to interactive libs/ui components.
  • Remediated common accessibility issues across UI (tabs keyboard pattern, icon-only button names, list/checkbox semantics, skip links, form error ARIA wiring, dialog semantics, contrast/link distinguishability).

Reviewed changes

Copilot reviewed 78 out of 80 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
scripts/a11y-scan-urls.mjs Ad-hoc URL scanning for non-authenticated/static surfaces (landing/docs).
scripts/a11y-merge-baseline.mjs Utility to regenerate/merge the committed baseline ratchet from scan results.
package.json Adds a11y scripts and dev deps for axe + vitest-axe.
pnpm-lock.yaml Locks new axe / vitest-axe dependencies.
libs/ui/tsconfig.lib.json Adds project refs needed for new UI test utilities usage.
libs/ui/src/lib/widgets/Icon.tsx Ensures icon-only controls can expose an accessible name (remove baked-in aria-hidden).
libs/ui/src/lib/toast/Toast.tsx Error toasts become assertive alerts; adds aria-live/aria-atomic.
libs/ui/src/lib/toast/tests/Toast.spec.tsx Updates role assertions for error toasts.
libs/ui/src/lib/tabs/Tabs.tsx Implements WAI-ARIA tabs keyboard behavior (Arrow/Home/End + activation).
libs/ui/src/lib/tabs/Tab.tsx Implements roving tabindex (only active tab in tab order).
libs/ui/src/lib/tabs/tests/Tabs.spec.tsx Adds keyboard behavior tests + axeScan assertion.
libs/ui/src/lib/sobject-list/SobjectListMultiSelect.tsx Names the objects list for screen readers.
libs/ui/src/lib/sobject-list/SobjectListFilter.tsx Adds accessible name for filter trigger button.
libs/ui/src/lib/sobject-list/SobjectList.tsx Names the objects list for screen readers.
libs/ui/src/lib/sobject-field-list/SobjectFieldList.tsx Adds icon descriptions + names the fields list.
libs/ui/src/lib/progress-indicator/ProgressStepIndicator.tsx Adds aria-label for progressbar.
libs/ui/src/lib/progress-indicator/ProgressIndicator.tsx Adds aria-label for progressbar.
libs/ui/src/lib/modal/tests/Modal.spec.tsx Adds axeScan assertion and refactors render helper.
libs/ui/src/lib/list/ListWithFilterMultiSelect.tsx Adds accessible names for reload button and list.
libs/ui/src/lib/list/ListItemCheckbox.tsx Removes listbox/option semantics for checkbox rows; ensures checkbox has a label.
libs/ui/src/lib/list/List.tsx Adds ariaLabel prop; adjusts semantics for checkbox lists vs listbox lists.
libs/ui/src/lib/layout/SkipToContent.tsx New skip-link component for bypassing header/navigation.
libs/ui/src/index.ts Exports SkipToContent from the UI library.
libs/ui/src/lib/form/select/Select.tsx Automatically wires aria-invalid / error aria-describedby onto native select children.
libs/ui/src/lib/form/input/Input.tsx Automatically wires aria-invalid / error aria-describedby onto native input children.
libs/ui/src/lib/form/form-a11y.utils.tsx Shared helper for stamping error semantics onto native controls.
libs/ui/src/lib/form/picklist/Picklist.tsx Fixes aria-controls to only reference mounted listbox while open.
libs/ui/src/lib/form/formGroupDropDown/FormGroupDropdown.tsx Fixes aria-controls to only reference mounted listbox while open.
libs/ui/src/lib/form/date/DatePicker.tsx Improves input descriptions + dialog semantics for the date popup.
libs/ui/src/lib/form/combobox/ComboboxListItem.tsx Moves option semantics to the focused element to ensure SR announcement.
libs/ui/src/lib/form/combobox/Combobox.tsx Fixes aria-controls to only reference mounted listbox while open.
libs/ui/src/lib/data-table/grid/renderers/CellRenderers.tsx Adds aria-labels for icon-only row action buttons.
libs/test/e2e-utils/src/lib/pageObjectModels/QueryPage.model.ts Updates selectors/assertions for checkbox-based selection semantics.
libs/test/e2e-utils/src/lib/pageObjectModels/OrgGroupPage.model.ts Updates drag initiation to use dedicated drag handle.
libs/test-utils/src/lib/a11y-test-utils.ts Introduces axeScan() helper and filters library-internal focus guards.
libs/test-utils/src/index.ts Exports axeScan() from @jetstream/test-utils.
libs/shared/ui-record-form/src/lib/UiRecordForm.tsx Adds label for columns select control.
libs/shared/ui-core/src/settings/SoqlQueryFormatConfig.tsx Ensures numeric inputs are properly label-associated via ids.
libs/shared/ui-core/src/record/UserSearchPopover.tsx Improves button title for clarity.
libs/shared/ui-core/src/query/QueryHistory/QueryHistoryModal.tsx Names list for screen readers.
libs/shared/ui-core/src/orgs/OrgInfoPopover.tsx Adds accessible name to org details trigger.
libs/shared/ui-core/src/formula-evaluator/FormulaEvaluatorRecordSearch.tsx Adds icon description for accessible name.
libs/shared/ui-core/src/app/HeaderHelpPopover.tsx Adds accessible name to help trigger.
libs/shared/ui-core/src/app/AppHome/AppHomeBillingUser.tsx Fixes invalid <dl> structure by nesting link inside <dd>.
libs/shared/ui-core/src/app/AppHome/AppHome.tsx Fixes invalid <dl> structure by nesting link inside <dd>.
libs/shared/ui-core/src/analysis/PermissionAnalysisHistoryModal.tsx Names list for screen readers.
libs/features/salesforce-api/src/SalesforceApiHistoryModal.tsx Names list for screen readers.
libs/features/platform-event-monitor/src/PlatformEventMonitorPublisherCard.tsx Adds icon description for accessible name.
libs/features/platform-event-monitor/src/PlatformEventMonitorListenerCard.tsx Adds icon descriptions for accessible names.
libs/features/org-groups/src/lib/SalesforceOrgCardDraggable.tsx Adds dedicated drag handle button for accessible DnD initiation.
libs/features/load-records-multi-object/src/LoadRecordsMultiObject.tsx Underlines link for non-color-only distinguishability.
libs/features/deploy/src/selection-components/UserSelection.tsx Names list for screen readers.
libs/features/debug-log-viewer/src/DebugLogViewerTable.tsx Ensures button-like cells have an accessible name when content is empty.
libs/features/debug-log-viewer/src/DebugLogViewer.tsx Adds icon descriptions for accessible names on controls.
libs/connected/connected-ui/src/lib/DescribeMetadataList.tsx Names list for screen readers.
apps/landing/pages/pricing/index.tsx Adjusts CTA contrast colors to meet WCAG contrast.
apps/landing/pages/goodbye/index.tsx Adjusts contrast + ensures link is distinguishable without color alone.
apps/landing/pages/desktop-app/index.tsx Adjusts CTA contrast colors to meet WCAG contrast.
apps/landing/pages/_document.js Adds <html lang="en">.
apps/landing/components/layouts/Layout.tsx Adds skip link + focusable target container.
apps/landing/components/Footer.tsx Improves footer text contrast.
apps/jetstream/src/app/components/billing/EnhancedBillingCard.tsx Adds aria-label for radio input.
apps/jetstream/src/app/app.tsx Adds SkipToContent and main-content focus target in web app shell.
apps/jetstream-desktop-client/src/app/app.tsx Adds SkipToContent and main-content focus target in desktop app shell.
apps/jetstream-e2e/tsconfig.json Adds project ref needed by new a11y tests import(s).
apps/jetstream-e2e/src/tests/query/query-results.spec.ts Updates assertions to checkbox checked state.
apps/jetstream-e2e/src/tests/query/query-builder.spec.ts Updates assertions to checkbox checked state.
apps/jetstream-e2e/src/tests/a11y/page-sweep.spec.ts New route-wide a11y sweep spec using APP_ROUTES.
apps/jetstream-e2e/src/tests/a11y/interactive-states.spec.ts New a11y scans for critical interactive UI states.
apps/jetstream-e2e/src/tests/a11y/a11y.utils.ts Shared scan harness: evidence output + baseline ratchet gating.
apps/jetstream-e2e/src/tests/a11y/a11y-baseline.json Adds initial committed baseline ratchet entries.
docs/accessibility/README.md Documents the accessibility program, tooling, and ratchet rules.
docs/accessibility/audit-2026/manual-checklist.md Adds a repeatable manual audit runbook/checklist.
docs/accessibility/audit-2026/findings.md Adds findings log + lint census + scan summary & status tracking.
docs/accessibility/vpat/jetstream-acr-DRAFT.md Adds draft ACR/VPAT structure for customer requests.
.oxlintrc.json Documents a11y lint ratchet and promotes no-redundant-roles to error.
.gitignore Ignores generated a11y-results evidence directories.
.github/workflows/ci.yml Uploads a11y scan results as CI artifacts per shard.
CLAUDE.md Adds explicit repo guidance for accessibility + a11y testing expectations.
.github/copilot-instructions.md Adds accessibility review/testing expectations to Copilot instructions.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/a11y-scan-urls.mjs Outdated
Comment thread libs/features/debug-log-viewer/src/DebugLogViewerTable.tsx Outdated
Comment thread libs/ui/src/lib/list/List.tsx Outdated
Comment thread scripts/a11y-scan-urls.mjs
Copilot AI review requested due to automatic review settings August 28, 2026 12:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 79 out of 81 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

libs/ui/src/lib/list/List.tsx:193

  • In checkbox mode (useCheckbox), the list intentionally moves focus into the individual checkboxes (per the comment), but the <ul> is still always tabIndex={0}. That creates an extra (and usually undesired) tab stop before the first checkbox and makes the "plain list" container focusable even though it’s no longer a listbox.
    libs/ui/src/lib/form/combobox/ComboboxListItem.tsx:131
  • ComboboxListItem now marks the focused element (<li role="option">) as aria-disabled={disabled}, but it still always calls onSelection(id) on click. If an option is disabled, it should not be selectable via pointer activation; otherwise assistive tech will announce it as disabled but it still behaves enabled.

@paustint
paustint marked this pull request as draft August 30, 2026 13:47
@paustint
paustint force-pushed the chore/wcag-a11y-scanning branch from 8608644 to 6d67931 Compare August 30, 2026 22:02
@paustint
paustint marked this pull request as ready for review August 30, 2026 22:03
@paustint
paustint force-pushed the chore/wcag-a11y-scanning branch 2 times, most recently from a9a4b6b to 82bdd47 Compare September 1, 2026 00:26
Copilot AI review requested due to automatic review settings September 1, 2026 00:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 229 out of 231 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment thread libs/ui/src/lib/widgets/Tooltip.tsx Outdated
Comment thread scripts/a11y-merge-baseline.mjs Outdated
Copilot AI review requested due to automatic review settings September 1, 2026 15:00
@paustint
paustint force-pushed the chore/wcag-a11y-scanning branch from 82bdd47 to 800e8da Compare September 1, 2026 15:00
@socket-security

socket-security Bot commented Sep 1, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​axe-core/​playwright@​4.13.01001001009970
Addedaxe-core@​4.13.01001001009970
Addedvitest-axe@​0.1.0991009780100

View full report

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 248 out of 251 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (1)

libs/ui/src/lib/widgets/Tooltip.tsx:212

  • onKeyDown invokes the onClick handler by casting a KeyboardEvent to a MouseEvent, which can break callers that rely on mouse-specific properties (or currentTarget typing) and is unsafe at runtime. Prefer triggering an actual click from the focused element so onClick receives the correct event shape.

Comment thread libs/ui/src/lib/list/ListItemCheckbox.tsx
Copilot AI review requested due to automatic review settings September 1, 2026 22:12
@paustint
paustint force-pushed the chore/wcag-a11y-scanning branch from 800e8da to 1b23f5b Compare September 1, 2026 22:12
@paustint

paustint commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Went through Copilot's suppressed (low-confidence) comments across the reviews — two were valid:

  • libs/ui/src/lib/form/combobox/ComboboxListItem.tsx:131 — fixed: the option announced aria-disabled but still ran onSelection on click. Guarded the click, and also the Enter path in all four combobox variants (the load-records related-object picker really does produce disabled options).
  • libs/ui/src/lib/widgets/Tooltip.tsx:212 — same finding as the posted review thread; fixed there. Keyboard activation now dispatches a real click so onClick receives a genuine MouseEvent, and trigger props no longer clobber triggerProps handlers with undefined keys.

libs/ui/src/lib/list/List.tsx:193 didn't apply — the ul is deliberately the checkbox list's single tab stop (composite-widget pattern): the checkboxes are tabIndex={-1} roving targets, so there is no extra tab stop before them.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The baseline merge script can overwrite the ratchet output even on missing/empty inputs, and CopyToClipboard’s in-button live region can destabilize the control’s accessible name.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Files not reviewed (1)

  • pnpm-lock.yaml: Generated file
  • Files reviewed: 249/252 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread libs/ui/src/lib/widgets/CopyToClipboard.tsx Outdated
Comment thread scripts/a11y-merge-baseline.mjs Outdated
Copilot AI review requested due to automatic review settings September 1, 2026 23:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The skip-link focus targets suppress the focus outline, which can make focus invisible for keyboard users and undermine the accessibility goals of the PR.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Files not reviewed (1)

  • pnpm-lock.yaml: Generated file
  • Files reviewed: 293/296 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread apps/landing/components/layouts/Layout.tsx
Comment thread libs/shared/ui-core/src/app/AppMainContent.tsx
Copilot AI review requested due to automatic review settings September 2, 2026 13:30
Enter on a focused, closed combobox did nothing (or submitted a wrapping
form), while Picklist, DropDown and Lightning's own combobox all open on
it. The list opens on keydown, and that press's keyup is skipped so it
does not fall into the "Enter picks the first option" branch and undo
the open. A modified Enter is left alone for page-level shortcuts.
The button disabled itself on click through the native attribute, which
drops focus to <body>. aria-disabled keeps it focusable, and the result is
announced since nothing visible confirms the cache was cleared.
With org groups configured, the "Choose Group" label's 1.5 line box pushed
the stacked org switcher past the 3.125rem global header, and the navbar
painted over the bottom 2px of the org combobox and "Add Org" focus rings.
A 1.2 line height on the selector keeps the whole stack inside the header;
1 was too tight for the 14px group name in the selected state.
The icon-only gear beside "Format" was announced as "Settings", which says
nothing about what it configures and collides with the app's Settings page.
…ng from a combobox list

The list selects on keydown and hands focus back to the input, so the same
press's keyup arrived at the input's "Enter picks the first option" branch.
Drill-in items made it worst: they keep the list open and refocus the input
after 50ms, so any normal key release drilled into the first child as well.
The input now acts on an Enter keyup only when it saw the keydown itself.
The modals open with the filename input focused and that input ran the
download on Enter keyup, so the keyup of the Enter that activated the
Download button downloaded and closed the modal immediately. Enter is now
handled on keydown, which the opening press never delivers to the input.
… keys

Space on a focused menu item fell through to type-ahead and jumped focus to
the first item; the trigger now also opens on ArrowDown (first or selected
item) and ArrowUp (last item), per the APG menu button pattern. The trigger
button is exposed through triggerRef for callers that must refocus it.
…hes the results

Closing the modal after a deploy re-runs the query, which disables the
Record actions trigger while it loads, and a disabled button cannot take
the focus the modal hands back. The hand-off now completes once the
trigger is enabled again.
… modal is open

The primary-action and go-back shortcuts listened on window with no notion of
where the action lived, so Cmd+Enter while reading a test result or coverage
source launched an Apex test run from behind the modal. Page-scope actions
(the default) now stay quiet while an aria-modal dialog is open; the two
shortcuts that belong to a modal (record modal Save, suite membership Save)
declare scope: 'dialog' and keep firing.
aria-readonly was derived from the column's `editable` flag alone, but that
flag also covers paste/clear eligibility on checkbox columns that have no
editor (every permission manager table), so those cells were hinted "Press
Enter to edit" while Enter toggles the checkbox. Read-only now follows the
same predicate as the editor path (editable AND an editor), and summary-row
cells, which only ever host filter and bulk-action controls, are read-only.
Findings V23-V43 with their fixes, the corrected lint census (71 hits in 39
files), the honest VPAT status (14 evidence-missing criteria still TBD), the
package version the draft was evaluated against, and the V rows in the
checklist's regression rule.
…h row disables them

The click that adds the fifth clause natively disabled the button under the
pointer or keyboard, which drops focus to <body>. Both buttons now disable
through aria-disabled (ariaDisabledButtonProps) so focus stays put and the
click guard still refuses a sixth row.
…ke its list one tab stop

Choosing a group rendered a different tree (Choose Group vs. name + Switch),
which re-mounted the popover and its trigger, so the popover had nothing to
return focus to and keyboard focus fell to <body>. The selector now renders
one stable tree, so closing the popover lands back on the trigger, which is
named "Switch group" when a group is active.

The group rows were each a Tab stop selected through an li click handler;
they are now the shared roving List: one tab stop, ArrowUp/Down, Home/End,
Enter/Space to choose. The heading also loses a copy-pasted "Refresh
Metadata" tooltip.
Finding V50 (owner-reported) with the fix, and the V range in the checklist's
regression rule and the VPAT's evaluation-methods list.
CI runs every project's Vitest concurrently through `nx run-many -t test`, so specs
that render real widgets (floating-ui popovers, 96-option comboboxes, virtualized
grids) and finish with an axe scan contend for CPU and overran Vitest's 5s default.
They surfaced as an intermittent timeout in a different spec on every run, which
reads as an unrelated failure rather than as load.

Raise the floor once in the shared jsdom setup instead of per test. 20s is still
short enough to catch a genuine hang.
…rtcuts

Monaco captures Tab as indentation, so a keyboard user who focused a code editor had
no way out of it. Ctrl+M was the only escape and it was documented in Settings, which
is not where anyone stuck in an editor looks (WCAG 2.1.2).

Escape now moves focus to the next control after the editor, and Shift+Escape to the
previous one. Both directions are needed: leaving only forwards makes the editor a
one-way valve, because shift-tabbing back from the next control lands inside it again
and Monaco captures Shift+Tab as outdent, stranding everything before the editor.

The handler runs in the capture phase, above the editor, so it acts before Monaco sees
the key. Monaco reclaims its textarea while handling the same keydown, so a move made
from inside a Monaco command was silently bounced back — which made every candidate
look unfocusable and walked the search off the end of the page. For the same reason the
widget and multi-cursor guards are a DOM and `getSelections()` check rather than
context keys.

Each editor also announces its own Cmd/Ctrl+Enter action. The button that runs it sits
in the card header, before the editor in the tab order, so a keyboard user who has just
finished typing cannot reach it by tabbing forward. Announcing it on the editor puts it
where the user is standing, rather than bending the tab order: a positive `tabindex`
hoists a control ahead of every natural tab stop document-wide.
Team Dashboard, Platform Events and Anonymous Apex, as V51-V57. Also supersedes C9:
it was closed on the strength of Ctrl+M documented in Settings, which this pass showed
nobody finds while stuck in an editor.
…type

A button without a `type` inside a `<form>` is a submit button. The Platform
Events publisher now wraps its record form in a real form, so its field clear
"x" and the inline date picker's Clear / Today / Previous / Next Month buttons
submitted the form and published an event.

Every helper button in the shared form primitives is now `type="button"`;
none of them is meant to submit anything.
`axeScan()` returned the filtered results and left the assertion to the caller,
while the lint ratchet counted a spec as covered as soon as it called
`axeScan(`. Eight specs called it bare, so they scanned and asserted nothing.

The helper now fails the test on any violation and still returns the results.
A spec that expects a logged, still-open violation (nested-interactive, X4)
names it through `knownViolations`, which keeps that rule out of the assertion
but in the returned results so the spec can pin exactly what it expects.
`Select` labelled `date-format`, but the control's id was `date-format-select`,
so the select had no accessible name; its `aria-describedby` also pointed at
nothing. The select now carries the labelled id and is described by the help
text.
`ProgressIndicator` and `ProgressStepIndicator` were always named "Progress",
which tells a screen reader user nothing when several bars share a surface
(the Jobs popover). Both take a `label`, and the five callers name theirs.
Findings V58–V71 from the rebased-branch review: the fixes, which are fixups
and which stand alone. The VPAT's keyboard-trap row and evaluation date, the
public accessibility page and the Settings page now describe the Escape /
Shift+Escape way out of code editors and the screen reader editor setting.
Copilot AI review requested due to automatic review settings September 14, 2026 01:14
@paustint
paustint force-pushed the chore/wcag-a11y-scanning branch from 2fcaa09 to fcb46e4 Compare September 14, 2026 01:14
Copilot stopped reviewing on behalf of paustint due to an error September 14, 2026 01:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants