From a1c216fa86dfb6b8d524ab31084b4db40663604d Mon Sep 17 00:00:00 2001 From: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com> Date: Fri, 23 Jan 2026 02:44:56 +0700 Subject: [PATCH 1/4] feat(Quiz,QuizQuestion): add action buttons for answer options --- src/quiz-question/answer.tsx | 41 ++- src/quiz-question/quiz-question.stories.tsx | 88 ++++++ src/quiz-question/quiz-question.test.tsx | 85 +++++- src/quiz-question/quiz-question.tsx | 3 +- src/quiz-question/types.ts | 15 ++ src/quiz/quiz.stories.tsx | 285 ++++++++++++++++++++ src/quiz/quiz.test.tsx | 91 +++++++ 7 files changed, 601 insertions(+), 7 deletions(-) diff --git a/src/quiz-question/answer.tsx b/src/quiz-question/answer.tsx index 4b37a688..fd6d4f54 100644 --- a/src/quiz-question/answer.tsx +++ b/src/quiz-question/answer.tsx @@ -1,8 +1,13 @@ import React from "react"; import { RadioGroup } from "@headlessui/react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { + faCheck, + faXmark, + faMicrophone, +} from "@fortawesome/free-solid-svg-icons"; +import { Button } from "../button"; import { QuizQuestionValidation, type QuizQuestionAnswer } from "./types"; interface AnswerProps< @@ -52,11 +57,15 @@ const radioOptionDefaultClasses = [ "p-[20px]", "flex", "items-center", + "col-start-1", + "row-start-1", ]; const radioWrapperDefaultClasses = [ - "flex", - "flex-col", + "grid", + "grid-cols-[1fr_auto]", + "grid-rows-[auto_auto]", + "gap-x-4", "border-x-4", "border-t-4", "last:border-b-4", @@ -110,7 +119,10 @@ export const Answer = ({ checked, validation, feedback, + action, }: AnswerProps) => { + const labelId = `quiz-answer-${value}-label`; + const getRadioWrapperCls = () => { const cls = [...radioWrapperDefaultClasses]; @@ -141,16 +153,35 @@ export const Answer = ({ {({ active }) => ( <> - + {label} )} + + {action && ( +
+ +
+ )} + {(!!validation || !!feedback) && ( // Remove the default bottom margin of the validation message `p`, // and apply a bottom padding of 20px to match the top padding of RadioGroup.Option -
+ // Span both columns for feedback +
{validation && ( alert("Playing audio for: Hello, how are you?"), + ariaLabel: "Practice speaking this answer", + }, + }, + { + label: "Hi there!", + value: 2, + action: { + onClick: () => alert("Playing audio for: Hi there!"), + ariaLabel: "Practice speaking this answer", + }, + }, + { + label: "Good morning", + value: 3, + action: { + onClick: () => alert("Playing audio for: Good morning"), + ariaLabel: "Practice speaking this answer", + }, + }, + { + label: "Hey", + value: 4, + // No action for this answer + }, + ], + position: 1, + }, + parameters: { + docs: { + source: { + code: `const App = () => { + const [answer, setAnswer] = useState(); + + return ( + console.log("Open speaking modal"), + ariaLabel: "Practice speaking this answer" + } + }, + { + label: "Hi there!", + value: 2, + action: { + onClick: () => console.log("Open speaking modal"), + ariaLabel: "Practice speaking this answer" + } + }, + { + label: "Good morning", + value: 3, + action: { + onClick: () => console.log("Open speaking modal"), + ariaLabel: "Practice speaking this answer" + } + }, + { + label: "Hey", + value: 4 + // No action for this answer + } + ]} + onChange={(newAnswer) => setAnswer(newAnswer)} + selectedAnswer={answer} + position={1} + /> + ); +}`, + }, + }, + }, +}; + export default story; diff --git a/src/quiz-question/quiz-question.test.tsx b/src/quiz-question/quiz-question.test.tsx index 450f1a33..8fcea32e 100644 --- a/src/quiz-question/quiz-question.test.tsx +++ b/src/quiz-question/quiz-question.test.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { render, screen, within } from "@testing-library/react"; +import { render, screen, within, fireEvent } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { QuizQuestion } from "./quiz-question"; @@ -300,6 +300,89 @@ describe("", () => { within(radioGroup).queryByText("Culpa dolores aut."), ).not.toBeInTheDocument(); }); + + it("should render action buttons when provided", () => { + const handleAction1 = jest.fn(); + const handleAction2 = jest.fn(); + + render( + , + ); + + const actionButton1 = screen.getByRole("button", { + name: "Practice speaking option 1", + }); + const actionButton2 = screen.getByRole("button", { + name: "Practice speaking option 2", + }); + + expect(actionButton1).toBeInTheDocument(); + expect(actionButton2).toBeInTheDocument(); + + expect(actionButton1).toHaveAttribute( + "aria-describedby", + "quiz-answer-1-label", + ); + expect(actionButton2).toHaveAttribute( + "aria-describedby", + "quiz-answer-2-label", + ); + + fireEvent.click(actionButton1); + expect(handleAction1).toHaveBeenCalledTimes(1); + + fireEvent.click(actionButton2); + expect(handleAction2).toHaveBeenCalledTimes(1); + + // Verify no action button for option 3 + expect( + screen.queryByRole("button", { + name: "Practice speaking option 3", + }), + ).not.toBeInTheDocument(); + }); + + it("should not render action buttons when not provided", () => { + render( + , + ); + + // Verify no buttons are rendered + const allButtons = screen.queryAllByRole("button", { hidden: true }); + expect(allButtons).toHaveLength(0); + }); }); // ------------------------------ diff --git a/src/quiz-question/quiz-question.tsx b/src/quiz-question/quiz-question.tsx index e893e39a..5d321904 100644 --- a/src/quiz-question/quiz-question.tsx +++ b/src/quiz-question/quiz-question.tsx @@ -65,7 +65,7 @@ export const QuizQuestion = ({ - {answers.map(({ value, label, feedback, validation }) => { + {answers.map(({ value, label, feedback, validation, action }) => { const checked = selectedAnswer === value; return ( ({ checked={checked} disabled={disabled} validation={validation} + action={action} /> ); })} diff --git a/src/quiz-question/types.ts b/src/quiz-question/types.ts index 2a022c38..8cfb30a2 100644 --- a/src/quiz-question/types.ts +++ b/src/quiz-question/types.ts @@ -9,6 +9,21 @@ export interface QuizQuestionAnswer { * Information needed to render the validation status */ validation?: QuizQuestionValidation; + + /** + * Optional action button configuration. + * When provided, renders an action button next to this answer. + */ + action?: { + /** + * Click handler for the action button + */ + onClick: () => void; + /** + * Accessible label for the action button + */ + ariaLabel: string; + }; } export interface QuizQuestionValidation { diff --git a/src/quiz/quiz.stories.tsx b/src/quiz/quiz.stories.tsx index 706553b9..26eb1e85 100644 --- a/src/quiz/quiz.stories.tsx +++ b/src/quiz/quiz.stories.tsx @@ -695,4 +695,289 @@ const App = () => { }, }; +const QuizWithActionButtons = () => { + const [actionLog, setActionLog] = useState([]); + + const initialQuestions: Question[] = [ + { + question: "Which question is incorrect?", + answers: [ + { + label: "Are you into photography?", + value: 1, + action: { + onClick: () => { + setActionLog((prev) => [ + ...prev, + "Practice: Are you into photography?", + ]); + console.log("Speaking practice: Are you into photography?"); + }, + ariaLabel: "Practice speaking Are you into photography?", + }, + }, + { + label: "Are they into photography?", + value: 2, + action: { + onClick: () => { + setActionLog((prev) => [ + ...prev, + "Practice: Are they into photography?", + ]); + console.log("Speaking practice: Are they into photography?"); + }, + ariaLabel: "Practice speaking Are they into photography?", + }, + }, + { + label: "Is he into photography?", + value: 3, + action: { + onClick: () => { + setActionLog((prev) => [ + ...prev, + "Practice: Is he into photography?", + ]); + console.log("Speaking practice: Is he into photography?"); + }, + ariaLabel: "Practice speaking Is he into photography?", + }, + }, + { label: "Am we into photography?", value: 4 }, + ], + correctAnswer: 4, + }, + { + question: "Which adjective shows a negative feeling?", + answers: [ + { + label: "Friendly", + value: 1, + action: { + onClick: () => { + setActionLog((prev) => [...prev, "Practice: Friendly"]); + console.log("Speaking practice: Friendly"); + }, + ariaLabel: "Practice speaking Friendly", + }, + }, + { + label: "Cool", + value: 2, + action: { + onClick: () => { + setActionLog((prev) => [...prev, "Practice: Cool"]); + console.log("Speaking practice: Cool"); + }, + ariaLabel: "Practice speaking Cool", + }, + }, + { + label: "Supportive", + value: 3, + action: { + onClick: () => { + setActionLog((prev) => [...prev, "Practice: Supportive"]); + console.log("Speaking practice: Supportive"); + }, + ariaLabel: "Practice speaking Supportive", + }, + }, + { label: "Boring", value: 4 }, + ], + correctAnswer: 4, + }, + { + question: + "What does this sentence mean: `I've played these games before`?", + answers: [ + { label: "You are playing them now.", value: 1 }, + { label: "You will play them later.", value: 2 }, + { label: "You never played them.", value: 3 }, + { label: "You played them in the past.", value: 4 }, + ], + correctAnswer: 4, + }, + ]; + + const { questions } = useQuiz({ + initialQuestions, + validationMessages: { + correct: "Correct.", + incorrect: "Incorrect.", + }, + passingPercent: 100, + }); + + return ( +
+ {actionLog.length > 0 && ( +
+

Action Log:

+
    + {actionLog.map((log, index) => ( +
  • {log}
  • + ))} +
+
+ )} + +
+ ); +}; + +export const WithActionButtons: Story = { + render: QuizWithActionButtons, + args: {}, + parameters: { + docs: { + description: { + story: + "Quiz with action buttons on selected answers. Useful for language learning features like 'Practice speaking'. Some answers have action buttons while others do not, demonstrating mixed configurations across questions.", + }, + source: { + code: ` +import { Quiz, useQuiz } from '@freecodecamp/ui'; + +const initialQuestions = [ + { + question: "Which question is incorrect?", + answers: [ + { + label: "Are you into photography?", + value: 1, + action: { + onClick: () => { + console.log("Speaking practice: Are you into photography?"); + }, + ariaLabel: "Practice speaking Are you into photography?", + }, + }, + { + label: "Are they into photography?", + value: 2, + action: { + onClick: () => { + console.log("Speaking practice: Are they into photography?"); + }, + ariaLabel: "Practice speaking Are they into photography?", + }, + }, + { + label: "Is he into photography?", + value: 3, + action: { + onClick: () => { + console.log("Speaking practice: Is he into photography?"); + }, + ariaLabel: "Practice speaking Is he into photography?", + }, + }, + { label: "Am we into photography?", value: 4 }, + ], + correctAnswer: 4, + }, + { + question: "Which adjective shows a negative feeling?", + answers: [ + { + label: "Friendly", + value: 1, + action: { + onClick: () => { + console.log("Speaking practice: Friendly"); + }, + ariaLabel: "Practice speaking Friendly", + }, + }, + { + label: "Cool", + value: 2, + action: { + onClick: () => { + console.log("Speaking practice: Cool"); + }, + ariaLabel: "Practice speaking Cool", + }, + }, + { + label: "Supportive", + value: 3, + action: { + onClick: () => { + console.log("Speaking practice: Supportive"); + }, + ariaLabel: "Practice speaking Supportive", + }, + }, + { label: "Boring", value: 4 }, + ], + correctAnswer: 4, + }, + { + question: "What does this sentence mean: \`I've played these games before\`?", + answers: [ + { label: "You are playing them now.", value: 1 }, + { label: "You will play them later.", value: 2 }, + { label: "You never played them.", value: 3 }, + { label: "You played them in the past.", value: 4 }, + ], + correctAnswer: 4, + }, + { + question: "What does 'ubiquitous' mean?", + answers: [ + { + label: "Rare or uncommon", + value: 1, + action: { + onClick: () => { + console.log("Speaking practice: Rare or uncommon"); + }, + ariaLabel: "Practice speaking Rare or uncommon", + }, + }, + { + label: "Present everywhere", + value: 2, + action: { + onClick: () => { + console.log("Speaking practice: Present everywhere"); + }, + ariaLabel: "Practice speaking Present everywhere", + }, + }, + { label: "Very expensive", value: 3 }, + ], + correctAnswer: 2, + }, + { + question: "What is the past tense of 'run' in English?", + answers: [ + { label: "Runned", value: 1 }, + { label: "Ran", value: 2 }, + { label: "Running", value: 3 }, + ], + correctAnswer: 2, + }, +]; + +const App = () => { + const { questions } = useQuiz({ + initialQuestions, + validationMessages: { + correct: "Correct.", + incorrect: "Incorrect.", + }, + passingPercent: 100, + }); + + return ; +};`, + }, + }, + }, +}; + export default story; diff --git a/src/quiz/quiz.test.tsx b/src/quiz/quiz.test.tsx index 96839035..896f382e 100644 --- a/src/quiz/quiz.test.tsx +++ b/src/quiz/quiz.test.tsx @@ -123,6 +123,97 @@ describe("", () => { expect(question).toBeRequired(); }); }); + + it("should render action buttons when answers have action configuration", async () => { + const actionHandlers = { + q1a1: jest.fn(), + q1a2: jest.fn(), + q2a1: jest.fn(), + }; + + const questions = [ + { + question: "Question 1", + answers: [ + { + label: "Answer 1", + value: 1, + action: { + onClick: actionHandlers.q1a1, + ariaLabel: "Practice Answer 1", + }, + }, + { + label: "Answer 2", + value: 2, + action: { + onClick: actionHandlers.q1a2, + ariaLabel: "Practice Answer 2", + }, + }, + { label: "Answer 3", value: 3 }, + ], + correctAnswer: 1, + }, + { + question: "Question 2", + answers: [ + { + label: "Answer A", + value: 1, + action: { + onClick: actionHandlers.q2a1, + ariaLabel: "Practice Answer A", + }, + }, + { label: "Answer B", value: 2 }, + ], + correctAnswer: 1, + }, + ]; + + render(); + + const question1Button1 = screen.getByRole("button", { + name: "Practice Answer 1", + }); + const question1Button2 = screen.getByRole("button", { + name: "Practice Answer 2", + }); + const question2Button1 = screen.getByRole("button", { + name: "Practice Answer A", + }); + + expect(question1Button1).toBeInTheDocument(); + expect(question1Button2).toBeInTheDocument(); + expect(question2Button1).toBeInTheDocument(); + + await userEvent.click(question1Button1); + await userEvent.click(question1Button2); + await userEvent.click(question2Button1); + + expect(actionHandlers.q1a1).toHaveBeenCalledTimes(1); + expect(actionHandlers.q1a2).toHaveBeenCalledTimes(1); + expect(actionHandlers.q2a1).toHaveBeenCalledTimes(1); + }); + + it("should not render action buttons when answers do not have action configuration", () => { + const questions = [ + { + question: "Question 1", + answers: [ + { label: "Answer 1", value: 1 }, + { label: "Answer 2", value: 2 }, + ], + correctAnswer: 1, + }, + ]; + + render(); + + const buttons = screen.queryAllByRole("button"); + expect(buttons).toHaveLength(0); + }); }); // ------------------------------ From 9a6e907da34b6487338a5165afd3f4adaf7d6cee Mon Sep 17 00:00:00 2001 From: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com> Date: Fri, 23 Jan 2026 03:02:28 +0700 Subject: [PATCH 2/4] fix(Answer): grid layout --- src/quiz-question/answer.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/quiz-question/answer.tsx b/src/quiz-question/answer.tsx index fd6d4f54..d6065cb0 100644 --- a/src/quiz-question/answer.tsx +++ b/src/quiz-question/answer.tsx @@ -63,9 +63,8 @@ const radioOptionDefaultClasses = [ const radioWrapperDefaultClasses = [ "grid", - "grid-cols-[1fr_auto]", + "grid-cols-1", "grid-rows-[auto_auto]", - "gap-x-4", "border-x-4", "border-t-4", "last:border-b-4", @@ -126,6 +125,11 @@ export const Answer = ({ const getRadioWrapperCls = () => { const cls = [...radioWrapperDefaultClasses]; + // Add second column for action button when action is provided + if (action) { + cls.push("grid-cols-[1fr_auto]", "gap-x-4"); + } + if (validation?.state === "correct") cls.push("border-l-background-success"); if (validation?.state === "incorrect") @@ -155,7 +159,7 @@ export const Answer = ({ {label} From f6b75d7d062d083572b0cba87b599b4ee7260fba Mon Sep 17 00:00:00 2001 From: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com> Date: Fri, 23 Jan 2026 03:11:47 +0700 Subject: [PATCH 3/4] refactor: remove redundant code --- src/quiz-question/answer.tsx | 2 - src/quiz-question/quiz-question.stories.tsx | 12 +- src/quiz/quiz.stories.tsx | 117 +++++--------------- 3 files changed, 31 insertions(+), 100 deletions(-) diff --git a/src/quiz-question/answer.tsx b/src/quiz-question/answer.tsx index d6065cb0..d297a4f7 100644 --- a/src/quiz-question/answer.tsx +++ b/src/quiz-question/answer.tsx @@ -170,11 +170,9 @@ export const Answer = ({ {action && (
diff --git a/src/quiz-question/quiz-question.stories.tsx b/src/quiz-question/quiz-question.stories.tsx index 9bb3563e..eb46f5e8 100644 --- a/src/quiz-question/quiz-question.stories.tsx +++ b/src/quiz-question/quiz-question.stories.tsx @@ -611,7 +611,7 @@ export const WithActionButtons: Story = { value: 1, action: { onClick: () => alert("Playing audio for: Hello, how are you?"), - ariaLabel: "Practice speaking this answer", + ariaLabel: "Practice speaking", }, }, { @@ -619,7 +619,7 @@ export const WithActionButtons: Story = { value: 2, action: { onClick: () => alert("Playing audio for: Hi there!"), - ariaLabel: "Practice speaking this answer", + ariaLabel: "Practice speaking", }, }, { @@ -627,7 +627,7 @@ export const WithActionButtons: Story = { value: 3, action: { onClick: () => alert("Playing audio for: Good morning"), - ariaLabel: "Practice speaking this answer", + ariaLabel: "Practice speaking", }, }, { @@ -653,7 +653,7 @@ export const WithActionButtons: Story = { value: 1, action: { onClick: () => console.log("Open speaking modal"), - ariaLabel: "Practice speaking this answer" + ariaLabel: "Practice speaking" } }, { @@ -661,7 +661,7 @@ export const WithActionButtons: Story = { value: 2, action: { onClick: () => console.log("Open speaking modal"), - ariaLabel: "Practice speaking this answer" + ariaLabel: "Practice speaking" } }, { @@ -669,7 +669,7 @@ export const WithActionButtons: Story = { value: 3, action: { onClick: () => console.log("Open speaking modal"), - ariaLabel: "Practice speaking this answer" + ariaLabel: "Practice speaking" } }, { diff --git a/src/quiz/quiz.stories.tsx b/src/quiz/quiz.stories.tsx index 26eb1e85..fce24b74 100644 --- a/src/quiz/quiz.stories.tsx +++ b/src/quiz/quiz.stories.tsx @@ -696,8 +696,6 @@ const App = () => { }; const QuizWithActionButtons = () => { - const [actionLog, setActionLog] = useState([]); - const initialQuestions: Question[] = [ { question: "Which question is incorrect?", @@ -707,13 +705,9 @@ const QuizWithActionButtons = () => { value: 1, action: { onClick: () => { - setActionLog((prev) => [ - ...prev, - "Practice: Are you into photography?", - ]); - console.log("Speaking practice: Are you into photography?"); + alert("Speaking practice: Are you into photography?"); }, - ariaLabel: "Practice speaking Are you into photography?", + ariaLabel: "Practice speaking", }, }, { @@ -721,13 +715,9 @@ const QuizWithActionButtons = () => { value: 2, action: { onClick: () => { - setActionLog((prev) => [ - ...prev, - "Practice: Are they into photography?", - ]); - console.log("Speaking practice: Are they into photography?"); + alert("Speaking practice: Are they into photography?"); }, - ariaLabel: "Practice speaking Are they into photography?", + ariaLabel: "Practice speaking", }, }, { @@ -735,13 +725,9 @@ const QuizWithActionButtons = () => { value: 3, action: { onClick: () => { - setActionLog((prev) => [ - ...prev, - "Practice: Is he into photography?", - ]); - console.log("Speaking practice: Is he into photography?"); + alert("Speaking practice: Is he into photography?"); }, - ariaLabel: "Practice speaking Is he into photography?", + ariaLabel: "Practice speaking", }, }, { label: "Am we into photography?", value: 4 }, @@ -756,10 +742,9 @@ const QuizWithActionButtons = () => { value: 1, action: { onClick: () => { - setActionLog((prev) => [...prev, "Practice: Friendly"]); - console.log("Speaking practice: Friendly"); + alert("Speaking practice: Friendly"); }, - ariaLabel: "Practice speaking Friendly", + ariaLabel: "Practice speaking", }, }, { @@ -767,10 +752,9 @@ const QuizWithActionButtons = () => { value: 2, action: { onClick: () => { - setActionLog((prev) => [...prev, "Practice: Cool"]); - console.log("Speaking practice: Cool"); + alert("Speaking practice: Cool"); }, - ariaLabel: "Practice speaking Cool", + ariaLabel: "Practice speaking", }, }, { @@ -778,10 +762,9 @@ const QuizWithActionButtons = () => { value: 3, action: { onClick: () => { - setActionLog((prev) => [...prev, "Practice: Supportive"]); - console.log("Speaking practice: Supportive"); + alert("Speaking practice: Supportive"); }, - ariaLabel: "Practice speaking Supportive", + ariaLabel: "Practice speaking", }, }, { label: "Boring", value: 4 }, @@ -810,21 +793,7 @@ const QuizWithActionButtons = () => { passingPercent: 100, }); - return ( -
- {actionLog.length > 0 && ( -
-

Action Log:

-
    - {actionLog.map((log, index) => ( -
  • {log}
  • - ))} -
-
- )} - -
- ); + return ; }; export const WithActionButtons: Story = { @@ -849,9 +818,9 @@ const initialQuestions = [ value: 1, action: { onClick: () => { - console.log("Speaking practice: Are you into photography?"); + alert("Speaking practice: Are you into photography?"); }, - ariaLabel: "Practice speaking Are you into photography?", + ariaLabel: "Practice speaking", }, }, { @@ -859,9 +828,9 @@ const initialQuestions = [ value: 2, action: { onClick: () => { - console.log("Speaking practice: Are they into photography?"); + alert("Speaking practice: Are they into photography?"); }, - ariaLabel: "Practice speaking Are they into photography?", + ariaLabel: "Practice speaking", }, }, { @@ -869,9 +838,9 @@ const initialQuestions = [ value: 3, action: { onClick: () => { - console.log("Speaking practice: Is he into photography?"); + alert("Speaking practice: Is he into photography?"); }, - ariaLabel: "Practice speaking Is he into photography?", + ariaLabel: "Practice speaking", }, }, { label: "Am we into photography?", value: 4 }, @@ -886,9 +855,9 @@ const initialQuestions = [ value: 1, action: { onClick: () => { - console.log("Speaking practice: Friendly"); + alert("Speaking practice: Friendly"); }, - ariaLabel: "Practice speaking Friendly", + ariaLabel: "Practice speaking", }, }, { @@ -896,9 +865,9 @@ const initialQuestions = [ value: 2, action: { onClick: () => { - console.log("Speaking practice: Cool"); + alert("Speaking practice: Cool"); }, - ariaLabel: "Practice speaking Cool", + ariaLabel: "Practice speaking", }, }, { @@ -906,9 +875,9 @@ const initialQuestions = [ value: 3, action: { onClick: () => { - console.log("Speaking practice: Supportive"); + alert("Speaking practice: Supportive"); }, - ariaLabel: "Practice speaking Supportive", + ariaLabel: "Practice speaking", }, }, { label: "Boring", value: 4 }, @@ -925,42 +894,6 @@ const initialQuestions = [ ], correctAnswer: 4, }, - { - question: "What does 'ubiquitous' mean?", - answers: [ - { - label: "Rare or uncommon", - value: 1, - action: { - onClick: () => { - console.log("Speaking practice: Rare or uncommon"); - }, - ariaLabel: "Practice speaking Rare or uncommon", - }, - }, - { - label: "Present everywhere", - value: 2, - action: { - onClick: () => { - console.log("Speaking practice: Present everywhere"); - }, - ariaLabel: "Practice speaking Present everywhere", - }, - }, - { label: "Very expensive", value: 3 }, - ], - correctAnswer: 2, - }, - { - question: "What is the past tense of 'run' in English?", - answers: [ - { label: "Runned", value: 1 }, - { label: "Ran", value: 2 }, - { label: "Running", value: 3 }, - ], - correctAnswer: 2, - }, ]; const App = () => { From a63a45fa295f7a1ae8fc539f5e473dbd86f81ca4 Mon Sep 17 00:00:00 2001 From: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com> Date: Fri, 23 Jan 2026 04:17:56 +0700 Subject: [PATCH 4/4] fix: explicit button role --- src/quiz-question/answer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/quiz-question/answer.tsx b/src/quiz-question/answer.tsx index d297a4f7..005a8f53 100644 --- a/src/quiz-question/answer.tsx +++ b/src/quiz-question/answer.tsx @@ -173,6 +173,7 @@ export const Answer = ({ onClick={action.onClick} aria-label={action.ariaLabel} aria-describedby={labelId} + role="button" >