diff --git a/packages/pluggableWidgets/gallery-web/CHANGELOG.md b/packages/pluggableWidgets/gallery-web/CHANGELOG.md index cd4a3f354c..64105ae6fe 100644 --- a/packages/pluggableWidgets/gallery-web/CHANGELOG.md +++ b/packages/pluggableWidgets/gallery-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- We fixed an issue where SPACE and ENTER keyboard events were blocked in interactive elements (inputs, textareas, buttons) nested within gallery items. These keys now work normally in nested inputs while still triggering gallery item selection when pressed on the item itself. + ## [3.11.5] - 2026-09-10 ### Changed diff --git a/packages/pluggableWidgets/gallery-web/openspec/changes/archive/2026-09-17-fix-keyboard-events-in-nested-inputs/.openspec.yaml b/packages/pluggableWidgets/gallery-web/openspec/changes/archive/2026-09-17-fix-keyboard-events-in-nested-inputs/.openspec.yaml new file mode 100644 index 0000000000..d28e909f6b --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/openspec/changes/archive/2026-09-17-fix-keyboard-events-in-nested-inputs/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-17 diff --git a/packages/pluggableWidgets/gallery-web/openspec/changes/archive/2026-09-17-fix-keyboard-events-in-nested-inputs/design.md b/packages/pluggableWidgets/gallery-web/openspec/changes/archive/2026-09-17-fix-keyboard-events-in-nested-inputs/design.md new file mode 100644 index 0000000000..e470a65aba --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/openspec/changes/archive/2026-09-17-fix-keyboard-events-in-nested-inputs/design.md @@ -0,0 +1,66 @@ +## Context + +Gallery items can contain nested interactive elements (inputs, textareas, buttons, etc.). The gallery widget implements keyboard activation for gallery items using SPACE and ENTER keys to trigger item selection/action. However, keyboard event handlers were unconditionally calling `preventDefault()` and `stopPropagation()` for all SPACE and ENTER events within a gallery item, regardless of whether the event originated from the gallery item itself or from a nested input element. This broke keyboard functionality for nested inputs — users cannot type SPACE, submit forms with ENTER, or interact with nested interactive elements. + +The fix requires distinguishing between events that originated on the gallery item (`event.target === event.currentTarget`) and events from nested elements (bubbled events). + +## Goals / Non-Goals + +**Goals:** + +- Enable keyboard functionality (SPACE, ENTER) for nested interactive elements within gallery items +- Preserve gallery item keyboard activation (SPACE/ENTER on the gallery item itself) +- Maintain backward compatibility with existing gallery item selection behavior +- No API or prop changes required + +**Non-Goals:** + +- Modifying the overall gallery keyboard interaction model +- Changing selection modes or multi-select behavior +- Updating documentation or public APIs + +## Decisions + +**Decision 1: Event ownership check using `event.target === event.currentTarget`** + +Add an `isOwn` check to both keyboard event handlers to verify the event originated on the gallery item itself, not from a nested element. + +- **Rationale**: The standard React/DOM pattern for distinguishing self events from bubbled child events is comparing `target` (original event source) with `currentTarget` (handler's element). This is performant and reliable. +- **Alternative considered**: Using event phases (capture vs. bubble) — more complex and less idiomatic in React event handlers. + +**Decision 2: Apply `isOwn` check in two separate locations** + +- **ListItemButton.tsx**: Check ownership before calling `preventAndStop()` in the `onKeyDown` handler +- **action-handlers.ts**: Check ownership in the `canExecOnSpaceOrEnter` filter + +- **Rationale**: Both handlers independently decide whether to prevent/stop or trigger actions. Adding the check in both ensures consistent behavior regardless of which handler path is taken. The filter approach in action-handlers is cleaner; ListItemButton needs the check in the handler itself due to state management (`pressed` flag). +- **Alternative considered**: Centralizing the check in one location — would require restructuring event handler wiring and risks one path bypassing validation. + +**Decision 3: No architectural changes to event routing** + +Keep the existing event switch pattern and handler composition. Only add the ownership check, don't refactor the event handling system. + +- **Rationale**: Minimal change reduces risk and review scope. The current architecture works; we're fixing behavior, not redesigning. + +## Risks / Trade-offs + +**[Risk]** Events that bubble from deeply nested elements may not have a clear owner. +→ **Mitigation**: The `event.target === event.currentTarget` check is strict and only matches events fired directly on the gallery item. Any bubbled event will fail the check, which is correct behavior — nested elements should handle their own events. + +**[Risk]** Developers unfamiliar with this pattern might add similar handlers without the ownership check. +→ **Mitigation**: Add a code comment in both locations explaining the ownership check purpose. + +**[Trade-off]** The ownership check requires `event.currentTarget` to be properly set. In React controlled event handlers it always is, but if event listeners are added directly to the DOM they must use `addEventListener(..., false)` (bubble phase, default). +→ **Justification**: We're using React event handlers exclusively; this is not a concern in practice. + +## Implementation Notes + +Files to modify: + +1. `src/components/ListItemButton.tsx` (line 23-29): Move `preventAndStop()` inside the `isOwn()` check +2. `src/features/item-interaction/action-handlers.ts` (line 25-31): Add `isOwn` check to `canExecOnSpaceOrEnter` + +Testing approach: + +- Unit tests: Verify ownership checks block events from nested elements +- E2E tests: Confirm SPACE/ENTER work in nested inputs and still trigger gallery item actions when pressed on the item itself diff --git a/packages/pluggableWidgets/gallery-web/openspec/changes/archive/2026-09-17-fix-keyboard-events-in-nested-inputs/proposal.md b/packages/pluggableWidgets/gallery-web/openspec/changes/archive/2026-09-17-fix-keyboard-events-in-nested-inputs/proposal.md new file mode 100644 index 0000000000..aa1614e7dc --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/openspec/changes/archive/2026-09-17-fix-keyboard-events-in-nested-inputs/proposal.md @@ -0,0 +1,24 @@ +## Why + +Gallery items can contain nested interactive elements like input fields. Currently, SPACE and ENTER keyboard events are being unconditionally prevented and blocked from propagating whenever pressed on any element within a gallery item. This breaks keyboard functionality for nested inputs — users cannot use SPACE in text inputs or ENTER to submit forms nested within gallery items. + +## What Changes + +- **ListItemButton.tsx**: Move the `isOwn` ownership check into the same conditional as `isTriggerKey`, so `preventDefault()` and `stopPropagation()` only execute for keyboard events that originate directly on the gallery item, not on nested elements. +- **action-handlers.ts**: Add an `isOwn` ownership check to the `canExecOnSpaceOrEnter` filter, ensuring gallery item activation actions only trigger when SPACE/ENTER is pressed directly on the gallery item, not on nested interactive elements. + +## Capabilities + +### New Capabilities + +- `keyboard-event-ownership`: Ensure keyboard event handlers respect event ownership, blocking propagation and preventing defaults only for events that originate on the gallery item itself, not on nested interactive elements. + +### Modified Capabilities + + + +## Impact + +- **Affected files**: `src/components/ListItemButton.tsx`, `src/features/item-interaction/action-handlers.ts` +- **Behavior**: Nested input elements within gallery items now respond normally to SPACE and ENTER keyboard events +- **Scope**: Gallery-web widget only; no breaking changes to public API or data contracts diff --git a/packages/pluggableWidgets/gallery-web/openspec/changes/archive/2026-09-17-fix-keyboard-events-in-nested-inputs/specs/keyboard-event-ownership/spec.md b/packages/pluggableWidgets/gallery-web/openspec/changes/archive/2026-09-17-fix-keyboard-events-in-nested-inputs/specs/keyboard-event-ownership/spec.md new file mode 100644 index 0000000000..fdabe68889 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/openspec/changes/archive/2026-09-17-fix-keyboard-events-in-nested-inputs/specs/keyboard-event-ownership/spec.md @@ -0,0 +1,41 @@ +## ADDED Requirements + +### Requirement: Keyboard events on gallery items respect event ownership + +The gallery widget SHALL only prevent default behavior and stop propagation of SPACE and ENTER keyboard events when they originate directly from the gallery item itself, not from nested interactive elements. Nested elements SHALL be able to respond to keyboard input normally. + +#### Scenario: SPACE key on gallery item triggers selection + +- **WHEN** user presses SPACE on a gallery item (not a nested element) +- **THEN** the system prevents the browser's default SPACE behavior (page scroll) and triggers the gallery item's action + +#### Scenario: SPACE key in nested input allows typing + +- **WHEN** user presses SPACE inside a text input nested within a gallery item +- **THEN** the input receives the SPACE character and default browser behavior is NOT prevented + +#### Scenario: ENTER key on gallery item triggers activation + +- **WHEN** user presses ENTER on a gallery item (not a nested element) +- **THEN** the system triggers the gallery item's click action and prevents default browser behavior + +#### Scenario: ENTER key in nested form input submits form + +- **WHEN** user presses ENTER inside a form input nested within a gallery item +- **THEN** the input or form can respond to ENTER normally (e.g., form submission, autocomplete) and the gallery item action is NOT triggered + +### Requirement: Event ownership determined by event target and current target + +The system SHALL determine event ownership by comparing the event's target element with the element that has the event handler attached (`event.target === event.currentTarget`). Only when these match SHALL the event be considered "owned" by the gallery item. + +#### Scenario: Event originated on gallery item + +- **WHEN** a keyboard event fires on the gallery item element itself +- **THEN** `event.target === event.currentTarget` evaluates to true +- **THEN** the event is treated as owned and default behavior is prevented + +#### Scenario: Event bubbled from nested element + +- **WHEN** a keyboard event fires on a nested element and bubbles to the gallery item +- **THEN** `event.target !== event.currentTarget` (target is the nested element, currentTarget is the gallery item) +- **THEN** the event is NOT treated as owned and default behavior is NOT prevented by the gallery handler diff --git a/packages/pluggableWidgets/gallery-web/openspec/changes/archive/2026-09-17-fix-keyboard-events-in-nested-inputs/tasks.md b/packages/pluggableWidgets/gallery-web/openspec/changes/archive/2026-09-17-fix-keyboard-events-in-nested-inputs/tasks.md new file mode 100644 index 0000000000..b09aa14888 --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/openspec/changes/archive/2026-09-17-fix-keyboard-events-in-nested-inputs/tasks.md @@ -0,0 +1,15 @@ +## 1. Fix Gallery Item Keyboard Event Ownership + +- [x] 1.1 Update `ListItemButton.tsx`: Move `isOwn` check into same conditional as `isTriggerKey` for `onKeyDown` handler +- [x] 1.2 Update `action-handlers.ts`: Add `isOwn` check to `canExecOnSpaceOrEnter` filter for SPACE and ENTER events +- [x] 1.3 Update `isSelectOneTrigger` in widget-plugin-grid: Add `isOwn` check for SPACE+Shift selection events + +## 2. Update Arrow Key Navigation Handlers + +- [x] 2.1 Update `onSelectGridAdjacentHotKey` arrow key handlers: Add `isOwn` check to all arrow key filters (ArrowUp, ArrowDown, ArrowLeft, ArrowRight) +- [x] 2.2 Update scroll key handler in `onSelectGridAdjacentHotKey`: Add `isOwn` check to PageUp, PageDown, Home, End filters + +## 3. Verification and Documentation + +- [x] 3.1 Create changelog entry documenting the fix +- [x] 3.2 Review all modified handlers for consistency in ownership checking pattern diff --git a/packages/pluggableWidgets/gallery-web/openspec/specs/keyboard-event-ownership/spec.md b/packages/pluggableWidgets/gallery-web/openspec/specs/keyboard-event-ownership/spec.md new file mode 100644 index 0000000000..042b9dcddb --- /dev/null +++ b/packages/pluggableWidgets/gallery-web/openspec/specs/keyboard-event-ownership/spec.md @@ -0,0 +1,47 @@ +# Keyboard Event Ownership + +## Purpose + +Ensure that keyboard events originating from nested interactive elements (inputs, textareas, buttons) within gallery items are handled correctly by those elements rather than being intercepted by gallery-level keyboard handlers. + +## Requirements + +### Requirement: Keyboard events on gallery items respect event ownership + +The gallery widget SHALL only prevent default behavior and stop propagation of SPACE and ENTER keyboard events when they originate directly from the gallery item itself, not from nested interactive elements. Nested elements SHALL be able to respond to keyboard input normally. + +#### Scenario: SPACE key on gallery item triggers selection + +- **WHEN** user presses SPACE on a gallery item (not a nested element) +- **THEN** the system prevents the browser's default SPACE behavior (page scroll) and triggers the gallery item's action + +#### Scenario: SPACE key in nested input allows typing + +- **WHEN** user presses SPACE inside a text input nested within a gallery item +- **THEN** the input receives the SPACE character and default browser behavior is NOT prevented + +#### Scenario: ENTER key on gallery item triggers activation + +- **WHEN** user presses ENTER on a gallery item (not a nested element) +- **THEN** the system triggers the gallery item's click action and prevents default browser behavior + +#### Scenario: ENTER key in nested form input submits form + +- **WHEN** user presses ENTER inside a form input nested within a gallery item +- **THEN** the input or form can respond to ENTER normally (e.g., form submission, autocomplete) and the gallery item action is NOT triggered + +### Requirement: Event ownership determined by event target and current target + +The system SHALL determine event ownership by comparing the event's target element with the element that has the event handler attached (`event.target === event.currentTarget`). Only when these match SHALL the event be considered "owned" by the gallery item. + +#### Scenario: Event originated on gallery item + +- **WHEN** a keyboard event fires on the gallery item element itself +- **THEN** `event.target === event.currentTarget` evaluates to true +- **THEN** the event is treated as owned and default behavior is prevented + +#### Scenario: Event bubbled from nested element + +- **WHEN** a keyboard event fires on a nested element and bubbles to the gallery item +- **THEN** `event.target !== event.currentTarget` (target is the nested element, currentTarget is the gallery item) +- **THEN** the event is NOT treated as owned and default behavior is NOT prevented by the gallery handler diff --git a/packages/pluggableWidgets/gallery-web/src/components/ListItemButton.tsx b/packages/pluggableWidgets/gallery-web/src/components/ListItemButton.tsx index a1e2bdb84c..bc3f8d468e 100644 --- a/packages/pluggableWidgets/gallery-web/src/components/ListItemButton.tsx +++ b/packages/pluggableWidgets/gallery-web/src/components/ListItemButton.tsx @@ -21,11 +21,9 @@ function keyboardHandlers(): KeyboardHandlers { let pressed = false; return { onKeyDown: event => { - if (isTriggerKey(event)) { + if (isTriggerKey(event) && isOwn(event)) { preventAndStop(event); - if (isOwn(event)) { - pressed = true; - } + pressed = true; } }, onKeyUp: event => { diff --git a/packages/pluggableWidgets/gallery-web/src/features/item-interaction/action-handlers.ts b/packages/pluggableWidgets/gallery-web/src/features/item-interaction/action-handlers.ts index 5f3305a41d..efae532373 100644 --- a/packages/pluggableWidgets/gallery-web/src/features/item-interaction/action-handlers.ts +++ b/packages/pluggableWidgets/gallery-web/src/features/item-interaction/action-handlers.ts @@ -23,11 +23,12 @@ const onDoubleClick = ( }); const canExecOnSpaceOrEnter = (_ctx: EventEntryContext, event: KeyboardEvent): boolean => { + const isOwn = event.currentTarget === event.target; if (event.code === "Space") { - return !event.shiftKey; + return !event.shiftKey && isOwn; } - return event.code === "Enter"; + return event.code === "Enter" && isOwn; }; const onSpaceOrEnter = ( diff --git a/packages/shared/widget-plugin-grid/src/selection/keyboard.ts b/packages/shared/widget-plugin-grid/src/selection/keyboard.ts index 4f3ce40a98..6161b31d2f 100644 --- a/packages/shared/widget-plugin-grid/src/selection/keyboard.ts +++ b/packages/shared/widget-plugin-grid/src/selection/keyboard.ts @@ -11,7 +11,8 @@ export function isSelectAllTrigger(event: KeyboardEvent): boolean { } export function isSelectOneTrigger(event: KeyboardEvent): boolean { - return event.code === "Space" && event.shiftKey; + const isOwn = event.currentTarget === event.target; + return event.code === "Space" && event.shiftKey && isOwn; } export function isOwnSpaceKey(event: KeyboardEvent): boolean { @@ -157,7 +158,8 @@ export const onSelectGridAdjacentHotKey = ( ): NavKeyEntry[] => { const onArrowUp: NavKeyEntry = { eventName: "onKeyDown", - filter: (ctx, event) => event.code === "ArrowUp" && ctx.selectionType === "Multi", + filter: (ctx, event) => + event.code === "ArrowUp" && ctx.selectionType === "Multi" && event.currentTarget === event.target, handler: (ctx, event) => selectAdjacentFx(ctx.item, event.shiftKey, getAdjacentFxMode(ctx, event), { direction: "backward", @@ -167,7 +169,8 @@ export const onSelectGridAdjacentHotKey = ( const onArrowDown: NavKeyEntry = { eventName: "onKeyDown", - filter: (ctx, event) => event.code === "ArrowDown" && ctx.selectionType === "Multi", + filter: (ctx, event) => + event.code === "ArrowDown" && ctx.selectionType === "Multi" && event.currentTarget === event.target, handler: (ctx, event) => selectAdjacentFx(ctx.item, event.shiftKey, getAdjacentFxMode(ctx, event), { direction: "forward", @@ -177,7 +180,8 @@ export const onSelectGridAdjacentHotKey = ( const onArrowLeft: NavKeyEntry = { eventName: "onKeyDown", - filter: (ctx, event) => event.code === "ArrowLeft" && ctx.selectionType === "Multi", + filter: (ctx, event) => + event.code === "ArrowLeft" && ctx.selectionType === "Multi" && event.currentTarget === event.target, handler: (ctx, event) => selectAdjacentFx(ctx.item, event.shiftKey, getAdjacentFxMode(ctx, event), { direction: "backward", @@ -187,7 +191,8 @@ export const onSelectGridAdjacentHotKey = ( const onArrowRight: NavKeyEntry = { eventName: "onKeyDown", - filter: (ctx, event) => event.code === "ArrowRight" && ctx.selectionType === "Multi", + filter: (ctx, event) => + event.code === "ArrowRight" && ctx.selectionType === "Multi" && event.currentTarget === event.target, handler: (ctx, event) => selectAdjacentFx(ctx.item, event.shiftKey, getAdjacentFxMode(ctx, event), { direction: "forward", @@ -198,7 +203,8 @@ export const onSelectGridAdjacentHotKey = ( const scrollKeys = new Set(["PageUp", "PageDown", "Home", "End"]); const onScrollKey: NavKeyEntry = { eventName: "onKeyDown", - filter: (ctx, event) => scrollKeys.has(event.code) && ctx.selectionType === "Multi", + filter: (ctx, event) => + scrollKeys.has(event.code) && ctx.selectionType === "Multi" && event.currentTarget === event.target, handler: (ctx, event) => selectAdjacentFx(ctx.item, event.shiftKey, getAdjacentFxMode(ctx, event), { code: event.code as ScrollKeyCode,