Trying to build a 5-star rating component just like the example link.
peer-hover
seems to work, yet peer-checked
doesn't work as peer-hover
does.
(items
contain an array [1,2,3,4,5], by the way)
Could you point out the reason why this problem happens?
import { RadioGroup } from '@headlessui/react'
import { useController } from "react-hook-form";
import { classNames } from '../libs/frontend/utils'
import { StarIcon } from '@heroicons/react/24/outline';
import { StarIcon as StarIconSolid } from '@heroicons/react/20/solid';
export const RadioGroupStars = (props) => {
const {
field: { value, onChange }
} = useController(props);
const { items } = props;
return (
<>
<RadioGroup
value={value}
onChange={onChange}
className="w-full my-1">
<RadioGroup.Label className="sr-only"> Choose a option </RadioGroup.Label>
<div className="flex flex-row-reverse justify-center gap-1">
{items.map((item) => (
<RadioGroup.Option
key={item}
value={item}
className={({ active, checked }) =>
classNames(
'cursor-pointer text-gray-200',
'flex-1 hover:text-yellow-600',
'peer',
'peer-hover:text-yellow-600 peer-checked:text-yellow-500',
active ? 'text-yellow-500' : '',
checked ? 'text-yellow-500' : '',
)
}
>
<RadioGroup.Label as={StarIconSolid} className='' />
</RadioGroup.Option>
))}
</div>
</RadioGroup>
</>
);
}
I think the reason peer-checked
not working could be because RadioGroup.Option
outputs a div
wrapping the icon as Label
(instead of a input
), therefore the pseudo-class :checked
is not applied, while peer-hover
does work.
Because the component has access to selected value
and the rating values are comparable:
items contain an array [1,2,3,4,5]
RadioGroup.Option
could compare own value with the selected value as a condition to render with different classes (or equivalently, compare the index
).
Because this list also uses flex-row-reverse
to implement the siblings hover, consider to reverse()
the items
before map()
to keep the iterated items in correct order.
Tested the example in live on: stackblitz (omitted logic for react-hook-form
for simplicity):
<div className="flex flex-row-reverse justify-center gap-1">
{[...items].reverse().map((item) => (
<RadioGroup.Option
key={item}
value={item}
className={({ active, checked }) =>
classNames(
"cursor-pointer text-gray-200",
"flex-1 hover:text-yellow-400",
"peer",
"peer-hover:text-yellow-400",
active ? "text-yellow-500" : "",
checked ? "text-yellow-500" : "",
// 👇 Add a compare with selected value here
value >= item ? "text-yellow-500" : ""
)
}
>
<RadioGroup.Label as={BsStarFill} className="w-6 h-6" />
</RadioGroup.Option>
))}
</div>
On a side note, because RadioGroup
requires a setValue
(a state set
function) for its onChange
prop, not too sure if the field.onChange
returned by useController()
would work with it.
If not, perhaps consider to host a state in the component and sync with useController, so that its functions could be still be used.