Catalog: give the query selector an accessible name - #5260
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #5260 +/- ##
==========================================
- Coverage 34.92% 34.92% -0.01%
==========================================
Files 741 741
Lines 23802 23804 +2
Branches 6429 6429
==========================================
Hits 8314 8314
- Misses 13744 13746 +2
Partials 1744 1744
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
fb3d493 to
2e05d7e
Compare
Local code review — findings and dispositionsReviewed as this layer's diff ( 1. Accepted —
|
| state | accessible name |
|---|---|
| before this branch | the query's name — label missing |
labelId only |
Select a query — selection missing |
labelId + id |
Select a query name1 |
So the field traded one omission for another: a screen-reader user could no longer tell which query was loaded, though the div still renders that name as text. Fixed in ae806fb by giving the Select an id, pinned by a test that fails when the id is dropped.
2. Accepted — the useId mock handed out a fresh id per call, not per component instance
utils/useId memoizes through React.useState(makeId); the spec's bare counter did not, so rendering the component twice moved the label id from test-id-2 to test-id-4. The committed snapshots held only because each snapshot test renders exactly once and QuerySelect happens to call useId twice before the id that lands in them — any later rerender, or an interaction that opens the menu, would renumber every id.
useId already takes its generator as a parameter, so the mock now delegates to the real hook and makes only the generation deterministic (bbb9319). The ids in the snapshots are unchanged.
3. Declined — "blanking on error assumes helperText is always present"
Correct that the two props are independent optionals, so an error whose message is empty would render a field blank with no explanation. But the suggested remedy — falling back to a placeholder when there is no helper — would put "Custom" back in the one place this branch exists to remove it from. A blank field under a red underline is the honest rendering of "nothing is loaded"; "Custom" asserts that a hand-written query is loaded. Left as is: if an error arrives with no message, that is the caller's contract to fix, not something the selector should paper over by claiming a query it does not have.
Also in this pass
Swept this layer's comments for narrative rather than constraint (2e05d7e): dropped "Blank under error" sitting above the branch that blanks under error, and two test comments retelling the labelId mechanism the component already states where a reader lands. What each keeps is the part the code cannot show.
The CHANGELOG entry is unchanged, deliberately. Across the whole PR the label is what newly reaches assistive tech — the selected query was announced before this branch and is announced again after the fix — so the entry as written already describes the net change.
Verification
npx vitest run app/containers/Queries (112 pass, from 111), npx tsc --noEmit, and oxfmt --check / oxlint on the touched files. Layers 3–9 were rebased onto the updated chain and force-pushed; each layer's own diff is byte-identical after the rebase, and every PR in the stack still shows only its own layer.
2e05d7e to
f275f2d
Compare
The query selector's `InputLabel` sat beside the `Select` without naming it: MUI v4 names the focusable `role="button"` display div through `labelId`, so assistive tech read the field as an unnamed button. Also stops the field asserting "Custom" — the label for a hand-written query — while its own helper text reports that the query list failed to load. Callers null the value on that path, and the fallback fired regardless of `error`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
addresses review findings f44, f27 One entry per shipped change, citing this PR rather than the omnibus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`labelId` on its own names the display div after the label, and because `aria-labelledby` wins over an element's contents, the selected query stops reaching assistive tech altogether. MUI builds the attribute as `labelId buttonId` and only emits `buttonId` when the Select is given an `id` (or a `name`): `SelectInput` derives it as `SelectDisplayProps.id || (name ? 'mui-component-select-' + name : undefined)`. So the field traded one omission for another. With a query selected, the accessible name was its name before this branch and just "Select a query" after it -- a screen-reader user could no longer tell which query was loaded, even though the div still renders that name as text beside them. Give the Select an `id` and both halves land: "Select a query name1". Pinned by a test that fails when the `id` is dropped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bare counter handed out a fresh id per *call*, where the hook it stands in for memoizes through `React.useState(makeId)`. Rendering the component twice moved the label id from `test-id-2` to `test-id-4`, so the committed snapshots hold only because each snapshot test renders exactly once and `QuerySelect` happens to call `useId` twice before the id that lands in them. Any later `rerender`, or an interaction that opens the menu, renumbers every id. `useId` already takes the generator as a parameter for this, so delegate to the real hook and make only the generation deterministic. The ids in the snapshots are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three passes added with this layer that narrate rather than constrain: "Blank under error" ahead of the branch that blanks under error; the test comment retelling the labelId mechanism the component already states where a reader lands, keeping only the symptom it exists to catch; and the error-state test repeating that rationale a third time. What each keeps is the part the code cannot show -- that callers null the value on a failed load, so the blank has to come from `error`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f275f2d to
0e69c8f
Compare
Description
Two defects in the shared query selector used by the Athena and ElasticSearch
query screens.
It has no accessible name. Its
InputLabelsits beside theSelectratherthan naming it. In MUI v4 the focusable node is the
role="button"display div,which takes its name from
labelId— so a keyboard or screen-reader user hearsan unnamed button where a sighted user reads "Select query".
It claims a query is loaded when the load failed.
renderValuefell back to"Custom"— the label for a hand-written query — whenevervaluewas null.Callers null the value on a failed load, so the field asserted "Custom" directly
beside its own helper text saying the query list could not be loaded. It now
renders blank on the error path and keeps "Custom" for the case it was written
for.
Review findings addressed
None folded in; the review raised nothing against this unit's code. Recorded:
shared
QuerySelectrather than at the caller. Left as is deliberately: thetrigger does not exist yet (
ElasticSearch.tsxpasses noerrorprop), sothere is no second call site to disagree with.
The same MUI v4 accessible-name problem is fixed twice, in two different
ways:
labelIdhere,SelectDisplayPropsin the Search sidebar (PR 3).Both are correct for their call site; the inconsistency is the finding. A third
case is still unfixed at
Queries/Athena/Workgroups.tsx:64-66. Unifying thisin the shared
Selectwrapper is a follow-up, not something either PR shouldreach across into.
Verification
8 tests, including the accessible name and the blank-under-error branch.
Position in the stack
PR 2 of 9, based on
stack/5217-1-data-products-lazy-adapter.This is part of the split of #5217 asked for in
f27. PRs 2
and 3 are the two smallest, uncontested accessibility fixes in that set and share
no symbol with anything above them — they are early in the stack so a release
gets them even if the contested units above stall.
TODO
🤖 Generated with Claude Code
Greptile Summary
This PR gives the shared query selector an accessible name and changes its null display during errors.
InputLabelto the MUI Select display node throughlabelId.Confidence Score: 4/5
The ElasticSearch custom-query regression should be fixed before merging because validation errors now blank a selector whose active mode is still “Custom.”
The accessible-name change is sound, but the shared display condition conflates Athena’s query-list load failure with ElasticSearch’s query-body validation error and hides a reachable active selection.
Files Needing Attention: catalog/app/containers/Queries/QuerySelect.tsx and catalog/app/containers/Queries/QuerySelect.spec.tsx
Important Files Changed
aria-labelledbyrelationship.Reviews (1): Last reviewed commit: "docs(changelog): entry for the query sel..." | Re-trigger Greptile