Untitled

 avatar
unknown
plain_text
2 months ago
5.9 kB
2
Indexable
import type { FlatMenuItem, FlatMenuItemChildsData } from '@components/SecondaryNavigation/common/types';
import { debounce } from '@utils/debounce';
import { useCallback, useEffect, useRef, useState } from 'react';

export const useAdaptiveTabs = (
  tabs: FlatMenuItem[],
  updateChildsMoreButtonItem: (childsData: FlatMenuItemChildsData) => void
) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const buttonBlockRef = useRef<HTMLDivElement>(null);
  const headingBlockRef = useRef<HTMLDivElement>(null);
  const [availableWidth, setAvailableWidth] = useState(0);
  const [tabWidths, setTabWidths] = useState<{ [key: string]: number }>({});

  // ✅ Use the extracted function
  const handleResize = useCallback(
    debounce(() => {
      setAvailableWidth(
        calculateAvailableSpace({
          container: containerRef.current,
          buttonBlock: buttonBlockRef.current,
          headingBlock: headingBlockRef.current,
        })
      );
    }, 50),
    []
  );

  useEffect(() => {
    setAvailableWidth(
      calculateAvailableSpace({
        container: containerRef.current,
        buttonBlock: buttonBlockRef.current,
        headingBlock: headingBlockRef.current,
      })
    );
  }, []);

  useEffect(() => {
    if (!containerRef.current) {
      return;
    }

    const observer = new ResizeObserver(handleResize);
    observer.observe(containerRef.current);

    return () => observer.disconnect();
  }, [handleResize]);

  useEffect(() => {
    if (!containerRef.current) {
      return;
    }

    const tabElements = containerRef.current.querySelectorAll('[data-tab-item]');
    const newWidths: { [key: string]: number } = {};

    for (const el of tabElements) {
      const id = el.getAttribute('data-id');
      const marginEnd = Number.parseInt(getComputedStyle(el).getPropertyValue('margin-inline-end')) || 0;

      if (id) {
        newWidths[id] = el.getBoundingClientRect().width + marginEnd;
      }
    }

    setTabWidths(newWidths);
  }, []);

  useEffect(() => {
    let totalWidth = 0;
    const hiddenTabsList: typeof tabs = [];

    const processedTabs = [...tabs];

    for (const tab of processedTabs) {
      totalWidth += tabWidths[tab.id] || 0;
      if (totalWidth > availableWidth) {
        hiddenTabsList.push(tab);
      }
    }

    const moreButtonChildRelatedProps = {
      childIds: hiddenTabsList.map((item) => item.id),
      childIdsCollection: hiddenTabsList.reduce<Record<string, boolean>>((acc, currentItem) => {
        acc[currentItem.id] = true;
        return acc;
      }, {}),
    };

    updateChildsMoreButtonItem(moreButtonChildRelatedProps);
  }, [availableWidth, tabWidths, tabs, updateChildsMoreButtonItem]);

  return { containerRef, buttonBlockRef, headingBlockRef };
};


const MORE_BUTTON_MARGIN_START = 20;

const calculateAvailableSpace = ({
  container,
  buttonBlock,
  headingBlock,
}: {
  container: HTMLDivElement | null;
  buttonBlock: HTMLDivElement | null;
  headingBlock: HTMLDivElement | null;
}): number => {
  if (!container || !buttonBlock || !headingBlock) {
    return 0;
  }

  const totalContainerWidth = container.offsetWidth;
  const buttonBlockWidth = buttonBlock.offsetWidth;
  const headingTitleBlockWidth = headingBlock.offsetWidth;
  const headingTitleMarginEnd =
    Number.parseInt(getComputedStyle(headingBlock).getPropertyValue('margin-inline-start')) || 0;

  const moreButtonElement = container.querySelector('[data-more-tab-item]');
  let moreButtonWidthAndMargin = 0;

  if (moreButtonElement) {
    moreButtonWidthAndMargin = moreButtonElement.getBoundingClientRect().width + MORE_BUTTON_MARGIN_START;
  }

  return (
    totalContainerWidth - buttonBlockWidth - headingTitleBlockWidth - headingTitleMarginEnd - moreButtonWidthAndMargin
  );
};

import { calculateAvailableSpace } from '../src/utils/calculateAvailableSpace';

describe('calculateAvailableSpace', () => {
  let container: HTMLDivElement;
  let buttonBlock: HTMLDivElement;
  let headingBlock: HTMLDivElement;
  let moreButton: HTMLDivElement;

  beforeEach(() => {
    document.body.innerHTML = `
      <div id="container" style="width: 500px;"></div>
      <div id="buttonBlock" style="width: 100px;"></div>
      <div id="headingBlock" style="width: 150px; margin-inline-start: 10px;"></div>
      <div id="moreButton" style="width: 50px;"></div>
    `;

    container = document.getElementById('container') as HTMLDivElement;
    buttonBlock = document.getElementById('buttonBlock') as HTMLDivElement;
    headingBlock = document.getElementById('headingBlock') as HTMLDivElement;
    moreButton = document.getElementById('moreButton') as HTMLDivElement;

    // Mock `querySelector` to return the more button
    jest.spyOn(container, 'querySelector').mockReturnValue(moreButton);
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  it('should calculate available space correctly', () => {
    const result = calculateAvailableSpace({ container, buttonBlock, headingBlock });
    expect(result).toBe(500 - 100 - 150 - 10 - 50 - 20); // 20 is MORE_BUTTON_MARGIN_START
  });

  it('should return 0 if container is null', () => {
    const result = calculateAvailableSpace({ container: null, buttonBlock, headingBlock });
    expect(result).toBe(0);
  });

  it('should return 0 if buttonBlock is null', () => {
    const result = calculateAvailableSpace({ container, buttonBlock: null, headingBlock });
    expect(result).toBe(0);
  });

  it('should return 0 if headingBlock is null', () => {
    const result = calculateAvailableSpace({ container, buttonBlock, headingBlock: null });
    expect(result).toBe(0);
  });

  it('should return full container width if there are no other elements', () => {
    const result = calculateAvailableSpace({ container, buttonBlock: null, headingBlock: null });
    expect(result).toBe(500);
  });
});
Editor is loading...
Leave a Comment