Composant Circle Choice ReactJS

Composant Circle Choice ReactJS
Alexandre P. dans Dev - mis à jour le 20-01-2025

Découvrez comment créer un composant React innovant pour remplacer select/radio, avec React-hook-form, Tailwind, et NextJS. Intuitif et personnalisable ! 🚀

Hey les développeurs et développeuses, 👋

Aujourd'hui nous allons développer un composant React pour remplacer un composant select ou radio.

Dernièrement je vous parlais d'un composant ReactJS que j'ai designé de manière à rendre plus pratique un choix multiple de 3, 4 ou 5 items:

cs2strats-after.gif

Surtout dans une situation où on l'on peut mettre une icone ou une image pour un choix, cela rend les choses beaucoup plus simple à visualiser.

Pour réaliser ce composant, j'ai utilisé React, React-hook-form, Tailwind sur NextJS 14.

Composants

Je commence par créer un composant CircleButton.tsx:

export type CircleButtonProps = {
  children: React.ReactNode;
  onClick: () => void;
}

export const CircleButton = ({ children, onClick }: CircleButtonProps) => {
  return (
    <div
      className="w-28 h-28 bg-white border border-gray-200 rounded-full shadow dark:bg-gray-800 dark:border-gray-700 flex flex-col items-center justify-center cursor-pointer hover:bg-gray-300"
      onClick={onClick}
    >
      {children}
    </div>
  );
};

Ensuite, je crée un composant CircleChoice.tsx:

import { useState } from "react";
import { Control, Controller } from "react-hook-form";
import { CircleButton } from "./circleButton";
import cn from "classnames";
import styles from "./CircleChoice.module.css"

type Choice = {
  value: string;
  content: React.ReactNode;
};

export type CircleChoiceProps<T> = {
  fieldTitle: string;
  fieldName: string;
  control: Control<any>;
  choices: Choice[];
};

export const CircleChoice = <T,>({ fieldName, control, fieldTitle, choices }: CircleChoiceProps<T>) => {
  const [isOpened, setIsOpened] = useState(false);

  const radius = 120; 

  return (
    <div className="mb-6 relative flex flex-col items-center justify-center">
      <Controller
        control={control}
        name={fieldName}
        render={({ field }) => (
          <>
          <div 
            className="fixed top-0 left-0 w-full h-full bg-black opacity-50 z-10" 
            style={{ display: isOpened ? "block" : "none" }} 
            onClick={() => setIsOpened(false)}
          >
          </div>
          <div className="relative z-20">
            <CircleButton
              onClick={() => {
                setIsOpened(!isOpened);
                field.onChange(field.value);
              }}
            >
              {
              field.value ? 
                choices.find(choice => choice.value === field.value)?.content : 
                fieldTitle
              }
            </CircleButton>

            {isOpened && (
              <div
                className={`absolute inset-0 transition-all duration-500 scale-100`}
                onClick={() => setIsOpened(false)}
              >
                
                <div
                  onClick={() => setIsOpened(false)}
                  className="absolute w-12 h-12 bg-gray-300 border border-gray-400 rounded-full shadow dark:bg-gray-800 dark:border-gray-700 flex flex-col items-center justify-center cursor-pointer hover:bg-gray-400"
                  style={{
                    transform: `translate(${radius / 4}px, ${radius / 4}px)`,
                  }}>
                    ✖
                </div>

                {choices.map((choice, index) => {
                  return (
                    <div
                      key={choice.value}
                      className={cn("absolute transition-transform duration-500", 
                        styles[`circle-button-anim-${choices.length}-${index}`]
                      )}
                    >
                      <CircleButton
                        onClick={() => {
                          field.onChange(choice.value);
                          setIsOpened(false);
                        }}
                      >
                        {choice.content}
                      </CircleButton>
                    </div>
                  );
                })}
              </div>
            )}
          </div>
          </>
        )}
      />
    </div>
  );
};

Et enfin j'ajoute mon module pour le CSS dans un fichier CircleChoice.module.css:

@keyframes moveAway-3-0 {
  from {
    transform: translate(0px, 0px);
    opacity: 0;
  }
  to {
    transform: translate(0px, -120px);
    opacity: 1;
  }
}

@keyframes moveAway-3-1 {
  from {
    transform: translate(0px, 0px);
    opacity: 0;
  }
  to {
    transform: translate(103.92px, 60px); 
    opacity: 1;
  }
}

@keyframes moveAway-3-2 {
  from {
    transform: translate(0px, 0px);
    opacity: 0;
  }
  to {
    transform: translate(-103.92px, 60px); 
    opacity: 1;
  }
}

.circle-button-anim-3-0 {
  animation: moveAway-3-0 0.5s ease-out forwards;
}

.circle-button-anim-3-1 {
  animation: moveAway-3-1 0.5s ease-out forwards;
}

.circle-button-anim-3-2 {
  animation: moveAway-3-2 0.5s ease-out forwards;
}

@keyframes moveAway-4-0 {
  from {
    transform: translate(0px, 0px);
    opacity: 0;
  }
  to {
    transform: translate(120px, 0px); 
    opacity: 1;
  }
}

@keyframes moveAway-4-1 {
  from {
    transform: translate(0px, 0px);
    opacity: 0;
  }
  to {
    transform: translate(0px, 120px); 
    opacity: 1;
  }
}

@keyframes moveAway-4-2 {
  from {
    transform: translate(0px, 0px);
    opacity: 0;
  }
  to {
    transform: translate(-120px, 0px);
    opacity: 1;
  }
}

@keyframes moveAway-4-3 {
  from {
    transform: translate(0px, 0px);
    opacity: 0;
  }
  to {
    transform: translate(0px, -120px); 
    opacity: 1;
  }
}

.circle-button-anim-4-0 {
  animation: moveAway-4-0 0.5s ease-out forwards;
}

.circle-button-anim-4-1 {
  animation: moveAway-4-1 0.5s ease-out forwards;
}

.circle-button-anim-4-2 {
  animation: moveAway-4-2 0.5s ease-out forwards;
}

.circle-button-anim-4-3 {
  animation: moveAway-4-3 0.5s ease-out forwards;
}

@keyframes moveAway-5-0 {
  from {
    transform: translate(0px, 0px);
    opacity: 0;
  }
  to {
    transform: translate(120px, 0px); 
    opacity: 1;
  }
}

@keyframes moveAway-5-1 {
  from {
    transform: translate(0px, 0px);
    opacity: 0;
  }
  to {
    transform: translate(37.08px, 114.43px); 
    opacity: 1;
  }
}

@keyframes moveAway-5-2 {
  from {
    transform: translate(0px, 0px);
    opacity: 0;
  }
  to {
    transform: translate(-97.08px, 70.63px); 
    opacity: 1;
  }
}

@keyframes moveAway-5-3 {
  from {
    transform: translate(0px, 0px);
    opacity: 0;
  }
  to {
    transform: translate(-97.08px, -70.63px); 
    opacity: 1;
  }
}

@keyframes moveAway-5-4 {
  from {
    transform: translate(0px, 0px);
    opacity: 0;
  }
  to {
    transform: translate(37.08px, -114.43px); 
    opacity: 1;
  }
}

.circle-button-anim-5-0 {
  animation: moveAway-5-0 0.5s ease-out forwards;
}

.circle-button-anim-5-1 {
  animation: moveAway-5-1 0.5s ease-out forwards;
}

.circle-button-anim-5-2 {
  animation: moveAway-5-2 0.5s ease-out forwards;
}

.circle-button-anim-5-3 {
  animation: moveAway-5-3 0.5s ease-out forwards;
}

.circle-button-anim-5-4 {
  animation: moveAway-5-4 0.5s ease-out forwards;
}

Enfin, à l'utilisation, j'ajoute dans mon composant le bout de code suivant:

const VEHICLES = {
  "car": "Voiture",
  "plane": "Avion",
  "train": "Train",
  "bus": "Bus"
}

// ...

<CircleChoice 
          fieldName="vehicle" 
          control={control} 
          fieldTitle="Choix du véhicule" 
          choices={Object.entries(VEHICLES).map(([key, value]) => ({ 
            value, 
            content: (<>
              <img 
                src={`/vehicles/${value.toLowerCase()}.png`} 
                alt={key} className="w-10 h-10 mb-2" />
              <span className="text-sm">{key}</span>
            </>) 
          }))} 
        /> 

Voilà, c'est assez simple et efficace je trouve, bon code à vous ! 😉

Je vous laisse personnaliser cela selon vos besoins.

#reactjs#typescript#dev#component

user picture

Alexandre P.

Développeur passionné depuis plus de 20 ans, j'ai une appétence particulière pour les défis techniques et changer de technologie ne me fait pas froid aux yeux.


Nous utilisons des cookies sur ce site pour améliorer votre expérience d'utilisateur.