La pilule réactive de vos rêves
- julienmesser
- 2 févr.
- 2 min de lecture

Il est temps pour un nouveau post, plongeons dans un autre essai passionnant avec React...
Le défi du jour : afficher des composants « pilules » horizontaux. Lorsqu'il n'y a pas assez de place pour tout afficher, ils ne doivent pas s'afficher sur la ligne suivante. Au lieu de cela, les pilules sont réduites :

Expliquons comment le réaliser sous forme de tutoriel :
A l'origine: un conteneur avec quelques pilules
Nous commençons avec un conteneur très simple qui affiche des pilules :
export const PillContainer= ({ pills }) => {
const maxDisplayedPills = pills.length;
return (
<div className="flex flex-row">
{ pills.slice(0, maxDisplayedPills).map((pill) => (<Pill key={pill.id} label={pill.label} />))
}
</div>
);
}
const Pill = ({ label }) => {
return (<div className="font-mono px-1 mx-0.5 border
border-current rounded-full whitespace-nowrap">
{label}
</div>);
}
Infos sur la largeur
Le défi consiste maintenant à identifier l'espace disponible et la largeur de chaque pilule à restituer. Pour cela, nous allons observer la largeur du conteneur lors du redimensionnement et la stocker dans un état "windowWidth" :
const [windowWidth, setWindowWidth] = useState(0);
// On initialization: setup an observer to watch the container's width
useEffect(() => {
const onResize = () => {
setWindowWidth(containerRef.current.offsetWidth);
};
onResize();
window.addEventListener('resize', onResize);
// Remove the event listener when the component unmounts
return () => {
window.removeEventListener('resize', onResize);
};
}, []);
Du côté de la pilule, nous utiliserons un hook "useLayoutEffect" qui est appelé après chaque rafraîchissement du composant. À ce stade, nous allons passer la taille au conteneur de la pilule en appelant un "handler" :
useLayoutEffect(() => {
onBoundingChange(pillKey, myRef.current.getBoundingClientRect());
}, [label]);
Cet "handler" stocke la taille de toutes les pilules dans un dictionnaire (pas un tableau : nous devons identifier correctement les pilules au cas où des pilules seraient ajoutées ou supprimées) :
const [pillsPositions, setPillsPositions] = useState({});
const onPillBoundingChange = (id, rect) => {
// Update the dictionary of pills's positions
setPillsPositions((prevAllPillsRightPosition) => ({ ...prevAllPillsRightPosition, [id]: { left: rect.left, right: rect.right } }));
};
Calcul du nombre de pilules pouvant être affichées
Dans le conteneur, nous calculons le nombre de pilules affichables. Cela se fait dans un useEffect exécuté si le dictionnaire change ou si la taille de la fenêtre est mise à jour :
const [maxDisplayedPills, setMaxDisplayedPills] = useState(pills.length);
// On size change: calculate the amount of pills that can be displayed
useEffect(() => {
const offsetFirstPill = (pillsPositions.length > 0) ? pillsPositions['(Pill 1)'].left : 0;
const amountDisplayablePills = Object.values(pillsPositions).filter((value) => value.right - offsetFirstPill < windowWidth).length;
setMaxDisplayedPills(amountDisplayablePills);
}, [pillsPositions, windowWidth]);
Et enfin dans le rendu on supprime les pilules qui doivent être réduites avec la fonction "slice" et on affiche à droite le nombre de pilules réduites :
return (
<div ref={containerRef} className="flex flex-row">
{ pills.slice(0, maxDisplayedPills).map((pill) => (<Pill key={pill.id} pillKey={pill.id} label={pill.label}
onBoundingChange={onPillBoundingChange} />))
}
{ pills.length > maxDisplayedPills &&
<Pill pillKey={masterKey} label={`+${pills.length - maxDisplayedPills}`} onBoundingChange={() => {}} />}
</div>
);
Références
Vous trouverez ici le code source: https://github.com/axis68/design-library/tree/main/src/components/pills
Vous trouverez ici le composant rendu sur Storybook: https://axis68.github.io/design-library/?path=/docs/components-pillcontainer--docs
Comments