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