Shrink of inside label in a flex item

Viewed 69

Hello everybody I need a help. The issue is the following: I have flex-container with tabs (number of tabs can be different as they are passed as array in React component).

Each tab contains label and span with a number. By initial conditions for this task, label should not be shorter than 3 letters + "..." (we apply ellipsis for it).

As I have understood, there is no other solution but to do it manually from code (ch unit is based on symbol 0, that's why this approach gives inaccurate result). But let's move to key issue. Text in label can have different length, and we can have different number of tabs.

I need to set tabs in container (let it be restricted by max-width of 900px) as long as it possible. What does it mean: we set tabs with full length of label if it is possible, if not - label shrinks until it reaches min-width (6ch). If number of tabs is too large (all labels have min-width, but tabs exceed container), I will not render them at all. I am going to implement that with help of useLayoutEffect with checks of exceeding container there.

The main problem now is that spans overflow tabs, in other words labels have possibility to shrink, but instead of that other tabs start shrinking arising problems with span. I have tried to use grid with templates columns of 1fr width (number of columns I can set by passing length of array to styled component). That works, but I need to have by tabs aligned to left side (instead of that they would take all available space) and I have problems with extra empty space if label + gap + span < 1fr of container.

By this moment I have no solution, but to hardcode min-width of tab, but all of us understand that it is unacceptable (not to mention the fact that there can be 10,000, for example, in span). I am asking for any help. I would be the happiest person if I found solution.

I have attached images with demonstration of issue, code, and link to codeSandbox with example (there you can insert tabs in mock_data, change length of words). CodeSandBox - https://codesandbox.io/s/gracious-dijkstra-61s9sp?file=/src/Component.jsx:0-1606

tabs have enough space

labels can shrink, but instead spans overflow tabs

import styled from "@emotion/styled";

const TabsList = styled.ul`
  list-style: none;
  display: flex;
  justify-content: flex-start;
  gap: 20px;
  margin: 0;
  margin-left: 20px;
  width: 100%;
  max-width: 900px;
  background: yellowgreen;
  /* because the first tab always will be "all" */
  li:first-of-type label {
    min-width: 20px;
  }
`;

const singleNumPaddingStyles = "0 8px";

const KeywordTab = styled.li`
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  padding-bottom: 4px;
  gap: 8px;
  label {
    display: block;
    font-weight: 400;
    line-height: 23px;
    cursor: pointer;
    user-select: none;
    text-transform: capitalize;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    &:hover {
      color: blue;
    }
    /* trying to set minimum 3char + ... */
    min-width: 6ch;
  }

  span {
    color: white;
    line-height: 23px;
    background-color: pink;
    user-select: none;
    padding: ${({ singleNum }) =>
      singleNum ? singleNumPaddingStyles : "0 4px"};
    border-radius: 4px;
  }
`;

const Group = ({ label, number }) => (
  <KeywordTab singleNum={number < 10}>
    <label>{label}</label>
    <span>{number}</span>
  </KeywordTab>
);

export const View = ({ dictionaries }) => {
  //logic (useLayoutEffect)
  return (
    <TabsList>
      {dictionaries.map(({ label, total }, index) => (
        <Group key={index} label={label} number={total} />
      ))}
    </TabsList>
  );
};

//very-very-very bad desicion: hardcode min-width
// of tab ~ 88px (53px for the first - it will always be "all")

1 Answers

I have achieved approximately desired behaviour by using grid instead of flexbox. On parent container i've started to set number of columns dynamically and give them width of minmax(min-content, max-content) (except for the first column as there is always expected element - All). Separate tab was rewritten to grid with 2 columns - (1fr min-content respectively)

import styled from "@emotion/styled";
import { useLayoutEffect, useRef, useState, forwardRef } from "react";


export const TabsListGrid = styled.ul`
  margin: 0;
  width: 100%;
  display: grid;
  list-style: none;
  background: yellowgreen;
  grid-gap: 20px;
  grid-template-columns: min-content repeat(${({ columns }) => columns - 1}, minmax(min-content, max-content));
  max-width: 712px;
  li:first-of-type label {
    min-width: min-content;
  }
`

const singleNumPaddingStyles = "0 8px";

const KeywordTab = styled.li`
  flex-shrink: 0;
  position: relative;
  overflow: hidden;
  display: grid;
  grid-template-columns: 1fr min-content;
  align-items: center;
  padding-bottom: 4px;
  grid-gap: 8px;
  label {
    font-weight: 400;
    line-height: 23px;
    cursor: pointer;
    user-select: none;
    text-transform: capitalize;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    &:hover {
      color: blue;
    }
    min-width: 6ch;
  }

  span {
    color: white;
    line-height: 23px;
    background-color: pink;
    user-select: none;
    padding: ${({ singleNum }) =>
      singleNum ? singleNumPaddingStyles : "0 4px"};
    border-radius: 4px;
  }
`;

const Group = forwardRef(({ label, number }, ref) => (
  <KeywordTab ref={ref} singleNum={number < 10}>
    <label>{label}</label>
    <span>{number}</span>
  </KeywordTab>
));

export const View = ({ dictionaries }) => {
  const [visible, setVisible] = useState(true);

  const tabsRef = useRef(null);
  const tabRef = useRef([]);
  useLayoutEffect(() => {
    if (tabsRef?.current?.getBoundingClientRect().width < 500) {
      setVisible(false);
    } else {
      setVisible(true);
    }
    console.log(tabRef);
  }, []);
  return (
    <>
      {visible && (
        <TabsListGrid ref={tabsRef} columns={dictionaries.length}>
          {dictionaries.map(({ label, total }, index) => (
            <Group
              ref={(el) => (tabRef.current[index] = el)}
              key={index}
              label={label}
              number={total}
            />
          ))}
        </TabsListGrid>
      )}
    </>
  );
};

Related