SKYnSPACE
SKYnSPACE

Reputation: 13

Trying to make 5 star rating using React, tailwind CSS, and Headless UI

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>
    </>
  );
}

Upvotes: 0

Views: 1782

Answers (2)

Santosh Karanam
Santosh Karanam

Reputation: 1217

i used below code with react and tailwing, using simple controls as much as possible

    export default function RatingControl() {

    const [rating, setRating] = useState(0);

    return (
    <div className="grid w-full sm:col-span-3 sm:grid-cols-10">
      <dt className="pb-2 font-bold text-gray-500 sm:col-span-10 ">
        Rate Survey
      </dt>
      {Array.from({ length: 5 }, (_, k) => k + 1).map((item) => (
        <button
          key={`buttonItem${item}`}
          type="button"
          onClick={() => {
            setRating(item);
          }}
          className={classNames(
            rating !== item ? 'bg-black' : 'bg-gray-800',
            ' mr-4 mt-2 lg:mt-0 rounded-md text-lg font-semibold text-gray-500 shadow-sm hover:text-white hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white'
          )}
        >
          {item}
        </button>
      ))}
    </div>
    );
    }

Upvotes: 0

John Li
John Li

Reputation: 7447

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.

Upvotes: 0

Related Questions