| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
A customer agreement requires WCAG 2.1 AA conformance efforts and an accessibility conformance report (VPAT) on request. Playwright specs now axe-scan every app route plus key interactive states, component specs can assert via axeScan() from @jetstream/test-utils, and docs/accessibility/ holds the findings log and draft report. Scans gate on a11y-baseline.json as a ratchet seeded from a full local run: violations already in the baseline pass, anything new fails, and the file should only ever shrink as findings are remediated (pnpm a11y:merge-baseline regenerates it). axeScan() filters Floating UI's focus-guard sentinels, which trip axe's button-name rule despite never holding focus.
First remediation pass from the WCAG 2.1 AA audit — the a11y baseline drops from 89 serious/critical entries to 10, with zero critical rules remaining. Three shared-component defects accounted for most of it: - Combobox/Picklist/FormGroupDropdown set aria-controls while the listbox popover is unmounted (invalid reference on every page); it is now set only while open, per the ARIA 1.2 combobox pattern. - Icon with omitContainer + description produced an svg that kept its baked-in aria-hidden="true", so icon-only buttons had no accessible name; the clone now strips aria-hidden and adds role="img". Header popovers, grid row actions, and list refresh/filter buttons also get explicit labels. - List rendered role="listbox" with no name and ListItemCheckbox passed label="" — listboxes now take ariaLabel and checkboxes are labeled from their heading. Remaining baselined rules (nested-interactive, definition-list, link-in-text-block) need markup restructuring and are tracked in docs/accessibility/audit-2026/findings.md.
…nding lang axe scans of the secondary surfaces (17 landing/auth pages, ~210 docs pages) via scripts/a11y-scan-urls.mjs, which needed the @playwright/test import and an explicit browser context. Docs content pages came back clean; landing was missing <html lang> everywhere (fixed) and shares a footer contrast issue with the docs homepage (tracked in the findings log). The manual-checklist doc is the beginner-oriented runbook for the keyboard / VoiceOver / visual passes that automated scanning cannot cover — its results feed the remaining TBD rows of the VPAT draft.
Second remediation pass — the axe baseline is down to a single rule on 3 pages (nested-interactive on list rows with trailing popover triggers, which needs a design decision and stays documented in the findings log). All 47 scans across app, landing, and docs are otherwise clean. Highlights beyond mechanical labeling: - Combobox options were announced as nothing: focus lands on the li, which had role="presentation" while role="option" sat on an inner div. The li now carries the option semantics. - Tabs implement the WAI-ARIA pattern: roving tabindex plus orientation-aware Arrow/Home/End with automatic activation. - Skip links in web/desktop shells and landing layout; landing wraps page content in a focusable #main-content target (a div, not <main> — several pages render their own main landmark). - Checkbox lists drop listbox/option roles: options cannot contain interactive children and focus moves to the labeled checkboxes, which announce their own state. E2E page objects updated to match. - Org cards drag from a dedicated handle so dnd-kit's button role no longer wraps a card full of interactive controls. This changes mouse UX: card-body dragging no longer initiates a drag. - Error toasts are role="alert"; Input/Select stamp aria-invalid and error association onto native control children. - Landing cyan CTAs/links darkened to cyan-700 to pass contrast. The remaining subquery E2E flake was verified pre-existing: a control run with the Tabs changes stashed failed at the same rate.
There was a problem hiding this comment.
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:
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. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Sorry, something went wrong.
- Remove unused TITLE binding from the a11y page-sweep route loop (CodeQL) - Default the a11y URL scan key port to 443 for https so http/https scans of the same host cannot collide and evidence reflects the real port - Skip the axe scan (and fail the run) when navigation fails in a11y-scan-urls.mjs instead of scanning the previously loaded page - Name debug log viewer cells by rendered content, labeling 'View log' only when the cell is truly empty (0/false values render content) - Align List.ariaLabel JSDoc with the optional prop contract
…name
The listener card's clear-all icon button gained an accessible name
("Unsubscribe from all events and clear results") in the a11y pass, and
getByRole name matching is substring by default — so the lax
'Unsubscribe' locator now resolved to two buttons and strict mode failed
the click deterministically in CI shard 4. exact: true, mirroring the
existing Subscribe locators (which had the same problem with
'Subscribe' vs 'Unsubscribe').
There was a problem hiding this comment.
Copilot reviewed 79 out of 81 changed files in this pull request and generated no new comments.
Files not reviewed (1)Previously missed (2) — in code that hasn't changed since the last review.
libs/ui/src/lib/list/List.tsx:193
Sorry, something went wrong.
Dragging now starts from the grip handle only, but the card body kept its cursor: grabbing from the whole-card-drag era (and slds-box_link's hover rule shows a pointer). The grab/grabbing cursors now live on the handle alone; the card shows a default cursor.
The app's <base href="/app"> makes a bare #fragment href resolve against the base URL, so following the skip link reloaded the page at /app instead of jumping within the current page. Focus now moves programmatically with the default navigation prevented.
Every group card renders identical "Make Active"/"Edit"/menu buttons, so
tabbing or listing buttons in a screen reader gave no clue which group
each acted on. Buttons now carry the group name in their accessible name
("Make Active - <group>", keeping the visible label as a prefix per
Label in Name), and Card gained an ariaLabel prop so entering a card
announces its group. E2E locators are card-scoped substring matches and
are unaffected.
Three pre-existing keyboard bugs surfaced by the manual audit: - SobjectList/SobjectListMultiSelect created ulRef with createRef in the render body and never attached it, so ArrowDown from the object filter silently did nothing (the fields list wires the same handler correctly). - Space on a field checkbox toggled twice: the native activation fires change AND a bubbled click, and the row's click handler toggled it back. The row handler now ignores events originating from the checkbox input/label. - Every field checkbox sat in the page tab order, so tabbing out of a 500-field list meant 500 presses. Checkboxes are tabIndex=-1; the list is one tab stop and arrow keys move between rows (roving tabindex).
Manual audit follow-ups on the query fields list:
- Per-row secondary controls stay out of the page tab order and are
reached with ArrowRight from the row checkbox (ArrowLeft returns) —
the "where is this field used" icon on every row was otherwise
re-flooding the tab order the roving-tabindex fix just cleaned up.
- Field checkboxes announced their raw id ("Account|Rating"): the rows
pass a ReactNode heading, so the label fallback bottomed out at id.
getContent now carries an explicit label and the field list provides
"Label (ApiName)".
SearchInput dispatched Up/Down on keyup but let the keydown bubble; a nested child-object field list sits inside a parent List row, so the parent's keydown handler moved focus to a root row before the nested filter's keyup could focus its own list. The keydown is now consumed when arrows are handled. The View/Hide fields expand buttons also leave the page tab order (reached with ArrowRight, like other row controls).
Tooltip triggers were plain spans: with no focusable element inside, keyboard users could never reveal the tooltip (WCAG 1.4.13), and the picklist/formula type badges layered a click-to-copy action on top with no keyboard path at all. Tooltip now accepts triggerTabIndex, and an interactive trigger (onClick + tabindex) exposes role="button" with Enter/Space activation. The field rows wire these at tabIndex=-1 so they join the row-local ArrowRight cycle instead of the page tab order.
KeyboardShortcut rendered only visual kbd glyphs — '⌘' announces as
"place of interest sign" at best, and the tooltip's description attaches
on open, after focus, so it was often never read at all. The widget now
renders assistive text ("Command + Enter") with the visuals aria-hidden,
and exports getAriaKeyshortcuts so triggers can announce the shortcut at
focus time via the aria-keyshortcuts attribute — wired on the query
Execute and Back buttons as the exemplar.
Manual audit findings on the query page: - Arrow keys closed portaled combobox menus (query filter Operator, order-by Nulls) in Chrome: options receive real focus during arrow navigation, but with usePortal they live outside the container the blur handler checked, so the first arrow press blurred-and-closed. The popover ref is now part of the containment check. - No visible focus indicator on lists and combobox options in Safari (and a clipped one in Chrome): these elements get no default focus ring in Safari, and an outline drawn outside the element is clipped by the scrolling container. Explicit :focus-visible outlines with outline-offset: -2px draw inside the element on the list, its rows, and combobox options. The ul keydown lint warning is suppressed with rationale: the ul is the composite's single tab stop delegating for focusable rows, a pattern jsx-a11y cannot recognize.
Two more manual-audit findings on the query page: - PopoverContainer forwarded its ref in a mount-time effect, but a non-eager popover renders nothing at mount and the effect deps never fire again — the forwarded ref stayed null forever. The blur handler's portal containment check therefore never passed and arrow keys still closed portaled combobox menus (filter Operator). The ref is now a composed callback ref on the floating div. - PicklistItem (order-by Order/Nulls and friends) had the same role-on-the-wrong-element bug as ComboboxListItem: focus lands on a role="presentation" li while role="option" sat on an inner div, so arrowing announced nothing — and no :focus-visible style meant Safari showed no focus ring at all. Option semantics moved to the li with an inset focus outline.
The spacer above the button was an empty label with a hardcoded 15px margin, a couple px short of a real label row, so delete buttons in order-by/filter rows sat visibly high. The spacer is now an invisible label with real content, inheriting the exact label metrics.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
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, and guards against regressions. Program docs, findings log, and the
draft VPAT live in docs/accessibility/.
Infrastructure
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).
landing/docs, and one jsx-a11y lint rule promoted to error (ratchet documented
in .oxlintrc.json).
Remediation — baseline went 89 serious/critical entries → 3, zero critical
violations across 47 scans (app, landing, docs):
li, which had role="presentation" while role="option" sat on an inner div.
aria-hidden="true", which swallowed the label Icon was already setting.
semantics on DatePicker, <html lang> on landing, contrast fixes.
Behavior changes to eyeball
initiates a drag (dnd-kit can't split pointer activation from the a11y role,
and the card body is full of buttons).
(white-on-cyan-500 is 2.3:1; unfixable without darkening).
children) — E2E page objects now assert on the row checkbox instead.
Deliberately not done: nested-interactive on list rows with trailing
popover triggers (3 pages) needs a design decision and stays baselined with
rationale in the findings log. The manual audit (keyboard/VoiceOver/visual;
runbook in docs/accessibility/audit-2026/manual-checklist.md) is what remains
before the VPAT draft can be finalized.
CI note: the query-builder subquery "Filter child objects" flake is
pre-existing — verified by a control run with these changes stashed, which
failed at the same rate.