라디오버튼 접근성 적용 훅

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

interface RadioProps {
  tabIndex: number;
  ref: React.RefObject<HTMLLabelElement>;
}

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

export function useRadioGroup(selectedIndex: number | null = null) {
  const [focusedIndex, setFocusedIndex] = useState<number>(-1);
  const groupRef = useRef<HTMLDivElement>(null);
  const radioRefs = useRef<React.RefObject<HTMLLabelElement>[]>([]);

  const getRadioCount = useCallback(() => {
    return groupRef.current?.querySelectorAll('[role="radio"]').length || 0;
  }, []);

  useEffect(() => {
    if (groupRef.current) {
      const count = getRadioCount();
      radioRefs.current = Array(count)
        .fill(null)
        .map((_, i) => radioRefs.current[i] || createRef<HTMLLabelElement>());
    }
  }, [getRadioCount]);

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

      let newIndex = focusedIndex;
      const startIndex = selectedIndex !== null ? selectedIndex : 0;

      switch (e.key) {
        case 'ArrowRight':
        case 'ArrowDown':
          e.preventDefault();
          if (focusedIndex === -1) {
            newIndex = (startIndex + 1) % count;
          } else {
            newIndex = (focusedIndex + 1) % count;
          }
          break;
        case 'ArrowLeft':
        case 'ArrowUp':
          e.preventDefault();
          if (focusedIndex === -1) {
            newIndex = (startIndex - 1 + count) % count;
          } else {
            newIndex = (focusedIndex - 1 + count) % count;
          }
          break;
        case ' ':
          e.preventDefault();
          if (focusedIndex !== -1) {
            const element = radioRefs.current[focusedIndex]?.current;
            if (element) {
              element.click();
            }
          }
          return;
      }

      if (newIndex !== focusedIndex) {
        setFocusedIndex(newIndex);
        const element = radioRefs.current[newIndex]?.current;
        if (element) {
          element.focus();
        }
      }
    },
    [focusedIndex, selectedIndex, getRadioCount]
  );

  const getRadioProps = useCallback(
    (index: number): RadioProps => {
      if (!radioRefs.current[index]) {
        radioRefs.current[index] = createRef<HTMLLabelElement>();
      }

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

  const getRadioGroupProps = useCallback((): RadioGroupProps => {
    return {
      onKeyDown,
      ref: groupRef,
    };
  }, [onKeyDown]);

  return {
    focusedIndex,
    getRadioGroupProps,
    getRadioProps,
  } as const;
} 

Comments

답글 남기기