2026-07-12 14:50:50 -07:00
|
|
|
import type { KeyboardEvent } from 'react';
|
|
|
|
|
|
|
|
|
|
export interface ClickableProps {
|
|
|
|
|
role: 'button';
|
|
|
|
|
tabIndex: number;
|
|
|
|
|
onClick: () => void;
|
|
|
|
|
onKeyDown: (e: KeyboardEvent) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Props to spread onto a non-interactive element (typically a `<div>`) that
|
|
|
|
|
* acts like a button (e.g. a clickable list row or grid tile), so it
|
|
|
|
|
* becomes keyboard-operable: `role="button"`, a tab stop, and Enter/Space
|
|
|
|
|
* activation in addition to the existing click behavior.
|
|
|
|
|
*
|
|
|
|
|
* Usage: `<div {...clickableProps(() => doThing())}>...</div>`
|
|
|
|
|
*/
|
|
|
|
|
export function clickableProps(onActivate: () => void): ClickableProps {
|
|
|
|
|
return {
|
|
|
|
|
role: 'button',
|
|
|
|
|
tabIndex: 0,
|
|
|
|
|
onClick: onActivate,
|
|
|
|
|
onKeyDown: (e: KeyboardEvent) => {
|
2026-07-12 14:58:13 -07:00
|
|
|
// Ignore keydown events that bubbled up from a nested interactive
|
|
|
|
|
// element (e.g. a child <button>). Handling only events targeted at
|
|
|
|
|
// this element lets the browser's native "click" synthesis for a
|
|
|
|
|
// focused child control proceed instead of being hijacked by the
|
|
|
|
|
// row's own Enter/Space activation.
|
|
|
|
|
if (e.target !== e.currentTarget) return;
|
2026-07-12 14:50:50 -07:00
|
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
onActivate();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|