From 99cc4c79f28279686b2500e3a461da7e3c92fbba Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 12 Jul 2026 14:58:13 -0700 Subject: [PATCH] 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. --- craft/src/utils/a11y.test.ts | 31 +++++++++++++++++++++++++++++++ craft/src/utils/a11y.ts | 6 ++++++ 2 files changed, 37 insertions(+) diff --git a/craft/src/utils/a11y.test.ts b/craft/src/utils/a11y.test.ts index a3cbba7..1643b40 100644 --- a/craft/src/utils/a11y.test.ts +++ b/craft/src/utils/a11y.test.ts @@ -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['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['onKeyDown']>[0]; + props.onKeyDown(e); + expect(onActivate).not.toHaveBeenCalled(); + expect(e.preventDefault).not.toHaveBeenCalled(); + }); }); diff --git a/craft/src/utils/a11y.ts b/craft/src/utils/a11y.ts index b71325e..5e72c6b 100644 --- a/craft/src/utils/a11y.ts +++ b/craft/src/utils/a11y.ts @@ -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