top of page

The responsive pill of your dream

  • julienmesser
  • Feb 2
  • 2 min read

See full prescribing information for complete details
See full prescribing information for complete details

Time for a new post, let's dive into an another exciting try with React...

Today's challenge: display a horizontal "pills" components. When there's not enough place to show everything, they wont't wrap to the next line. Instead, the items will be collapsed:




Let's explain how to realize it as a kind of tutorial:


Starting with a simple container with some pills


We start with a very simple container that displays pills:

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

Getting the information about the width


So, now the challenge is to identify the available space and the width of each pill to be rendered. For this we will observe the container's width on resize and store it in a a state "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);
  };
}, []);

On the pill's side we will use a "useLayoutEffect" hook which is called after each component's refresh. At this point we will pass the size to the pill's container by calling a handler:

useLayoutEffect(() => {
    onBoundingChange(pillKey, myRef.current.getBoundingClientRect());
}, [label]);

This handler store the size of all the pills in a dictionary (not an array: we need to identify the pills properly in case pills are added or removed):

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

Computation how many pills can be displayed


In the container we compute the number of displayable pills. This is done in a useEffect executed if the dictionary changes or if the window size is updated:

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]);

And finally in the rendering we remove the pills which have to be collapsed with a "slice" and summarize the number of collapsed pills:

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

References

 
 
 

Comments


JulienGraphic.jpeg

Hello it's me Julien

Passionate about many fields such as IT, I created this blog. I hope you enjoy reading me.

    Subscribe if you want to follow my publications.

    Inscription

    Merci pour votre inscription!

    bottom of page