next js 탭 컨트롤 키보드 접근성 적용

import { useState, useCallback, useEffect, useRef, createRef } from 'react';

interface TabProps {
  tabIndex: number;
  ref: React.RefObject<HTMLElement>;
}

interface TabListProps {
  onKeyDown: (e: React.KeyboardEvent) => void;
  ref: React.RefObject<HTMLElement>;
}

export function useTabs() {
  const [focusedIndex, setFocusedIndex] = useState<number>(0);
  const tabListRef = useRef<HTMLElement>(null);
  const tabRefs = useRef<React.RefObject<HTMLElement>[]>([]);
  const isInitialMount = useRef(true);

  const getTabCount = useCallback(() => {
    return tabListRef.current?.querySelectorAll('[role="tab"]').length || 0;
  }, []);

  useEffect(() => {
    if (tabListRef.current) {
      const count = getTabCount();
      tabRefs.current = Array(count)
        .fill(null)
        .map((_, i) => tabRefs.current[i] || createRef<HTMLElement>());
    }
  }, [getTabCount]);

  useEffect(() => {
    if (isInitialMount.current) {
      isInitialMount.current = false;
      return;
    }

    if (focusedIndex >= 0 && tabRefs.current[focusedIndex]?.current) {
      tabRefs.current[focusedIndex].current?.focus();
    }
  }, [focusedIndex]);

  const onKeyDown = useCallback(
    (e: React.KeyboardEvent) => {
      const count = getTabCount();
      if (count === 0) return;

      let newIndex = focusedIndex;

      if (e.key === 'ArrowRight') {
        e.preventDefault();
        newIndex = (focusedIndex + 1) % count;
      } else if (e.key === 'ArrowLeft') {
        e.preventDefault();
        newIndex = (focusedIndex - 1 + count) % count;
      } else if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        const element = tabRefs.current[focusedIndex]?.current;
        if (element) {
          element.click();
        }
        return;
      }

      if (newIndex !== focusedIndex) {
        setFocusedIndex(newIndex);
      }
    },
    [focusedIndex]
  );

  const getTabProps = useCallback(
    (index: number): TabProps => {
      if (!tabRefs.current[index]) {
        tabRefs.current[index] = createRef<HTMLElement>();
      }

      return {
        tabIndex: index === focusedIndex ? 0 : -1,
        ref: tabRefs.current[index],
      };
    },
    [focusedIndex]
  );

  const getTabListProps = useCallback((): TabListProps => {
    return {
      onKeyDown,
      ref: tabListRef,
    };
  }, [onKeyDown]);

  return {
    focusedIndex,
    getTabListProps,
    getTabProps,
  } as const;
}

Comments

답글 남기기