PlusMagi's Blog By Pitt Phunsanit Technology Custom Hook: useKeyboardAction สำหรับ React / Next.js เพื่อแก้ A11y อัตโนมัติ

Custom Hook: useKeyboardAction สำหรับ React / Next.js เพื่อแก้ A11y อัตโนมัติ

เพื่อไม่ต้องคอยเขียน role="button", tabIndex={0} และดักจับปุ่ม Enter / Space ซ้ำ ๆ ในทุกคอมโพเนนต์ เราสามารถสร้าง Custom Hook ชื่อ useKeyboardAction เพื่อมัด Props ด้าน Accessibility และ Event Handler ให้พร้อม Spread ลงใน <div> หรือ <span> ได้ทันที


โค้ด Hook (TypeScript / JavaScript)

สร้างไฟล์ hooks/useKeyboardAction.ts (หรือ .js)

JavaScript

import { useCallback, type KeyboardEvent, type MouseEvent } from 'react';

interface UseKeyboardActionOptions {
  disabled?: boolean;
  stopPropagation?: boolean;
}

/**
 * Hook สำหรับสร้าง Props ที่ทำให้ Non-interactive elements (เช่น div, span)
 * รองรับการคลิกผ่านคีย์บอร์ด (Enter / Space) ตามมาตรฐาน WCAG
 */
export function useKeyboardAction<t extends="" htmlelement="HTMLDivElement">(
  action: (event: MouseEvent<t> | KeyboardEvent<t>) => void,
  options: UseKeyboardActionOptions = {}
) {
  const { disabled = false, stopPropagation = false } = options;

  const handleClick = useCallback(
    (event: MouseEvent<t>) => {
      if (disabled) return;
      if (stopPropagation) event.stopPropagation();
      action(event);
    },
    [action, disabled, stopPropagation]
  );

  const handleKeyDown = useCallback(
    (event: KeyboardEvent<t>) => {
      if (disabled) return;

      // รองรับปุ่ม Enter และ Spacebar
      if (event.key === 'Enter' || event.key === ' ') {
        // ป้องกันหน้าเว็บเลื่อนลง (Default scroll behavior) ตอนกด Spacebar
        event.preventDefault();
        
        if (stopPropagation) event.stopPropagation();
        action(event);
      }
    },
    [action, disabled, stopPropagation]
  );

  return {
    role: 'button' as const,
    tabIndex: disabled ? -1 : 0,
    'aria-disabled': disabled ? true : undefined,
    onClick: handleClick,
    onKeyDown: handleKeyDown,
  };
}</t></t></t></t></t>

ตัวอย่างการนำไปใช้งานจริงใน Component

เพียงแค่เรียก Hook แล้ว Spread Props ({...keyboardAction}) ใส่ <div> ตัว ESLint จะหยุดแจ้งเตือนทันที และโค้ดจะผ่านทั้ง click-events-have-key-events และ no-static-element-interactions

JavaScript

import React from 'react';
import { useKeyboardAction } from '@/hooks/useKeyboardAction';

export const InteractiveCard = () => {
  const handleSelectCard = () => {
    console.log('การ์ดถูกเลือกแล้ว!');
  };

  // 1. ผูกฟังก์ชันเข้ากับ Hook
  const cardActionProps = useKeyboardAction(handleSelectCard);

  return (
    // 2. Spread Props ลงใน div ได้เลย
    <div
      {...cardActionProps}
      className="p-6 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 
                 cursor-pointer transition shadow-sm hover:shadow-md
                 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2"
    >
      <h3 class="font-bold text-lg text-slate-900 dark:text-white">การ์ดบริการคลาวด์</h3>
      <p class="text-slate-600 dark:text-slate-300 text-sm mt-1">
        คลิกด้วยเมาส์ หรือกดปุ่ม Tab แล้วกด Enter/Space เพื่อเลือกรายการนี้
      </p>
    </div>
  );
};

รองรับสถานะ Disabled

หากการ์ดหรือคอมโพเนนต์อยู่ในสถานะปิดการใช้งาน (Disabled) สามารถส่ง flag เข้าไปได้ โดย Hook จะปรับ tabIndex เป็น -1 และใส่ aria-disabled="true" ให้อัตโนมัติ เพื่อไม่ให้คีย์บอร์ดเลื่อนมาติด

JavaScript

const disabledActionProps = useKeyboardAction(handleAction, { disabled: true });

return (
  <div 
    {...disabledActionProps} 
    className="opacity-50 cursor-not-allowed ..."
  >
    ปุ่มปิดการใช้งาน
  </div>
);

สิ่งที่ Hook นี้จัดการให้เบื้องหลัง

คุณสมบัติสิ่งที่ Hook จัดการให้ประโยชน์ด้าน Accessibility
role="button"ระบุบทบาทของ ElementScreen Reader จะอ่านออกเสียงว่า “ปุ่ม” แทนที่จะเป็นกลุ่มข้อความ
tabIndex={0}เปิดให้กด Tab เข้ามาได้Keyboard-only Users สามารถเลื่อนโฟกัสเข้ามายังคอมโพเนนต์ได้
event.key === ' 'ดักการกด Spacebar พร้อมสั่ง preventDefault()ป้องกันหน้าเว็บกระตุกเลื่อนลง (Scroll) เมื่อผู้ใช้กดเว้นวรรคเพื่อคลิก
aria-disabledระบุสถานะเมื่อปิดใช้งานช่วยให้ Screen Reader แจ้งเตือนผู้ใช้ว่าปุ่มนี้ยังไม่พร้อมให้กด