How to find which chip got wrapped to next line in material ui autocomplete

Viewed 418

I have a material ui autocomplete like this

autocomplete

I want to calculate that the fourth chip wrapped to next line so that I can make further calculations. Is there a way to do that in javascript/react with this autocomplete of material UI.

So far the documentation of autocomplete material ui is not helping in this case.

Basically what I have to do is to make the number of limitTags dynamic so that on changing the width of the autocomplete the limitTags is set accordingly and is not a static number. So far my idea was to calculate the width of each tag, add it altogether and compare with the container width. That has been done but it still is buggy because as you can see in the image attached even though the combined length of the tags is less than the container width, the limitTags is 4 but it should be 3 because the last one wrapped to the next line. And now I'm stuck again. How can I detect which of the very first tag wrapped to the next line in order to calculate the limitTags value

Here's what I have done so far

const DEFAULT_LIMIT_TAGS = -1;

const useLimitTags = (value, autoCompleteContainerRef, tagRef) => {
  const [limitTags, setLimitTags] = useState(DEFAULT_LIMIT_TAGS);
  const [autoCompleteContainerWidth, setAutoCompleteContainerWidth] = useState(0);
  const parentNode = useRef(null);

  useOnResize(autoCompleteContainerRef, setAutoCompleteContainerWidth);

  useLayoutEffect(() => {
    if (!parentNode.current && tagRef?.current) {
      parentNode.current = tagRef.current.parentNode;
    }

    if (parentNode?.current) {
      const children = [...parentNode.current.children];
      const tags = children.slice(0, value.length);
      const endAdornmentWidth = children[children.length - 2].offsetWidth;
      const containerWidth = autoCompleteContainerWidth - endAdornmentWidth - 20; // 20 is the margin around the last closing icon and the dropd own icon

      const tagsCombinedWidth = tags.reduce((sum, child, index, arr) => {
        const tagsWidth = sum + child.offsetWidth;

        if (tagsWidth > containerWidth) {
          setLimitTags(index - 1);
          arr.splice(0, index + 1); // break
        }

        return tagsWidth;
      }, 0);

      if (tagsCombinedWidth <= containerWidth) {
        setLimitTags(DEFAULT_LIMIT_TAGS);
      }
    }
  }, [value, autoCompleteContainerWidth, tagRef]);

  return limitTags;
};

And useOnResize is using Resize Observer API to calculate width of the changing autocomplete container

import { useEffect } from 'react';
import isFunction from 'lodash-es/isFunction';

const useOnResize = (componentRef, callback) => {
  const shouldMountObserver = isFunction(callback) && componentRef?.current;
  const resizeObserver =
    shouldMountObserver &&
    new ResizeObserver((entries) => {
      for (const entry of entries) {
        const { width: detectedWidth } = entry.contentRect;
        callback(detectedWidth);
      }
    });
  useEffect(() => {
    const savedRef = componentRef?.current;
    shouldMountObserver && resizeObserver?.observe(componentRef.current);

    return () => {
      if (savedRef) {
        resizeObserver?.unobserve(savedRef);
      }
    }; // Resized observer omitted in deps array on purpose as it is not supposed to change between rerender
  }, [componentRef?.current]); // eslint-disable-line react-hooks/exhaustive-deps
};

export default useOnResize;

1 Answers

Here's one approach using HTMLElement.offsetTop:

  1. Collect your DOM Element chips in an array (I believe you are doing this with your const tags constant).
  2. Iterate through your collection of chips, and use some means of:
    1. Tracking the index of each chip in your collection. You can use this index to get the total number of chips aligned in the same row.
    2. Capturing an identifier associated with the chip. I believe Material-UI automatically adds a data-tag-index="[integer]" attribute to each chip. This is to identify the first chip that falls onto the next row. You can use your own custom identifier with the data-[custom]="[custom_value]" attribute.
    3. Finding the vertical height of each chip from the top of its closest ancestor. Use the HTMLElement.offsetTop property of the chip to get this height.
  3. The vertical height of the first chip is your baseline value. As you iterate through the collection you can detect when a chip has a vertical height that is different from the baseline value. The first chip with a different height marks the first one to have been pushed down to a new row.
  4. The index (as demonstrated by the code sample below) is the number of chips aligned in the first row.
  5. Use some identifier (e.g. chipDOMElement.dataset.tagIndex or something else you've created ) to identify the actual 'first-chip-in-next-row' for further use.

The code below illustrates the above steps, and is used in this jsFiddle demo (click Run to start the demo).

Code sample

React.useEffect( () => {
    let chips = document.querySelectorAll( '.MuiChip-root' );
    let index = 0;
    let nextRowChipId = null;

    if ( typeof( chips ) != 'undefined' && chips != null &&
         typeof( chips[0] ) != 'undefined' && chips[0] != null
    ) {
        let baseline = chips[0].offsetTop;
        for ( let chip of chips ) {
            if ( chip.offsetTop > baseline ) {
                nextRowChipId = chip.dataset.tagIndex;    
                break;
            } else {
                index++;
            }
        }
        setChipsInfo( {
            numberOfFirstRowChips: index,
            nextRowChipId
        } );
    } else {
        setChipsInfo( {
            numberOfFirstRowChips: 0,
            nextRowChipId: null
        } );
    }
}, [ list ] ); // list is an array of chip data (objects). The array is managed with React.useState().
Related