Skip to content

actions: [() => []] doesn't call preventDefault despite guard match #2271

@christianhg

Description

@christianhg

Description

When a behavior's guard matches but all action functions return empty arrays ([]), the native browser event is not prevented — even though the behavior has claimed ownership of the event.

This creates a contradiction: defaultBehaviorOverwritten is set to true (the behavior "owns" the event and no other behaviors run), but nativeEventPrevented remains false (the browser's default action still fires).

Reproduction

// This prevents the browser default (correct):
defineBehavior({
  on: 'keyboard.keydown',
  guard: ({ event }) => event.originEvent.key === 'Tab',
  actions: [],
})

// This does NOT prevent the browser default (unexpected):
defineBehavior({
  on: 'keyboard.keydown',
  guard: ({ event }) => event.originEvent.key === 'Tab',
  actions: [() => []],
})

In the second case, the guard matches, defaultBehaviorOverwritten is set to true in behavior.perform-event.ts, and the break after the action loop prevents any other behavior from handling the event. But because the action function returned [], the continue in the action loop skips it without setting nativeEventPrevented = true.

The result: the behavior claimed the event (no other behavior runs), but the browser's default action (e.g., Tab changing focus) still fires.

Expected behavior

If a behavior's guard matches, the native event should be prevented regardless of what the action functions return. The behavior has claimed ownership — an empty action return should mean "I handled it by doing nothing" (noop), not "actually never mind, let the browser have it."

Suggested fix

After the action loop completes for a matched behavior, if defaultBehaviorOverwritten is true but nativeEventPrevented is still false, set nativeEventPrevented = true. This ensures that a matched guard with empty action returns still suppresses the browser default, while preserving the existing forward semantics (which explicitly sets nativeEventPrevented = false when it wants the native event to proceed).

Impact

This is a subtle API footgun. actions: [] and actions: [() => []] look nearly identical but have opposite effects on preventDefault. Developers writing conditional action logic (where an action function might return [] in some cases) will hit unexpected browser behavior.

Real-world example: a Shift+Tab behavior at indent level 0 that wants to do nothing. If written as actions: [({snapshot}) => { if (level === 0) return []; ... }], the browser's default Tab behavior fires at level 0 even though the guard matched.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions