fix(builder): clickableProps must ignore keydown bubbled from nested controls
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.
This commit is contained in:
@@ -48,4 +48,35 @@ describe('clickableProps', () => {
|
||||
expect(onActivate).not.toHaveBeenCalled();
|
||||
expect(e.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('onKeyDown activates when the event target is the row itself', () => {
|
||||
const onActivate = vi.fn();
|
||||
const props = clickableProps(onActivate);
|
||||
const rowEl = {};
|
||||
const e = {
|
||||
key: 'Enter',
|
||||
target: rowEl,
|
||||
currentTarget: rowEl,
|
||||
preventDefault: vi.fn(),
|
||||
} as unknown as Parameters<ReturnType<typeof clickableProps>['onKeyDown']>[0];
|
||||
props.onKeyDown(e);
|
||||
expect(onActivate).toHaveBeenCalledTimes(1);
|
||||
expect(e.preventDefault).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('onKeyDown ignores a bubbled event from a nested control (e.g. a child button)', () => {
|
||||
const onActivate = vi.fn();
|
||||
const props = clickableProps(onActivate);
|
||||
const rowEl = {};
|
||||
const childButtonEl = {};
|
||||
const e = {
|
||||
key: 'Enter',
|
||||
target: childButtonEl,
|
||||
currentTarget: rowEl,
|
||||
preventDefault: vi.fn(),
|
||||
} as unknown as Parameters<ReturnType<typeof clickableProps>['onKeyDown']>[0];
|
||||
props.onKeyDown(e);
|
||||
expect(onActivate).not.toHaveBeenCalled();
|
||||
expect(e.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,12 @@ export function clickableProps(onActivate: () => void): ClickableProps {
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user