Skip to content

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

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

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

Conversation

@paustint

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, and guards against regressions. Program docs, findings log, and the
draft VPAT live in docs/accessibility/.

Infrastructure

  • Playwright axe scans of every app route + 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).
  • axeScan() component assertions in Vitest, pnpm a11y:scan-urls for
    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):

  • Combobox options were announced as nothing: arrow keys move real focus to the
    li, which had role="presentation" while role="option" sat on an inner div.
  • Icon-only buttons had no accessible names: generated icon svgs hardcode
    aria-hidden="true", which swallowed the label Icon was already setting.
  • Tabs implement the WAI-ARIA pattern (roving tabindex, Arrow/Home/End).
  • Skip links, aria-invalid wiring in Input/Select, labeled listboxes, dialog
    semantics on DatePicker, <html lang> on landing, contrast fixes.

Behavior changes to eyeball

  • Org cards drag from a dedicated grip handle; card-body dragging no longer
    initiates a drag (dnd-kit can't split pointer activation from the a11y role,
    and the card body is full of buttons).
  • Landing CTAs on pricing/desktop-app/goodbye darkened cyan-500 → cyan-700
    (white-on-cyan-500 is 2.3:1; unfixable without darkening).
  • Error toasts are now role="alert" (assertive); others stay polite.
  • Checkbox lists dropped listbox/option roles (options can't contain interactive
    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.

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 on lines +87 to +90
{/* The success checkmark is visual-only — announce the outcome (focus stays on this button) */}
<span role="status" aria-live="polite" className="slds-assistive-text">
{isCopied ? 'Copied to clipboard' : ''}
</span>
Comment thread scripts/a11y-merge-baseline.mjs Outdated
Comment on lines +27 to +47
let fileCount = 0;

for (const dir of resultsDirs) {
if (!existsSync(dir)) {
console.error(`Results directory not found: ${dir}`);
process.exitCode = 1;
continue;
}
for (const file of readdirSync(dir).filter((name) => name.endsWith('.json'))) {
const { scanKey, violations } = JSON.parse(readFileSync(join(dir, file), 'utf8'));
if (!scanKey) {
continue;
}
fileCount++;
const ruleIds = (violations || []).filter(({ impact }) => GATED_IMPACTS.has(impact)).map(({ id }) => id);
baseline[scanKey] = Array.from(new Set([...(baseline[scanKey] || []), ...ruleIds])).sort();
}
}

const sortedBaseline = Object.fromEntries(Object.entries(baseline).sort(([a], [b]) => a.localeCompare(b)));
writeFileSync(BASELINE_PATH, `${JSON.stringify(sortedBaseline, null, 2)}\n`);
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

{userHeaderWithoutNavigation && <HeaderNoNavigation />}
{children}
{/* Skip-link target only — several pages render their own <main> landmark, so this must stay a plain div */}
<div id="main-content" tabIndex={-1} className="outline-none">
return (
<>
<FocusMainContentOnRouteChange />
<main id={MAIN_CONTENT_ID} tabIndex={-1} style={{ outline: 'none' }} className={className} data-testid="content">

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

There are a few correctness/a11y issues in the new interactions (notably keyboard activation handling and skip-link focus visibility) that should be addressed before merging.

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

Review details

Suppressed comments (2)

libs/shared/ui-core/src/app/AppMainContent.tsx:32

  • The skip-link / route-change focus target is explicitly suppressing its focus outline. Since this element is intentionally focused for keyboard users, removing the outline can make focus location invisible (WCAG 2.4.7). Prefer keeping the default outline (or styling :focus-visible) rather than forcing outline: none.
    apps/landing/components/layouts/Layout.tsx:44
  • This skip-link target explicitly removes the focus outline (outline-none). Because it’s the element that receives focus after activating the skip link, hiding the outline can make focus hard to locate for keyboard users (WCAG 2.4.7). Consider removing outline-none (or replacing it with a visible focus-visible style).
  • Files reviewed: 294/402 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment on lines 25 to 29
if (isEnterOrSpace(event)) {
event.preventDefault();
event.stopPropagation();
onActionClick && onActionClick();
}
Both "Learn more" links now carry their subject in the link name, so the
rule has zero hits and joins the error tier.
…elines

oxlint cannot fail on the warn-tier jsx-a11y rules without failing on the 75
legacy sites, so scripts/a11y-lint-ratchet.mjs baselines the allowed hits per
file and rule and fails on growth. The same run requires axeScan() in any
libs/ui spec not grandfathered in and an a11y-baseline.json entry for every
swept APP_ROUTES key, so a new route is gated at commit time instead of in a
20-minute E2E job. Runs in pre-commit and the CI lint job.
…ation

"Remove Org" and the keep/confirm pair replace each other in the DOM, so
focus fell to <body> on both transitions. Focus now lands on "Keep Org"
(with the warning attached as its description) and returns to "Remove Org"
on keep. Removing the org unmounts the popover with its trigger, so the org
switcher takes focus once the selection has moved off the removed org.
The toggle reveals content below it, so it now carries aria-expanded and
aria-controls (both valid on a checkbox in ARIA 1.2) and a label that says
what it shows; the revealed region stays in the DOM while collapsed so the
reference always resolves.
…g focused

Panel remembered document.activeElement at open time, which is <body> after a
mouse click in Safari, so closing the SOQL Query drawer left focus nowhere.
Panel now ignores <body> and falls back to an explicit returnFocusTo target;
the query results toolbar button is wired up as that target.
Screen readers announce a gridcell's content but nothing about the editor or
the popover trigger behind it. GridBody now points the focused cell's
aria-describedby at hidden hints ("Editable. Press Enter to edit." /
"Contains controls. Press Enter to interact...") right before focusing it,
so the hint is read with the cell. Applied to the single focused cell only.
…nter or Space

The input handles keys on keyup, so the release of the Enter/Space that
selected an option (keydown, in the list) landed on the freshly focused input
and hit its "closed: open the list" branch. The keyup that completes a
selection is now skipped.
…saving

Enter deliberately never submits the long record form; the app-wide primary
action shortcut now saves instead and is announced on the Save button. The
spinner shown while loading or saving is mirrored to a status region.
…s popover

The recent queries were click-only rows. They are now the shared roving
listbox: Enter/Space loads a query, Cmd/Ctrl+Enter executes it and
Shift+Enter restores it (mirroring the click modifiers), ArrowDown from
"View All History" enters the list, and Tab leaves the compact SOQL editor
instead of indenting.
…o the record modal

Recent records in the View Record Details popover were click-only rows and a
natively disabled Submit made Tab leave (and close) the popover; the list is
now the shared roving listbox, ArrowDown from the id input enters it and
Submit stays focusable while aria-disabled. Both this popover and Search
Users close before opening the record modal, so the dialog is announced and
takes focus instead of the popover keeping it.
The default row height was 28.5px, so every other row was translated to a
half-pixel offset and its 1px border straddled two device pixels on 1x
displays (faint, uneven lines, most visible in Safari). Row heights are now
whole pixels: the default is 29 and callback/measured heights round up.
The modal remounts in view mode after saving, so a screen reader only heard
the new title; a polite "Record saved" is now announced.
autoFocus on the group name input ran before floating-ui recorded which
element opened the modal, so the modal "returned" focus to its own detached
input and keyboard focus fell to <body>. Modal now takes an initialFocus ref
(floating-ui's supported way to pick the first focused element) and the group
modal uses it; deleting a group or the last org hands focus to "Create New
Group" since their menus unmount with the card.
dnd-kit's default announcements interpolate the draggable and droppable ids,
which are UUIDs on this page. The cards now carry their labels in the drag
data and a custom Accessibility plugin reads them ("Acme Sandbox is over
Production Orgs"); the drag handle is named "Move <org>". @dnd-kit/dom
becomes a direct dependency because it owns the Accessibility plugin.
Input, Select and Textarea already stamp aria-invalid through
associateErrorsWithControls, but Combobox, Picklist, Checkbox, DatePicker,
RadioGroup, Slider, the file selectors and the manual record-id input only
linked their error text — a screen reader read the message without ever
saying the field was invalid (the load records field mapping rows use the
Combobox).
…accept or reject

The banner was an anonymous div with "Accept" and "Reject" buttons, so a
screen reader user heard two bare verbs with no context. It is now a region
named by its heading, and the buttons read "Accept cookies" / "Reject
cookies" with the explanation as their description.
floating-ui records the return-focus target one render after the modal
mounts, so a child with autoFocus became that target and, unmounting with the
modal, left focus on <body>. Modal now captures the opener during its first
render and, two frames after closing, focuses it if nothing else took focus.
Covers the remaining autoFocus modals (email change, API history, query
history) without touching them.
A cell whose only interactive content is links (open in Salesforce) is now
described as "Contains a link. Press Enter to move to it" instead of the
generic controls hint; the Automation Control quick filter also gets an
accessible name that says what it filters.
…mation Control's loaded count

SalesforceLogin links only showed a decorative new-window icon, so screen
readers never heard that they leave the app. Automation Control mounts its
table with rows already loaded, which bypasses the grid's filter-count
announcement, so the page announces the loaded item count itself.
…errors

The next-step button was disabled with no explanation. Its gate is now the
same pure function that produces the reason ("Select an object and upload a
file to continue", "Resolve 2 mapping errors to continue", ...), shown as a
tooltip on hover and focus and mirrored as the button's accessible
description while it stays focusable via aria-disabled. The field-mapping
step also announces its error count as it changes.
The hint said "press Enter to interact" for every cell with a control, but
Enter toggles a lone expander, opens a lone link, activates a lone control,
enters a lone text field, and only switches to Actionable mode for cells
with several controls. The hint now follows the same decision tree.
Copilot AI review requested due to automatic review settings September 2, 2026 14:44

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

OrgsCombobox’s new ref handling is incorrect for React function components (won’t work at runtime and will warn), and the web-extension popup select introduces an invalid self-referential aria-describedby.

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

Review details
  • Files reviewed: 294/443 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment on lines 89 to 92
<select
id="color-scheme-select"
id="color-scheme"
aria-describedby="color-scheme"
className="slds-select"
Comment on lines 126 to 130
export interface OrgsComboboxProps {
/** Lets a parent move focus into the org switcher, e.g. after the selected org (and its info popover) is removed */
ref?: Ref<ComboboxWithGroupedItemsRef>;
orgs: SalesforceOrgUi[];
selectedOrg: Maybe<SalesforceOrgUi>;
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