Skip to content

fix(automation-control): surface deploy errors and fix stale loading rows in review modal - #2035

Draft
paustint wants to merge 84 commits into
chore/wcag-a11y-scanningfrom
fix/automation-control-deploy-review-modal
Draft

fix(automation-control): surface deploy errors and fix stale loading rows in review modal#2035
paustint wants to merge 84 commits into
chore/wcag-a11y-scanningfrom
fix/automation-control-deploy-review-modal

Conversation

@paustint

@paustint paustint commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

TODO rebase/re-do PR after dependent PR merges. rebases on target pr caused this one to become out of date.

Fixes all four follow-ups from the deploy review modal (Automation Control → Review Changes). Branched from chore/wcag-a11y-scanning per the issue note (shared files); retarget to main after #2011 merges.

Changes

  1. Stale loading row after completion (bug): rows that failed metadata retrieval were flipped to Deploying along with everything else when the deploy started, but deployMetadata filters out rows with a retrieveError (in both the tooling-API and metadata-API paths), so they never received a result and sat on a spinner forever. They now keep their Error state.
  2. Error rows have a visible treatment (P2): error rows get the grid's standard save-error red row background via rowClass, in addition to the icon + text.
  3. Error message is a visible column (P2 / WCAG 1.4.13-adjacent): new wrapping "Error Message" column with dynamic row height (getWrappedTextRowHeight, same pattern as the apex-test-runner Message column). The status tooltip and copy-to-clipboard remain as secondary affordances, and all three now read from one shared getDeploymentItemErrorMessage — which also falls back to retrieve errors, so retrieve-failed rows no longer show "An unknown error has occurred" while holding a real message.
  4. Visible success-vs-error summary (P3): a results banner above the table renders the same computed "N succeeded, X failed" string the assistive live region announces, so the visible and announced summaries share one source.

Also marks the findings-doc entry resolved and adds unit tests for the error-message extraction (deploy-error join, retrieve-error fallback, generic fallback, non-error rows).

Closes #2029

Includes automated PR-review feedback and the E2E Unsubscribe locator disambiguation that the icon-button rename required.
- arrow/Space/Enter selection and roving tab order work end to end
- nested field-list filters no longer move the parent list
- row-local ArrowRight reaches secondary controls
- field-list tooltips and copy badges are reachable and announced
- keyboard shortcuts inside tooltips are announced to screen readers
…ty, and filter jitter

- visible (inset) focus indicators in lists and comboboxes on Safari
- arrow keys work in portaled combobox/picklist menus; option roles sit on the focused element
- menus no longer jump as stale filters clear
…nnounced labels

- load-type radios (and four more groups) had per-value names, making each its own group
- groups expose role=radiogroup with the legend announced on entry
…sibility

- inline history editing manages focus and alignment
- save-query is a real toggle button; focus survives saving a favorite
…alogs they open

- focus returns to the trigger after selecting an item
- the trigger is focused BEFORE the selection callback so a modal it opens records the right return target
…sion, result-count announcements

- the clear (X) button returns focus to the input
- password managers no longer inject UI/screen-reader noise
- settled filter result counts are announced in grids and list panels
Browsers expose no screen-reader detection API, so Monaco's screen
reader mode is an explicit opt-in (Settings > Code Editors on web,
desktop, and extension), persisted in localStorage and applied to every
editor via the shared wrapper's default options; everyone else keeps
Monaco's own 'auto' detection. The setting documents Alt+F1 editor
accessibility help and the Ctrl(+Shift)+M tab-trap toggle.
… (M36-M56)

C8 (dnd keyboard reorder) and C10 (virtualization row counts) verified
in the manual pass; C9 resolved via the editor screen-reader setting.
M36-M56 record the second sweep covering every remaining surface, and
the open list now reflects what actually remains (jgrid retoken,
nested-interactive decision, copy-button name sweep, VPAT finalization).
useAnnouncer owns the clear-then-set re-announce idiom that five features
hand-rolled (with drift: one grew an isMounted guard the others lacked),
and ShowingCountStatus renders the visible "Showing X of Y" line and its
debounced announcement from one string — the two had already drifted apart
in ListWithFilterMultiSelect. The permission manager tables now announce
through one table-level region via the table context instead of mounting
a live region per action column.
…s renderer

The severity-to-cellClass mapping and sr-only View-details button were
triplicated: byte-identical blocks in the two permission trees plus a
divergent copy in the export grid that sniffed the class string for
'--error'. withFindingDetailsCell owns both halves from a severity
accessor; rendered DOM is unchanged.
The skip-link target, main landmark, and route-change focus wiring were
copy-pasted across the web, desktop, and extension shells, and the
'main-content' id had two independent definitions (SkipToContent's
default vs ui-core's constant). AppMainContent owns the main element and
FocusMainContentOnRouteChange; MAIN_CONTENT_ID now lives in @jetstream/ui
next to SkipToContent, its only other consumer. Shell DOM is unchanged.
The canvas shell was the only one without a skip link, main landmark, or
route-change focus management — the copy-paste block never reached it.
One-line adoption of AppMainContent plus SkipToContent brings it to
parity with the other three shells.
…ract

The stay-focusable-while-disabled idiom was hand-rolled at 14 sites in
three shapes (conditional-undefined, plain boolean rendering
aria-disabled="false", and a Link preventDefault variant), with the same
explanatory comment pasted at seven of them and the not-browser-enforced
invariant documented only in the ui-styles CSS. ariaDisabledButtonProps
returns the attribute and the guarded onClick together so they cannot
drift; the disabled path calls preventDefault, covering links too.
… DOM convention

PerformLoad focused a new run's tab via document.getElementById('tab-' +
id), hard-coding Tab.tsx's private anchor-id scheme — a rename there
would silently break the retry focus hand-off. TabsRef gains focusTab(id)
next to changeTab, owned by the component that renders the ids.
…guards

The popover/modal/dropdown/dialog selector existed as three copies
(keyboard navigation, GridBody refocus guard, GridContainer blur guard)
and had already diverged — the blur guard was missing [role=dialog], the
same drift that previously lost .slds-dropdown in one copy.
GRID_OVERLAY_SELECTOR is now the single list; GridContainer appends its
site-specific .jgrid-editor.
…r row

M31 described the polling implementation that has been replaced by
deterministic row ids plus an effect; C3 still called TimePicker a thin
Picklist wrapper after M39 rebuilt it as a combobox.
… navigation

Every accordion header was a page tab stop — 77 of them on Account's
Related Objects (Subquery) list — making the rest of the query builder
unreachable by keyboard. Accordion gains an opt-in singleTabStop
roving-tabindex mode: ArrowUp/ArrowDown move between headers (wrapping,
skipping disabled sections), Home/End jump to the ends, Enter/Space
toggles natively, and Tab moves from the header into the open section's
embedded controls. The tab stop starts on the initially open section so
a navigator jump lands where the user left off. Other accordions keep
the APG default of a tab stop per header. Logged as M57.
…pened overlays

React synthetic events propagate through portals following the REACT
tree, so pressing Enter/Space on a button inside a portaled overlay a
cell renderer opened (the record lookup popover) reached the grid's
root onKeyDown, which activated the active cell's control and
toggle-closed the popover before the real button's click could fire.
The handler now ignores events whose DOM target is outside the grid
root — those keys belong to the overlay.
… loads

The View Record button's autoFocus fired when the footer mounted AFTER
the async record fetch, yanking focus from the close button mid-
interaction. Focus now starts and stays on the close button, per the
popover's normal initial-focus behavior. Logged as M58.
…stand-alone PR

Error-row visibility, tooltip-only error messages, a visible
success-vs-error summary (the assistive message already computes it),
and the stale loading row are all pre-existing and deferred — captured
here so the conformance trail points at the follow-up work.
…ists

Every test class was two tab stops (chevron + checkbox) plus one per
expanded method, so keyboard users could not get past the list — the
class tree and suite-membership list are now single tab stops with
arrow-key navigation. Run actions moved to the page header (matching
other pages) with the Cmd/Ctrl+Enter primary-action shortcut; launch
state lifted into useApexTestRunLauncher so the header owns the buttons.
Launching a run now focuses the Test Runs tab instead of dropping focus
to <body>, and the scroll containers no longer clip focus rings. The
suite modal separates Select All from the list and saves with the same
shortcut (the page-level run shortcut is suspended while it is open).
Logged as M59.
The 28.5px default row crowded the badges against the row edges, and the
full-height click-target wrapper defeated the cell's own flex centering,
pinning cell content to the top.
DataTable's quickFilterText is inert unless includeQuickFilter is also
set — the grid's global filter only engages when both are present, and
only the text was passed.
…popover close

A filter matching zero rows made every grid key return early, so the
header row — the only way to broaden the filter again — was unreachable.
Navigation keys now route to the header/summary rows, Tab-in seeds the
header, and a stale active cell snaps to the header keeping its column.

Closing a header-filter popover could also strand focus on the filter
icon: an overlay unmounting with focus inside it fires no focusout, so
the return-focus-to-cell check never ran. It now runs on focusin too,
and a return target whose row was filtered away falls back to that
column's header cell. Logged as M60.
…rows in review modal

Rows that failed metadata retrieval were flipped to 'Deploying' along with
everything else, but deployMetadata skips them so they never received a
result and sat on a spinner after the deployment finished. Error rows now
keep their state, get the grid's standard save-error red treatment, and
show their full error message in a visible wrapping column (the tooltip
remains, and now also covers retrieve errors that previously showed
"unknown error"). A visible results banner shares one computed
"N succeeded, X failed" summary with the assistive live region.

Closes #2029
@paustint
paustint force-pushed the chore/wcag-a11y-scanning branch from cbfa1ca to 77f52f7 Compare September 2, 2026 13:30
@paustint
paustint marked this pull request as draft September 5, 2026 14:22
@paustint
paustint force-pushed the chore/wcag-a11y-scanning branch 8 times, most recently from 2fcaa09 to fcb46e4 Compare September 14, 2026 01:14
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.

1 participant