Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/five-seas-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": patch
---

fix - add roles to ul and li items to make sure they are visible to screen readers
12 changes: 10 additions & 2 deletions packages/perseus/src/widgets/radio/choice.new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ export interface IndicatorProps {
showCorrectness?: "correct" | "wrong";
updateChecked: (isChecked: boolean) => void;
}
/*
* NOTE: Redundant ARIA roles for <li> element
* Redundant roles were added to address the issue of different screen
* readers handling <fieldset> and <legend> elements inconsistently.
* `role="listitem"` attribute preserves list semantics, as some screen
* readers may remove the implicit list role when `list-style: none` is
* applied via CSS.
*/

const Choice = (props: IndicatorProps) => {
const showCorrectness = props.showCorrectness;
Expand All @@ -34,8 +42,8 @@ const Choice = (props: IndicatorProps) => {
Therefore, WCAG 2.1.1 is still satisfied since functionality is
aligned with the input method.
*/
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions
<li className={classes} onClick={clickHandler}>
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/no-redundant-roles
<li className={classes} onClick={clickHandler} role="listitem">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if some of these rules are helpful if we need to keep overriding them in our efforts to have a good a11y experience. 🤔

<Indicator
buttonRef={buttonRef}
checked={props.checked}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ export interface MultipleChoiceComponentProps {
* button semantics for both versions of the widget,
* and implement additional ARIA attributes where needed.
*
* NOTE: Redundant ARIA roles for <ul> element
* Different screen readers handle <fieldset> and <legend> elements inconsistently,
* causing unpredictable announcement behavior:
* - NVDA + Chrome: Announces the legend when entering the fieldset,
* providing context for the choices
* - VoiceOver + Safari: Often fails to announce the legend when users
* navigate directly to form controls, especially with certain
* navigation methods (arrow keys) or on mobile
* This inconsistency means some users hear the critical instructions
* ("Choose 1 answer", "Choose 3 answers", etc.) while others miss them
* entirely, depending on their assistive technology and navigation method.
* Redundant roles were added to address this issue.`role="list"` attribute
* preserves list semantics, as some screen readers may remove the implicit
* list role when `list-style: none` is applied via CSS.
*
* Created as part of the Radio Revitalization Project (LEMS-2933).
*/
const MultipleChoiceComponent = ({
Expand Down Expand Up @@ -119,9 +134,11 @@ const MultipleChoiceComponent = ({
{instructions}
</legend>
<ScrollableView id={scrollId} overflowX="auto">
{/* eslint-disable-next-line jsx-a11y/no-redundant-roles */}
<ul
aria-labelledby={legendId}
className={choiceListClasses}
role="list"
>
<ChoiceListItems
choices={choices}
Expand Down
Loading