A11Y: inline category errors use display labels as description IDs #831

Open
opened 2026-10-02 13:22:05 +00:00 by kayg · 1 comment
Owner

Context

Source accessibility review of origin/dev at c4a61e8cf090170f35b1bed3350d9de20c83ecd5, job rev-a11y, assigned #427. The inline category field names its validation error through an ID built from the category's display label.

Evidence

  • apps/web/src/lib/components/money/AssignedCell.svelte:92: aria-describedby is assigned-error-${category}.
  • apps/web/src/lib/components/money/AssignedCell.svelte:102: the error span uses the same interpolated value as its id.
  • apps/web/src/routes/money/[budget]/[month]/+page.svelte:268: the caller passes category.name, not an ID.

For a label such as Long label, the attribute becomes aria-describedby="assigned-error-Long label". ARIA parses this as two ID references. Neither points to the span whose ID contains the space. Display labels are also not guaranteed to be unique across groups. The alert can announce the new error, but the invalid field's persistent error description is not connected. This is an Info and Relationships (WCAG 1.3.1) gap.

Expected and exact fix

Use a unique component ID from Svelte's $props.id() or a supplied stable item ID with a fixed suffix. Reuse it for the error element and its reference. Keep the visible category label for the accessible field name. Do not sanitize the display label into an ID: that can still collide.

Test idea

Render a category with spaces and two categories that have the same display label in separate groups. Trigger validation without API writes. Check that each invalid field has exactly one existing error-description target, with unique IDs. Inspect each field's accessible description after the initial alert announcement and after focus returns. Use no real financial data in fixtures or issue comments.

Duplicate search

Searches: "assigned-error" and "error references", both with no results. This is one issue for the inline category-field family.

# Context Source accessibility review of `origin/dev` at `c4a61e8cf090170f35b1bed3350d9de20c83ecd5`, job rev-a11y, assigned #427. The inline category field names its validation error through an ID built from the category's display label. # Evidence - `apps/web/src/lib/components/money/AssignedCell.svelte:92`: `aria-describedby` is `assigned-error-${category}`. - `apps/web/src/lib/components/money/AssignedCell.svelte:102`: the error span uses the same interpolated value as its `id`. - `apps/web/src/routes/money/[budget]/[month]/+page.svelte:268`: the caller passes `category.name`, not an ID. For a label such as `Long label`, the attribute becomes `aria-describedby="assigned-error-Long label"`. ARIA parses this as two ID references. Neither points to the span whose ID contains the space. Display labels are also not guaranteed to be unique across groups. The alert can announce the new error, but the invalid field's persistent error description is not connected. This is an Info and Relationships (WCAG 1.3.1) gap. # Expected and exact fix Use a unique component ID from Svelte's `$props.id()` or a supplied stable item ID with a fixed suffix. Reuse it for the error element and its reference. Keep the visible category label for the accessible field name. Do not sanitize the display label into an ID: that can still collide. # Test idea Render a category with spaces and two categories that have the same display label in separate groups. Trigger validation without API writes. Check that each invalid field has exactly one existing error-description target, with unique IDs. Inspect each field's accessible description after the initial alert announcement and after focus returns. Use no real financial data in fixtures or issue comments. # Duplicate search Searches: `"assigned-error"` and `"error references"`, both with no results. This is one issue for the inline category-field family.
Author
Owner

Final audit probe, exit 0, using one Chromium browser and no product build:

REPRODUCED #740: hidden explicit initial target leaves focus outside the trap.
REPRODUCED #740: a hidden last candidate lets Tab leave the trap.
REPRODUCED #848 supporting behavior: the shared trap permits programmatic background focus.
REPRODUCED #831: a category label with a space resolves no error-description ID references.

The #848 case tests the actual shared focusTrap action with visible controls, not the complete overlay. Source evidence in the issue establishes the missing shell inert binding. The #831 case checks browser ID-reference parsing with a neutral label, not the full category editor. These probes support the findings and do not replace production acceptance checks.

Final audit probe, exit 0, using one Chromium browser and no product build: ```text REPRODUCED #740: hidden explicit initial target leaves focus outside the trap. REPRODUCED #740: a hidden last candidate lets Tab leave the trap. REPRODUCED #848 supporting behavior: the shared trap permits programmatic background focus. REPRODUCED #831: a category label with a space resolves no error-description ID references. ``` The #848 case tests the actual shared focusTrap action with visible controls, not the complete overlay. Source evidence in the issue establishes the missing shell inert binding. The #831 case checks browser ID-reference parsing with a neutral label, not the full category editor. These probes support the findings and do not replace production acceptance checks.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#831
No description provided.