Skip to content

fix: resolve a11y errors#3347

Open
aresnik11 wants to merge 14 commits into
mainfrom
ajr-sb-a11y
Open

fix: resolve a11y errors#3347
aresnik11 wants to merge 14 commits into
mainfrom
ajr-sb-a11y

Conversation

@aresnik11
Copy link
Copy Markdown
Contributor

@aresnik11 aresnik11 commented May 13, 2026

Overview

This PR fixes Storybook accessibility violations surfaced after enabling the a11y addon's error test mode (previously todo). Changes fall into three buckets: component-level a11y fixes, story/documentation updates so examples demonstrate correct patterns, and unit test updates to match the new behavior and satisfy codecov.

Component changes

ConnectedFormGroup

  • ConnectedFormGroup now passes htmlFor={${name}-${uniqueIdSuffix}} to their underlying form elements and the same value is passed to the form label
  • Why: ConnectedFormGroup renders a label via FormGroupLabel which sets htmlFor={id || name} but there was no id to match that htmlFor within the input, so the label association did not work. Most of the underlying form elements have id={id || rest.htmlFor} so setting htmlFor upstream matches them up

List

  • ListRow now explicitly sets as={isTable ? 'tr' : 'li'} on the row element.
  • Why: Ensures table list rows render as <tr> elements for correct table semantics.

DataList

  • TableRow renders screenreader-only "Loading data" text for columns with type: 'header' while in a loading state.
  • Why: Loading shimmer placeholders are visually empty; header-type columns need an accessible label so assistive technology users know content is loading.

Tabs

  • TabNavLink default role changed from 'tab' to 'button'.
  • Why: TabNavLink is used inside TabNav (<nav>) for link-based navigation, not inside a tablist. Using role="tab" was semantically incorrect and failed a11y checks. Consumers can still override via the role prop.

Menu

  • MenuSeparator: moved role="separator" from the outer <li> to the inner visual divider <Box>.
  • Why: A <li> with role="separator" is not valid; the separator role belongs on the element that visually represents the divider.
  • MenuItem: icon-only menu items now fall back to an explicit aria-label prop when no label is provided.
  • Why: Icon-only items need an accessible name; this allows callers to supply one directly.

Tag

  • Tag dismiss button always receives an aria-label (Dismiss ${children} Tag), even when disabled.
  • Why: Setting aria-label="" on a disabled dismiss button removed its accessible name entirely.

Storybook & documentation updates

Stories were updated to pass a11y checks and demonstrate correct labeling patterns:

Form inputs — htmlForid migration

Across Input, Select, SelectDropdown, TextArea, FormGroup, GridForm, and ConnectedForm stories:

  • Replaced incorrect use of htmlFor/name on input components with id on the input and htmlFor on the parent FormGroup.
  • Updated MDX docs for Input, Select, and SelectDropdown to reflect the correct pattern: provide htmlFor on FormGroup and id on the input.
  • Standalone Select/SelectDropdown stories without a FormGroup wrapper now include aria-label where needed.

Other story fixes

  • TextArea: wrapped in a FormGroup decorator so the textarea has a visible label.
  • Toggle: added default label prop.
  • Checkbox: fixed Unchecked story to actually render unchecked (checked: false).
  • Rotation: added aria-label to the demo toggle button.
  • Layout: added tabIndex={0} to scrollable overflow examples so keyboard users can focus them.
  • AccordionButtonDeprecated (Blue): wrapped in a navy Box for sufficient color contrast.
  • Badge: fixed invalid bg="green" token to bg="green-400".
  • PreviewTip: added aria-label to icon-only avatar example.
  • Tabs (Controlled): removed invalid label prop from Input; fixed typo in htmlFor.
  • ImageGallery: associated control labels with inputs via <label htmlFor> + matching id.
  • ConnectedForm / ConnectedFormGroup / GridForm: fixed duplicate field names and missing label associations.

PR Checklist

  • Related to designs:
  • Related to JIRA ticket: GMT-1630
  • Version plan added/updated (or not needed)
  • I have run this code to verify it works
  • This PR includes unit tests for the code change
  • This PR includes testing instructions tests for the code change
  • The alpha package of this PR is passing end-to-end tests in all relevant Codecademy repositories

Testing Instructions

  1. Confirm all the tests pass (a11y failures will now fail in CI)

ConnectedForm / ConnectedFormGroup

  1. Open Organisms/ConnectedForm stories.
  2. Tab through a form with Input, Select, TextArea, Checkbox, and RadioGroup.
  3. Confirm each field’s visible label is announced by the screen reader / shows in the a11y tree as the control’s accessible name.
  4. Click a FormGroupLabel — focus should move to the associated input (label–input association via generated id: {name}-{useId suffix}).
  5. If you pass an explicit id prop on a field, the label should still associate correctly.

ConnectedCheckbox

  • Checkbox label click toggles the checkbox.
  • htmlFor uses the same unique suffix pattern as other connected fields.

DataList / DataGrid (loading)

  1. Use a DataGrid story or app usage with loading={true} and a header-type column.
  2. With a screen reader (VoiceOver/NVDA), confirm “Loading data” is announced in header cells while shimmer is shown.
  3. Non-loading grids should be unchanged.

List (table mode)

  • Expandable rows inside a table layout should render as <tr> (not invalid <li> inside <table>).
  • Inspect DOM on a List table story if available.

Menu

  • MenuSeparator: separator role is on the inner divider Box, not the <li> wrapper (no nested interactive/separator conflict).
  • MenuItem (icon-only): if you pass aria-label on an icon-only item, it should be used when the ariaLabel prop is absent.

TabNavLink

  • Default renders as role="button" (link styled as tab button for nav contexts).
  • Explicit role="tab" still works when passed.
  • Keyboard: focusable, activatable with Enter.

Tag (selection variant)

  • Dismiss button keeps aria-label="Dismiss {children} Tag" when the tag is disabled (not an empty string).

PR Links and Envs

Repository PR Link
Monolith Monolith PR
Mono Mono PR

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 13, 2026

View your CI Pipeline Execution ↗ for commit dd03040


☁️ Nx Cloud last updated this comment at 2026-06-03 20:52:42 UTC

@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

⚠️ JUnit XML file not found

The CLI was unable to find any JUnit XML files to upload.
For more help, visit our troubleshooting guide.

@codecademydev
Copy link
Copy Markdown
Collaborator

📬 Published Alpha Packages:

Package Version npm Diff
@codecademy/gamut 70.0.1-alpha.a2dd9b.0 npm diff
@codecademy/gamut-icons 9.57.8-alpha.a2dd9b.0 npm diff
@codecademy/gamut-illustrations 0.58.14-alpha.a2dd9b.0 npm diff
@codecademy/gamut-kit 1.0.1-alpha.a2dd9b.0 npm diff
@codecademy/gamut-patterns 0.10.33-alpha.a2dd9b.0 npm diff
@codecademy/gamut-styles 20.0.1-alpha.a2dd9b.0 npm diff
@codecademy/gamut-tests 6.0.4-alpha.a2dd9b.0 npm diff
@codecademy/variance 0.26.2-alpha.a2dd9b.0 npm diff
eslint-plugin-gamut 2.4.4-alpha.a2dd9b.0 npm diff

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

@aresnik11 aresnik11 changed the title error a11y failures fix: resolve a11y errors Jun 4, 2026
@aresnik11 aresnik11 marked this pull request as ready for review June 4, 2026 21:04
@aresnik11 aresnik11 requested a review from a team as a code owner June 4, 2026 21:04
@aresnik11 aresnik11 requested review from LinKCoding and dreamwasp June 4, 2026 21:04
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