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()
};
}
답글 남기기
댓글을 달기 위해서는 로그인해야합니다.