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:
2026-07-12 14:58:13 -07:00
parent 4b8dd8baee
commit 99cc4c79f2
2 changed files with 37 additions and 0 deletions
+31
View File
@@ -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();
});
});
+6
View File
@@ -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();