Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove aria attributes in order to read card body content #90

Merged
merged 4 commits into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
25 changes: 18 additions & 7 deletions src/components/AnswersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ export interface AnswersTableProps {
onKeyPress?: () => void;
contentRenderer?: JSX.Element;
instructions?: JSX.Element;
previewMode?: boolean;
}

export const AnswersTable = (props: AnswersTableProps) => {
let idCounter = 0;

const {
question, hideAnswers, type = defaultAnswerType, answered_count, choicesEnabled, correct_answer_id,
incorrectAnswerId, answer_id, feedback_html, correct_answer_feedback_html,
incorrectAnswerId, answer_id, feedback_html, correct_answer_feedback_html, previewMode,
show_all_feedback = false, tableFeedbackEnabled, hasCorrectAnswer, onChangeAnswer, onKeyPress, answerIdOrder, instructions
} = props;
if (hideAnswers) { return null; }
Expand All @@ -53,7 +54,7 @@ export const AnswersTable = (props: AnswersTableProps) => {
onChangeAnswer: onChangeAnswer,
type,
answered_count,
disabled: !choicesEnabled,
disabled: previewMode || !choicesEnabled,
show_all_feedback,
tableFeedbackEnabled,
onKeyPress
Expand All @@ -64,10 +65,10 @@ export const AnswersTable = (props: AnswersTableProps) => {
const answersHtml = answers.map((answer, i) => {
const additionalProps: { answer: AnswerType, iter: number, key: string }
= {
answer: {
...answer,
question_id: typeof question.id === 'string' ? parseInt(question.id, 10) : question.id
},
answer: {
...answer,
question_id: typeof question.id === 'string' ? parseInt(question.id, 10) : question.id
},
iter: i,
key: `${questionAnswerProps.qid}-option-${i}`,
};
Expand Down Expand Up @@ -103,7 +104,17 @@ export const AnswersTable = (props: AnswersTableProps) => {
});

return (
<div role="radiogroup" aria-label="Answer choices" className="answers-table">
Copy link
Author

Choose a reason for hiding this comment

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

@jivey using NVDA I noticed that Answer choices appears always, no matter the content. Removing this aria attributes NVDA is capable to read inside card's content. I didn't see any other a11y issue after removing role="radiogroup" but let me know if you're fine with this approach

Copy link
Member

Choose a reason for hiding this comment

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

The label and role here were recently added for accessibility so they probably need to stay here... can you share a screenshot of the NVDA speech that you are seeing? When I test with focus first off, I get this speech which is reading the options:

Answer choices  grouping       radio button  unavailable  not checked    clickable  A  Option 1     radio button  unavailable  not checked    clickable  B  Option 2     radio button  unavailable  not checked    clickable  C  Option 3     radio button  unavailable  not checked    clickable  D  Option 4

Additionally, I'm seeing we have a new issue here I didn't see before: those are being read as clickable, can we disable them while in preview mode?

Copy link
Author

Choose a reason for hiding this comment

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

Using VoiceOver with the label and role I get this:
Screenshot 2025-02-19 at 10 05 26 AM

Copy link
Member

Choose a reason for hiding this comment

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

Ah okay yes that seems like a problem, at least for our purposes. It's a newer change that I'm not familiar with: #78 My initial thought is that an aria label shouldn't be on that, as it has a role, but I'm not sure. Can you look into that? And if you determine the label needs to be there, we may have to make it conditional for preview cards...

Copy link
Author

Choose a reason for hiding this comment

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

For some reason role is removing Answers from screen reader. I removed the label just to test but Answers still missing as you can see in my previous screenshot. @TylerZeroMaster maybe you can help us to understand this a11y changes. Are we expecting that answers from a card only appears as "Answer choices" in screen readers?

Copy link
Contributor

Choose a reason for hiding this comment

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

@jomcarvajal I think the aria-label and role can be removed from the AnswersTable if all the radio buttons have the same name. When they have the same name, they should implicitly be in a radiogroup.

When using HTML's native input radio button, , the radio buttons are grouped when each of input radio buttons in the group is given the same name.

I may have followed the original issue, which suggested adding the radiogroup role, too closely when I implemented those changes. After further reading:

It is recommended to create radio groups by using same-named HTML input radio buttons, but, if you must use ARIA roles and attributes instead of semantic HTML form controls, custom radio buttons can and should act like native HTML radio input buttons.

So if it works better to create an implicit radiogroup by using same-named radio buttons, then I am all for that solution.

link for both quotes

<div
{
...(!previewMode
? {
role: "radiogroup",
'aria-label': "Answer choices"
}
: {})
}
className="answers-table"
>
{instructions}
{answersHtml}
</div>
Expand Down
7 changes: 7 additions & 0 deletions src/components/Exercise.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -376,5 +376,12 @@ describe('Exercise', () => {
).toJSON();
expect(tree).toMatchSnapshot();
});

it('matches snapshot with previewMode', () => {
const tree = renderer.create(
<Exercise {...props} show_all_feedback previewMode />
).toJSON();
expect(tree).toMatchSnapshot();
});
});
});
24 changes: 22 additions & 2 deletions src/components/Exercise.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,26 @@ export const Default = () => {
);
};

export const AnswerInteractionDisable = () => {
const [selectedAnswerId, setSelectedAnswerId] = useState<number>(0);
const [apiIsPending, setApiIsPending] = useState(false);
const props = exerciseWithQuestionStatesProps();
props.questionStates['1'].answer_id = selectedAnswerId;
props.questionStates['1'].apiIsPending = apiIsPending;
return (
<Exercise
{...props}
onAnswerChange={(
a: Omit<Answer, 'id'> & { id: number; question_id: number },
) => {
setSelectedAnswerId(a.id);
}}
onAnswerSave={() => setApiIsPending(true)}
previewMode
/>
);
};

export const DefaultWithoutFeedback = () => {
const [selectedAnswerId, setSelectedAnswerId] = useState<number>(0);
const [apiIsPending, setApiIsPending] = useState(false)
Expand Down Expand Up @@ -959,8 +979,8 @@ export const OverlayCard = () => {
return (
<TextResizerProvider>
<h2>Exercise cards</h2>
<Exercise {...props1} className='preview-card' />
<Exercise {...props2} className='preview-card' />
<Exercise {...props1} className='preview-card' previewMode />
<Exercise {...props2} className='preview-card' previewMode />
<h2>Exercise Preview cards</h2>
{showDetails1 && <h2>Details 1!</h2>}
<ExercisePreview
Expand Down
4 changes: 3 additions & 1 deletion src/components/Exercise/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,9 @@ export const Exercise = styled(({
scrollToQuestion,
exerciseIcons,
overlayChildren,
previewMode = false,
...props
}: { className?: string } & (ExerciseWithStepDataProps | ExerciseWithQuestionStatesProps) & OverlayProps) => {
}: { className?: string, previewMode?: boolean } & (ExerciseWithStepDataProps | ExerciseWithQuestionStatesProps) & OverlayProps) => {
const legacyStepRender = 'feedback_html' in step;
const questionsRef = React.useRef<Array<HTMLDivElement>>([]);
const container = React.useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -236,6 +237,7 @@ export const Exercise = styled(({
'canUpdateCurrentStep' in props ?
props.canUpdateCurrentStep : !(i + 1 === exercise.questions.length)
}
previewMode={previewMode}
/>
)
}
Expand Down
1 change: 1 addition & 0 deletions src/components/ExercisePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export const ExercisePreview = (
<Exercise
exercise={exercise}
className={selected ? 'preview-card is-selected' : 'preview-card'}
previewMode
{
...(enableOverlay
? {
Expand Down
4 changes: 3 additions & 1 deletion src/components/ExerciseQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface ExerciseQuestionProps {
show_all_feedback?: boolean;
tableFeedbackEnabled?: boolean;
hasFeedback?: ExerciseBaseProps['hasFeedback'];
previewMode?: boolean;
}

const AttemptsRemaining = ({ count }: { count: number }) => {
Expand Down Expand Up @@ -97,7 +98,7 @@ export const ExerciseQuestion = React.forwardRef((props: ExerciseQuestionProps,
answer_id, hasMultipleAttempts, attempts_remaining, published_comments, detailedSolution,
canAnswer, needsSaved, attempt_number, apiIsPending, onAnswerSave, onNextStep, canUpdateCurrentStep,
displaySolution, available_points, free_response, show_all_feedback, tableFeedbackEnabled,
hasFeedback
hasFeedback, previewMode
} = props;

const [shouldContinue, setShouldContinue] = React.useState(false)
Expand Down Expand Up @@ -129,6 +130,7 @@ export const ExerciseQuestion = React.forwardRef((props: ExerciseQuestionProps,
displaySolution={displaySolution}
show_all_feedback={show_all_feedback}
tableFeedbackEnabled={tableFeedbackEnabled}
previewMode={previewMode}
>
<FreeResponseReview free_response={free_response} />
</Question>
Expand Down
4 changes: 3 additions & 1 deletion src/components/Question.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ export interface QuestionProps {
children?: ReactNode;
answerIdOrder?: ID[];
choicesEnabled?: boolean;
previewMode?: boolean;
}

export const Question = React.forwardRef((props: QuestionProps, ref: React.ForwardedRef<HTMLDivElement>) => {
Expand Down Expand Up @@ -296,7 +297,8 @@ export const Question = React.forwardRef((props: QuestionProps, ref: React.Forwa
<AnswersTable
{...props}
onChangeAnswer={props.onChange}
hasCorrectAnswer={hasCorrectAnswer} />
hasCorrectAnswer={hasCorrectAnswer}
/>

{solution}
{props.displayFormats ? <FormatsListing formats={formats} /> : undefined}
Expand Down
200 changes: 200 additions & 0 deletions src/components/__snapshots__/Exercise.spec.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,206 @@ exports[`Exercise with overlay rendering matches snapshot 1`] = `
</div>
`;

exports[`Exercise with overlay rendering matches snapshot with previewMode 1`] = `
<div
className="sc-ksZaOG gLzA-Dq"
>
<div
className="sc-gsnTZi dxdISE"
data-task-step-id={1}
>
<div
className="sc-bczRLJ eAXREJ exercise-step sc-breuTD dSaRVj sc-hAZoDl iLMhfL"
>
<div
onBlur={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
tabIndex={0}
>
<div
id="step-card"
>
<div
className="sc-dkzDqf jTQbcy step-card-header"
>
<div>
<h2
className="question-info"
>
<span>
Question 1
</span>
<span
className="num-questions"
>
 /
1
</span>
<span
className="separator"
>
|
</span>
<span
className="question-id"
>
ID:
1234@4
</span>
</h2>
</div>
</div>
<div
className="sc-hKMtZM bwRTaS"
>
<div>
<div
className="step-card-body exercise-context"
dangerouslySetInnerHTML={
Object {
"__html": "Context",
}
}
/>
<div
className="step-card-body exercise-stimulus"
dangerouslySetInnerHTML={
Object {
"__html": "<b>Stimulus HTML</b>",
}
}
/>
<div
data-test-id="student-exercise-question"
>
<div
className="sc-iBkjds lXypw openstax-question step-card-body"
data-question-number={1}
data-test-id="question"
>
<div
className="answers-table"
>
<div
className="openstax-answer"
>
<section
className="answers-answer disabled answer-selected"
>
<input
checked={true}
className="answer-input-box"
disabled={true}
id="1234@5-option-0"
name="1234@5-options"
onChange={[Function]}
type="radio"
/>
<label
className="answer-label"
htmlFor="1234@5-option-0"
>
<span
aria-label="Selected Choice A:"
className="answer-letter-wrapper"
data-answer-choice="A"
data-test-id="answer-choice-A"
/>
<div
className="answer-answer"
>
<span
className="answer-content"
dangerouslySetInnerHTML={
Object {
"__html": "True",
}
}
/>
</div>
</label>
</section>
</div>
<div
className="openstax-answer"
>
<section
className="answers-answer disabled"
>
<input
checked={false}
className="answer-input-box"
disabled={true}
id="1234@5-option-1"
name="1234@5-options"
onChange={[Function]}
type="radio"
/>
<label
className="answer-label"
htmlFor="1234@5-option-1"
>
<span
aria-label="Choice B:"
className="answer-letter-wrapper"
data-answer-choice="B"
data-test-id="answer-choice-B"
/>
<div
className="answer-answer"
>
<span
className="answer-content"
dangerouslySetInnerHTML={
Object {
"__html": "False",
}
}
/>
</div>
</label>
</section>
</div>
</div>
</div>
<div
className="sc-ftvSup icqnDT step-card-footer"
>
<div
className="step-card-footer-inner"
>
<div
className="points"
role="status"
>
<span
className="attempts-left"
/>
</div>
<div
className="controls"
>
<button
className="sc-jSMfEi hsxEPT"
data-test-id="continue-btn"
onClick={[Function]}
>
Next
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

exports[`Exercise with question state data matches snapshot 1`] = `
<div
className="sc-ksZaOG gLzA-Dq"
Expand Down
Loading