Skip to content

[ShadCN]: Replace Chakra radio hooks with RadioGroup Radix #14914

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

Merged
Changes from all 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
125 changes: 46 additions & 79 deletions src/components/Quiz/QuizWidget/QuizRadioGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useMemo } from "react"
import { useMemo, useState } from "react"
import { useTranslation } from "next-i18next"
import { useRadio, useRadioGroup, UseRadioProps } from "@chakra-ui/react"
import * as RadioGroup from "@radix-ui/react-radio-group"

import type {
AnswerChoice,
Expand Down Expand Up @@ -30,21 +30,16 @@ export const QuizRadioGroup = ({
}: QuizRadioGroupProps) => {
const { t } = useTranslation("learn-quizzes")

const [selectedAnswer, setSelectedAnswer] =
useState<RadioGroup.RadioGroupProps["value"]>("")

const handleSelection = (answerId: AnswerKey) => {
setSelectedAnswer(answerId)
const isCorrect =
answerId === questions[currentQuestionIndex].correctAnswerId
setCurrentQuestionAnswerChoice({ answerId, isCorrect })
}

const {
getRadioProps,
getRootProps,
value: selectedAnswer,
} = useRadioGroup({
name: `quiz-question-${currentQuestionIndex}`,
onChange: handleSelection,
})

const {
answers,
correctAnswerId,
Expand All @@ -65,125 +60,97 @@ export const QuizRadioGroup = ({
)

return (
<fieldset className="w-full" {...getRootProps()}>
<fieldset className="w-full">
<legend className="mb-6 w-full text-center text-2xl font-bold">
<span className="sr-only">
{t("question-number", { number: currentQuestionIndex + 1 })}
</span>
{t(prompt)}
</legend>

<div
<RadioGroup.Root
className="md:px-12 lg:px-16"
data-testid="question-group"
id={questionId}
value={selectedAnswer}
onValueChange={handleSelection}
>
<Stack className="gap-4">
{answers.map(({ id, label }, idx) => {
const display =
!answerStatus || id === selectedAnswer ? "inline-flex" : "hidden"

return (
<div key={id} className={display}>
<CustomRadio
label={t(label)}
isAnswerVisible={!!answerStatus}
isSelectedCorrect={isSelectedCorrect}
index={idx}
{...getRadioProps({ value: id })}
value={id}
/>
</div>
)
})}
</Stack>

{!!answerStatus && (
<Stack className="mt-6 gap-2">
<p className="font-bold">{t("explanation")}</p>

<p className="m-0">{t(explanation)}</p>
</Stack>
)}
</div>
</RadioGroup.Root>
</fieldset>
)
}

type CustomRadioProps = UseRadioProps & {
type CustomRadioProps = RadioGroup.RadioGroupItemProps & {
index: number
isAnswerVisible: boolean
isSelectedCorrect: boolean
label: string
}

const CustomRadio = ({
isAnswerVisible,
index,
isAnswerVisible,
isSelectedCorrect,
label,
...radioProps
...itemProps
}: CustomRadioProps) => {
const INPUT_ID = `quiz-question-answer-${index}`
const { state, getInputProps, getRadioProps, getLabelProps } = useRadio({
...radioProps,
id: INPUT_ID,
})

const buttonBg = useMemo<string>(() => {
if (!state.isChecked) return "bg-background-highlight"
if (!isAnswerVisible) return "bg-primary"
if (!isSelectedCorrect) return "bg-error"
return "bg-success"
}, [state.isChecked, isAnswerVisible, isSelectedCorrect])

const radioInputProps = getInputProps({ id: INPUT_ID })

return (
<>
<label
// `htmlFor` for proper accessibility with label and input
{...getLabelProps({ htmlFor: INPUT_ID })}
className="w-full"
>
<HStack
{...getRadioProps()}
id={radioInputProps.value}
data-testid="quiz-question-answer"
data-group
data-answer-visible={isAnswerVisible || undefined}
data-selected-correct={isSelectedCorrect || undefined}
// Override: `aria-hidden` is marked true in `getRadioProps`
aria-hidden="false"
<HStack
id={itemProps.value}
data-testid="quiz-question-answer"
data-group
data-answer-visible={isAnswerVisible || undefined}
data-selected-correct={isSelectedCorrect || undefined}
className={cn(
"w-full cursor-pointer gap-2 rounded bg-background-highlight p-2 text-start text-body data-[answer-visible]:cursor-default",
"hover:outline hover:outline-1 hover:outline-primary hover:data-[answer-visible]:outline-none",
"data-[state='checked']:data-[answer-visible]:bg-error",
"data-[state='checked']:data-[answer-visible]:data-[selected-correct]:bg-success",
"data-[state='checked']:text-white",
"data-[state='checked']:not-[data-answer-visible]:bg-primary"
Comment on lines +130 to +133
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note to self: clean up these style checks!

)}
asChild
>
<RadioGroup.Item {...itemProps}>
<Center
className={cn(
"w-full cursor-pointer gap-2 rounded p-2 text-body data-[answer-visible]:cursor-default",
buttonBg,
"hover:outline hover:outline-1 hover:outline-primary hover:data-[answer-visible]:outline-none",
// TODO: Upon removing custom radio props, flip remove `data` for checked
"data-[checked]:data-[answer-visible]:bg-error",
"data-[checked]:data-[answer-visible]:data-[selected-correct]:bg-success",
"data-[checked]:text-white",
"data-[checked]:not-[data-answer-visible]:bg-primary"
"size-6 flex-shrink-0 flex-grow-0 rounded-full bg-disabled text-white",
"[:is([data-state='checked'],:hover)_>_&]:text-white",
"[:is([data-state='checked'],:hover)_>_&]:bg-primary-action",
"[:is([data-state='checked'],:hover)[data-answer-visible]_>_&]:bg-white",
"[:is([data-state='checked'],:hover)[data-answer-visible]_>_&]:text-error",
"[:is([data-state='checked'],:hover)[data-answer-visible][data-selected-correct]_>_&]:text-success"
)}
>
<Center
className={cn(
"size-6 flex-shrink-0 flex-grow-0 rounded-full bg-disabled text-white",
// TODO: Upon removing custom radio props, flip remove `data` for checked
"[:is([data-checked],:hover)_>_&]:text-white",
"[:is([data-checked],:hover)_>_&]:bg-primary-action",
"[:is([data-checked],:hover)[data-answer-visible]_>_&]:bg-white",
"[:is([data-checked],:hover)[data-answer-visible]_>_&]:text-error",
"[:is([data-checked],:hover)[data-answer-visible][data-selected-correct]_>_&]:text-success"
)}
>
<p className="text-lg font-bold leading-none">
{String.fromCharCode(97 + index).toUpperCase()}
</p>
</Center>
<span>{label}</span>
</HStack>
</label>
<input {...radioInputProps} />
</>
<p className="text-lg font-bold leading-none">
{String.fromCharCode(97 + index).toUpperCase()}
</p>
</Center>
<span className="w-full">{label}</span>
</RadioGroup.Item>
</HStack>
)
}