99cc4c79f2
Enter/Space on a clickableProps row unconditionally called e.preventDefault() + onActivate(), even when the keydown bubbled up from a nested interactive child (e.g. the Delete/Rename icon buttons in AssetsPanel/PagesPanel). preventDefault() anywhere in the propagation path cancels the browser's native click synthesis for the focused child button, so its onClick never fired and the row's onActivate hijacked the action instead. Guard on e.target !== e.currentTarget so only keydowns targeted at the row itself are handled.
37 lines
1.2 KiB
TypeScript
37 lines
1.2 KiB
TypeScript
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) => {
|
|
// 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;
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
onActivate();
|
|
}
|
|
},
|
|
};
|
|
}
|