From cb989b6ef8a603f423ad685ab897a4f3c5e2bd34 Mon Sep 17 00:00:00 2001 From: Jake Archibald Date: Fri, 9 Aug 2024 19:29:16 +0100 Subject: [PATCH] Fixes to event dispatching (#403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make immediatePropogationStopped private-ish * Use workspace polyfill * Stopping immediate propagation also stops regular propagation * Assorted dispatching fixes - Respect stopPropagation throughout both capturing and bubbling - Call listeners on the target element in both the capturing and bubbling phases - Simplify returning defaultPrevented * Add changeset * Better param name * Fix lockfile * Trying again… --- .changeset/strong-sheep-sing.md | 9 ++++++ packages/core/package.json | 2 +- packages/polyfill/source/Event.ts | 40 ++++++++++++------------- packages/polyfill/source/EventTarget.ts | 27 ++++++++--------- packages/polyfill/source/constants.ts | 1 + 5 files changed, 42 insertions(+), 37 deletions(-) create mode 100644 .changeset/strong-sheep-sing.md diff --git a/.changeset/strong-sheep-sing.md b/.changeset/strong-sheep-sing.md new file mode 100644 index 00000000..729fd015 --- /dev/null +++ b/.changeset/strong-sheep-sing.md @@ -0,0 +1,9 @@ +--- +'@remote-dom/polyfill': patch +--- + +Bug fixes to event dispatching + +- Listeners on the target are now called during both the capture and bubble phases. +- `stopPropagation` now respected. +- `stopImmediatePropagation` now also stops regular propagation. diff --git a/packages/core/package.json b/packages/core/package.json index 869720cd..cfb1dc6e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -76,7 +76,7 @@ "build": "rollup --config ./rollup.config.js" }, "dependencies": { - "@remote-dom/polyfill": "^1.0.3", + "@remote-dom/polyfill": "workspace:^1.0.3", "htm": "^3.1.1" }, "peerDependencies": { diff --git a/packages/polyfill/source/Event.ts b/packages/polyfill/source/Event.ts index 5bbf8d15..0b2390d8 100644 --- a/packages/polyfill/source/Event.ts +++ b/packages/polyfill/source/Event.ts @@ -1,4 +1,9 @@ -import {PATH, IS_TRUSTED, LISTENERS} from './constants.ts'; +import { + PATH, + IS_TRUSTED, + LISTENERS, + STOP_IMMEDIATE_PROPAGATION, +} from './constants.ts'; import type {EventTarget} from './EventTarget.ts'; export const enum EventPhase { @@ -41,12 +46,12 @@ export class Event { composed = false; defaultPrevented = false; cancelBubble = false; - immediatePropagationStopped = false; eventPhase: EventPhase = 0; // private inPassiveListener = false; data?: any; [PATH]: EventTarget[] = []; [IS_TRUSTED]!: boolean; + [STOP_IMMEDIATE_PROPAGATION] = false; constructor( public type: string, @@ -73,7 +78,8 @@ export class Event { } stopImmediatePropagation() { - this.immediatePropagationStopped = true; + this[STOP_IMMEDIATE_PROPAGATION] = true; + this.cancelBubble = true; } preventDefault() { @@ -98,43 +104,35 @@ export class Event { export function fireEvent( event: Event, - target: EventTarget, - phase: EventPhase, -) { - const listeners = target[LISTENERS]; + currentTarget: EventTarget, + phase: EventPhase.BUBBLING_PHASE | EventPhase.CAPTURING_PHASE, +): void { + const listeners = currentTarget[LISTENERS]; const list = listeners?.get( `${event.type}${ phase === EventPhase.CAPTURING_PHASE ? CAPTURE_MARKER : '' }`, ); - if (!list) return false; - let defaultPrevented = false; + if (!list) return; for (const listener of list) { - event.eventPhase = phase; - event.currentTarget = target; + event.eventPhase = + event.target === currentTarget ? EventPhase.AT_TARGET : phase; + event.currentTarget = currentTarget; try { if (typeof listener === 'object') { listener.handleEvent(event as any); } else { - listener.call(target, event as any); + listener.call(currentTarget, event as any); } } catch (err) { setTimeout(thrower, 0, err); } - if (event.defaultPrevented === true) { - defaultPrevented = true; - } - - if (event.immediatePropagationStopped) { - break; - } + if (event[STOP_IMMEDIATE_PROPAGATION]) break; } - - return defaultPrevented; } function thrower(error: any) { diff --git a/packages/polyfill/source/EventTarget.ts b/packages/polyfill/source/EventTarget.ts index b4295233..3aee1559 100644 --- a/packages/polyfill/source/EventTarget.ts +++ b/packages/polyfill/source/EventTarget.ts @@ -95,23 +95,20 @@ export class EventTarget { event.target = this; event.srcElement = this; event[PATH] = path; - let defaultPrevented = false; - for (let i = path.length; --i; ) { - if (fireEvent(event, path[i]!, EventPhase.CAPTURING_PHASE)) { - defaultPrevented = true; - } - } - if (fireEvent(event, this, EventPhase.AT_TARGET)) { - defaultPrevented = true; + + for (let i = path.length; i--; ) { + fireEvent(event, path[i]!, EventPhase.CAPTURING_PHASE); + if (event.cancelBubble) return event.defaultPrevented; } - if (event.bubbles) { - for (let i = 1; i < path.length; i++) { - if (fireEvent(event, path[i]!, EventPhase.BUBBLING_PHASE)) { - defaultPrevented = true; - } - } + + const bubblePath = event.bubbles ? path : path.slice(0, 1); + + for (let i = 0; i < bubblePath.length; i++) { + fireEvent(event, bubblePath[i]!, EventPhase.BUBBLING_PHASE); + if (event.cancelBubble) return event.defaultPrevented; } - return !defaultPrevented; + + return event.defaultPrevented; } } diff --git a/packages/polyfill/source/constants.ts b/packages/polyfill/source/constants.ts index 80504ba2..00e2b61d 100644 --- a/packages/polyfill/source/constants.ts +++ b/packages/polyfill/source/constants.ts @@ -13,6 +13,7 @@ export const USER_PROPERTIES = Symbol('user_properties'); export const LISTENERS = Symbol('listeners'); export const IS_TRUSTED = Symbol('isTrusted'); export const PATH = Symbol('path'); +export const STOP_IMMEDIATE_PROPAGATION = Symbol('stop_immediate_propagation'); export const CONTENT = Symbol('content'); export const SLOT = Symbol('slot');