Skip to content

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

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

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

Conversation

@paustint

@paustint paustint commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Jetstream is working toward WCAG 2.1 AA, with an accessibility conformance report
(VPAT) for customers who ask for one. 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 page at /accessibility on the docs site states the goals and the known
gaps without claiming full conformance.

Infrastructure and gates

  • Playwright axe scans of every top-level 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 + the required build-and-test 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 gain a grip handle, which carries the keyboard drag and the button
    role; the mouse can still pick a card up anywhere, as before.
  • 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 and Create Records (committing the
    field still being typed first); 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.
  • <main> is focusable only while the skip link or a route change hands it
    focus, so a click on page text no longer resets Tab or stops keyboard scrolling.
  • Grid-heavy tab sets (Permission Analysis, Manage Permissions, Field Usage,
    multi-object load results, Data History payloads) and the extension panel's
    tabs select on Enter/Space rather than as the arrows pass.
  • The extension panel opens with focus on its tab list, not the record-id box
    (a paste-to-open handler is logged as a possible follow-up).
  • Cmd/Ctrl+Enter on Query Results reloads when there are no unsaved edits (the
    grid used to swallow it); with edits it still opens Preview Changes.
  • Radio lost its idPrefix prop: help and error text are described on the
    group, not on each radio.

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. A fourth round
on 09-18 asked a different question - does any existing workflow get worse than
main - across eight slices of the rebased branch (findings V72-V96). It
removed the <form> wrap that let Enter publish a platform event, stopped the
extension's page button taking focus from Salesforce fields, let a modal close
on the first Escape when a popover is still open beneath it, and restored
several mouse affordances (card-body drag, click-to-toggle org details, toggle
label styling, date picker Tab-out). On 09-20 CI wiring was tightened (an
E2E-only change now runs E2E; the a11y-results check blocks a merge). On 09-22
nine reviewers each hunted one regression class across the whole diff (V97-V114;
headline: the permanent <main tabIndex=-1>). On 09-23 a seven-lens full-diff
review found no blockers; its minor findings and the open Copilot threads are
fixed as V115-V131, including the public page wording above.

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.01001001009870
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 Outdated
Comment thread libs/shared/ui-core/src/app/AppMainContent.tsx Outdated
Copilot AI review requested due to automatic review settings September 2, 2026 13:30
paustint and others added 29 commits September 23, 2026 20:29
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.
…es a record

Record form fields commit their value to the parent on blur. The Save button
gets that for free (the click blurs the field), but the shortcut fired with
focus still in the field, so the record was created or updated without the
value being typed - dropped silently, or "field required" on a field that
visibly had a value.

usePrimaryActionShortcut gains commitFocusedField: it blurs and refocuses the
focused text field, then runs the latest handler once React has applied the
commit. Emitting on every keystroke instead would re-render the whole form
per key on large objects.
Findings V72-V96 from an eight-slice review that asked a different question
from the earlier rounds: what gets worse than main for an existing user. Also
records the Copilot threads that were verified as false positives, and the
behavior changes that were reviewed and kept.
The affected guard matched against `nx show projects --affected --type=app`,
but nx types `jetstream-e2e` and `jetstream-desktop-e2e` as `e2e`, not `app`,
so naming them in `affected-projects` could never match. A pull request that
only touched a spec, the a11y baseline or a shared page object skipped E2E
entirely and still reported green.

Drop the `--type` filter. The guard's grep already anchors on the whole
project name, so matching stays exact.
`merge-e2e-reports` owns the assertion that the a11y specs actually produced
scan results, but it is not one of the protected contexts, so skipping or
deleting those specs turned that job red without stopping the merge.

The `e2e` gate — the context branch protection points at — now verifies the
merge job's result as well as the shards'.
Naming the Data History "View" and deploy "Metadata view" toggles made their
labels visible; `hideLabel` keeps them for screen readers only. Groups that
also hold a checkbox, the Pro upgrade button or the Google picker keep the
native group role, since radiogroup would present those controls as radios.
…s pass

With automatic activation every tab the arrow keys passed mounted its grid -
profiles x fields in Permission Analysis on a large org. Manual activation is
opt-in per Tabs, so lightweight tab sets still select on arrow. The extension
panel opts in too: its User Search tab focuses its input as it mounts, which
pulled focus out of the tab list on the way to Quick Links.
`pnpm install` runs husky's prepare script, so each release commit ran the
whole-workspace pre-commit hook. A lint or ratchet failure that reached main
would have blocked every release until someone fixed it.
The results grid claimed every Cmd/Ctrl+Enter for its Preview Changes modal,
even with nothing to preview, so the Reload shortcut never fired. It now claims
the key only with unsaved edits, a save in flight, or a press from an open cell
editor, whose commit may be the first edit.
Modal also closed on the Escape keyup, so the press the code editor used to
close its autocomplete or clear a selection closed the Query History modal and
discarded the edit. The keyup now closes only when the modal saw that press's
keydown, and never for an Escape that cancels an IME conversion.
Only Cmd was passed through, so on Windows and Linux Ctrl+Enter re-selected the
focused row instead of running the page or dialog shortcut.
Every radio pointed aria-describedby at its group's help-text and error ids,
which exist only when the group renders them. The group's fieldset already
carries that description, so Radio drops the reference and its idPrefix prop.
The button disabled itself natively once the export started, which dropped
keyboard focus to the page.
The chosen menu item unmounts with the menu, which left keyboard focus on the
page body.

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 review overview

🔵 Needs a closer look

Unresolved moderate accessibility and test assertion findings remain.

Review effort: Lite
Findings: None

This branch has not been deployed

No deployments
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