useKeyboardControl

import { KeyboardEvent, useCallback } from 'react';

type ControlRole = 'switch' | 'button' | 'toggleButton' | 'checkbox' | 'link';
type ActionKey = 'Enter' | 'Space';

interface UseKeyboardControlProps {
  /** 컴포넌트의 역할을 지정합니다. */
  role: ControlRole;
  /** 키보드 액션 발생 시 실행될 콜백 함수 */
  onKeyAction: (key: ActionKey) => void;
  /** 컴포넌트의 활성화 여부 (기본값: true) */
  isEnabled?: boolean;
  /** 현재 선택 상태 (switch, checkbox, toggleButton에만 적용) */
  isSelected?: boolean;
  /** 사용자 정의 tabIndex (기본값: 0) */
  customTabIndex?: number;
  /** ARIA 속성을 자동으로 추가할지 여부 (기본값: true) */
  includeARIA?: boolean;
}

interface KeyboardControlProps {
  /** 키보드 이벤트 핸들러 */
  onKeyDown: (e: KeyboardEvent) => void;
  /** 탭 인덱스 */
  tabIndex: number;
  /** 컴포넌트의 역할 (includeARIA가 true인 경우에만 포함) */
  role?: 'button' | 'switch' | 'checkbox' | 'link';
  /** ARIA 속성들 (includeARIA가 true인 경우에만 포함) */
  'aria-disabled'?: boolean;
  'aria-checked'?: boolean;
  'aria-pressed'?: boolean;
}

/**
 * 키보드 접근성을 지원하는 컨트롤 요소를 위한 커스텀 훅
 * 
 * @example
 * // 키보드 접근성만 사용하는 경우
 * const keyboardProps = useKeyboardControl({
 *   role: 'button',
 *   onKeyAction: (key) => {
 *     if (key === 'Enter') handleEnterKey();
 *     if (key === 'Space') handleSpaceKey();
 *   },
 *   includeARIA: false
 * });
 * 
 * // 링크로 사용 (Enter 키만 처리)
 * const linkProps = useKeyboardControl({
 *   role: 'link',
 *   onKeyAction: (key) => {
 *     if (key === 'Enter') navigate('/path');
 *   }
 * });
 * 
 * // 스위치로 사용 (Space 키만 처리)
 * const switchProps = useKeyboardControl({
 *   role: 'switch',
 *   onKeyAction: (key) => {
 *     if (key === 'Space') setIsOn(prev => !prev);
 *   },
 *   isSelected: isOn
 * });
 */
export function useKeyboardControl({
  role,
  onKeyAction,
  isEnabled = true,
  isSelected,
  customTabIndex,
  includeARIA = true
}: UseKeyboardControlProps): KeyboardControlProps {
  const handleKeyDown = useCallback((e: KeyboardEvent) => {
    if (!isEnabled) return;

    // 각 역할에 따른 키 처리
    switch (role) {
      case 'link':
        // 링크는 Enter 키만 지원
        if (e.key === 'Enter') {
          e.preventDefault();
          onKeyAction('Enter');
        }
        break;
      case 'button':
      case 'toggleButton':
        // 버튼은 Enter와 Space 모두 지원
        if (e.key === 'Enter') {
          e.preventDefault();
          onKeyAction('Enter');
        } else if (e.key === ' ') {
          e.preventDefault();
          onKeyAction('Space');
        }
        break;
      default:
        // 나머지(switch, checkbox)는 Space만 지원
        if (e.key === ' ') {
          e.preventDefault();
          onKeyAction('Space');
        }
    }
  }, [isEnabled, onKeyAction, role]);

  // role에 따른 ARIA 속성 설정
  const getAriaAttributes = useCallback(() => {
    if (!includeARIA) return {};

    const baseAttributes = {
      'aria-disabled': !isEnabled
    };

    switch (role) {
      case 'switch':
      case 'checkbox':
        return {
          ...baseAttributes,
          'aria-checked': isSelected
        };
      case 'toggleButton':
        return {
          ...baseAttributes,
          'aria-pressed': isSelected
        };
      case 'button':
      case 'link':
        return baseAttributes;
      default:
        return baseAttributes;
    }
  }, [role, isEnabled, isSelected, includeARIA]);

  // role 매핑 (toggleButton은 실제 DOM에서는 button role을 사용)
  const getDOMRole = useCallback((): 'button' | 'switch' | 'checkbox' | 'link' | undefined => {
    if (!includeARIA) return undefined;
    return role === 'toggleButton' ? 'button' : role;
  }, [role, includeARIA]);

  return {
    ...(includeARIA && { role: getDOMRole() }),
    onKeyDown: handleKeyDown,
    tabIndex: isEnabled ? (customTabIndex ?? 0) : -1,
    ...getAriaAttributes()
  };
} 

Comments

답글 남기기