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.
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:
defaultBehaviorOverwrittenis set totrue(the behavior "owns" the event and no other behaviors run), butnativeEventPreventedremainsfalse(the browser's default action still fires).Reproduction
In the second case, the guard matches,
defaultBehaviorOverwrittenis set totrueinbehavior.perform-event.ts, and thebreakafter the action loop prevents any other behavior from handling the event. But because the action function returned[], thecontinuein the action loop skips it without settingnativeEventPrevented = 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
defaultBehaviorOverwrittenistruebutnativeEventPreventedis stillfalse, setnativeEventPrevented = true. This ensures that a matched guard with empty action returns still suppresses the browser default, while preserving the existingforwardsemantics (which explicitly setsnativeEventPrevented = falsewhen it wants the native event to proceed).Impact
This is a subtle API footgun.
actions: []andactions: [() => []]look nearly identical but have opposite effects onpreventDefault. 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.