From 478c4310184b45d8a640a9f711bc6381cd104c13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 2 Jan 2025 02:22:22 -0800 Subject: [PATCH] Implement Event and EventTarget Summary: Changelog: [internal] This implements a (mostly) spec-compliant version of the [`Event`](https://dom.spec.whatwg.org/#interface-event) and [`EventTarget`](https://dom.spec.whatwg.org/#interface-eventtarget) Web interfaces. It does not implement legacy methods in either of the interfaces, and ignores the parts of the spec that are related to Web-specific quirks (shadow roots, re-mapping of animation events with webkit prefixes, etc.). IMPORTANT: This only creates the interfaces and does not expose them externally yet (no `Event` or `EventTarget` in the global scope). Differential Revision: D67738145 --- .../src/private/webapis/dom/events/Event.js | 177 ++++ .../private/webapis/dom/events/EventTarget.js | 386 ++++++++ .../dom/events/__tests__/Event-itest.js | 151 +++ .../dom/events/__tests__/EventTarget-itest.js | 930 ++++++++++++++++++ .../dom/events/internals/EventInternals.js | 120 +++ .../events/internals/EventTargetInternals.js | 52 + 6 files changed, 1816 insertions(+) create mode 100644 packages/react-native/src/private/webapis/dom/events/Event.js create mode 100644 packages/react-native/src/private/webapis/dom/events/EventTarget.js create mode 100644 packages/react-native/src/private/webapis/dom/events/__tests__/Event-itest.js create mode 100644 packages/react-native/src/private/webapis/dom/events/__tests__/EventTarget-itest.js create mode 100644 packages/react-native/src/private/webapis/dom/events/internals/EventInternals.js create mode 100644 packages/react-native/src/private/webapis/dom/events/internals/EventTargetInternals.js diff --git a/packages/react-native/src/private/webapis/dom/events/Event.js b/packages/react-native/src/private/webapis/dom/events/Event.js new file mode 100644 index 00000000000000..7985a1bbe7e24a --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/events/Event.js @@ -0,0 +1,177 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + */ + +/** + * This module implements the `Event` interface from the DOM. + * See https://dom.spec.whatwg.org/#interface-event. + */ + +// flowlint unsafe-getters-setters:off + +import type EventTarget from './EventTarget'; + +import { + COMPOSED_PATH_KEY, + CURRENT_TARGET_KEY, + EVENT_PHASE_KEY, + IN_PASSIVE_LISTENER_FLAG_KEY, + IS_TRUSTED_KEY, + STOP_IMMEDIATE_PROPAGATION_FLAG_KEY, + STOP_PROPAGATION_FLAG_KEY, + TARGET_KEY, + getComposedPath, + getCurrentTarget, + getEventPhase, + getInPassiveListenerFlag, + getIsTrusted, + getTarget, + setStopImmediatePropagationFlag, + setStopPropagationFlag, +} from './internals/EventInternals'; + +type EventInit = { + bubbles?: boolean, + cancelable?: boolean, + composed?: boolean, +}; + +export default class Event { + static NONE: 0 = 0; + static CAPTURING_PHASE: 1 = 1; + static AT_TARGET: 2 = 2; + static BUBBLING_PHASE: 3 = 3; + + #bubbles: boolean; + #cancelable: boolean; + #composed: boolean; + #type: string; + + #defaultPrevented: boolean = false; + #timeStamp: number = performance.now(); + + // $FlowExpectedError[unsupported-syntax] + [COMPOSED_PATH_KEY]: boolean = []; + + // $FlowExpectedError[unsupported-syntax] + [CURRENT_TARGET_KEY]: EventTarget | null = null; + + // $FlowExpectedError[unsupported-syntax] + [EVENT_PHASE_KEY]: boolean = Event.NONE; + + // $FlowExpectedError[unsupported-syntax] + [IN_PASSIVE_LISTENER_FLAG_KEY]: boolean = false; + + // $FlowExpectedError[unsupported-syntax] + [IS_TRUSTED_KEY]: boolean = false; + + // $FlowExpectedError[unsupported-syntax] + [STOP_IMMEDIATE_PROPAGATION_FLAG_KEY]: boolean = false; + + // $FlowExpectedError[unsupported-syntax] + [STOP_PROPAGATION_FLAG_KEY]: boolean = false; + + // $FlowExpectedError[unsupported-syntax] + [TARGET_KEY]: EventTarget | null = null; + + constructor(type: string, options?: ?EventInit) { + if (arguments.length < 1) { + throw new TypeError( + "Failed to construct 'Event': 1 argument required, but only 0 present.", + ); + } + + if (options != null && typeof options !== 'object') { + throw new TypeError( + "Failed to construct 'Event': The provided value is not of type 'EventInit'.", + ); + } + + this.#type = String(type); + this.#bubbles = Boolean(options?.bubbles); + this.#cancelable = Boolean(options?.cancelable); + this.#composed = Boolean(options?.composed); + } + + get bubbles(): boolean { + return this.#bubbles; + } + + get cancelable(): boolean { + return this.#cancelable; + } + + get composed(): boolean { + return this.#composed; + } + + get currentTarget(): EventTarget | null { + return getCurrentTarget(this); + } + + get defaultPrevented(): boolean { + return this.#defaultPrevented; + } + + get eventPhase(): EventPhase { + return getEventPhase(this); + } + + get isTrusted(): boolean { + return getIsTrusted(this); + } + + get target(): EventTarget | null { + return getTarget(this); + } + + get timeStamp(): number { + return this.#timeStamp; + } + + get type(): string { + return this.#type; + } + + composedPath(): $ReadOnlyArray { + return getComposedPath(this).slice(); + } + + preventDefault(): void { + if (!this.#cancelable) { + return; + } + + if (getInPassiveListenerFlag(this)) { + console.error( + new Error( + 'Unable to preventDefault inside passive event listener invocation.', + ), + ); + return; + } + + this.#defaultPrevented = true; + } + + stopImmediatePropagation(): void { + setStopPropagationFlag(this, true); + setStopImmediatePropagationFlag(this, true); + } + + stopPropagation(): void { + setStopPropagationFlag(this, true); + } +} + +export type EventPhase = + | (typeof Event)['NONE'] + | (typeof Event)['CAPTURING_PHASE'] + | (typeof Event)['AT_TARGET'] + | (typeof Event)['BUBBLING_PHASE']; diff --git a/packages/react-native/src/private/webapis/dom/events/EventTarget.js b/packages/react-native/src/private/webapis/dom/events/EventTarget.js new file mode 100644 index 00000000000000..8d4e8bc9bd3e67 --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/events/EventTarget.js @@ -0,0 +1,386 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + */ + +/** + * This module implements the `EventTarget` and related interfaces from the DOM. + * See https://dom.spec.whatwg.org/#interface-eventtarget. + */ + +import type {EventPhase} from './Event'; + +import Event from './Event'; +import { + getStopImmediatePropagationFlag, + getStopPropagationFlag, + setComposedPath, + setCurrentTarget, + setEventPhase, + setInPassiveListenerFlag, + setIsTrusted, + setStopImmediatePropagationFlag, + setStopPropagationFlag, + setTarget, +} from './internals/EventInternals'; +import { + EVENT_TARGET_GET_THE_PARENT_KEY, + INTERNAL_DISPATCH_METHOD_KEY, +} from './internals/EventTargetInternals'; + +export type EventListener = + | ((event: Event) => void) + | interface { + handleEvent(event: Event): void, + }; + +export type EventListenerOptions = { + capture?: boolean, +}; + +export type AddEventListenerOptions = { + ...EventListenerOptions, + passive?: boolean, + once?: boolean, + signal?: AbortSignal, +}; + +type EventListenerRegistration = { + +callback: EventListener, + +passive: boolean, + +once: boolean, + removed: boolean, +}; + +function getDefaultPassiveValue( + type: string, + eventTarget: EventTarget, +): boolean { + return false; +} + +export default class EventTarget { + #listeners: Map> = new Map(); + #captureListeners: Map> = new Map(); + + addEventListener( + type: string, + callback: EventListener | null, + optionsOrUseCapture?: AddEventListenerOptions | boolean = {}, + ): void { + if (arguments.length < 2) { + throw new TypeError( + `Failed to execute 'addEventListener' on 'EventTarget': 2 arguments required, but only ${arguments.length} present.`, + ); + } + + if (callback == null) { + return; + } + + validateCallback(callback, 'addEventListener'); + + const processedType = String(type); + + let capture; + let passive; + let once; + let signal; + + if ( + optionsOrUseCapture != null && + (typeof optionsOrUseCapture === 'object' || + typeof optionsOrUseCapture === 'function') + ) { + capture = optionsOrUseCapture.capture ?? false; + passive = + optionsOrUseCapture.passive ?? + getDefaultPassiveValue(processedType, this); + once = optionsOrUseCapture.once ?? false; + signal = optionsOrUseCapture.signal ?? null; + } else { + capture = Boolean(optionsOrUseCapture); + passive = false; + once = false; + signal = null; + } + + if (signal?.aborted) { + return; + } + + const listenerMap = capture ? this.#captureListeners : this.#listeners; + let listenerList = listenerMap.get(processedType); + if (listenerList == null) { + listenerList = []; + listenerMap.set(processedType, listenerList); + } else { + for (const listener of listenerList) { + if (listener.callback === callback) { + return; + } + } + } + + listenerList.push({ + callback, + passive, + once, + removed: false, + }); + + if (signal != null) { + signal.addEventListener('abort', () => { + this.removeEventListener(processedType, callback, capture); + }); + } + } + + removeEventListener( + type: string, + callback: EventListener, + optionsOrUseCapture?: EventListenerOptions | boolean = {}, + ): void { + if (arguments.length < 2) { + throw new TypeError( + `Failed to execute 'removeEventListener' on 'EventTarget': 2 arguments required, but only ${arguments.length} present.`, + ); + } + + if (callback == null) { + return; + } + + validateCallback(callback, 'removeEventListener'); + + const processedType = String(type); + + const capture = + typeof optionsOrUseCapture === 'boolean' + ? optionsOrUseCapture + : optionsOrUseCapture.capture ?? false; + + const listenerMap = capture ? this.#captureListeners : this.#listeners; + const listenerList = listenerMap.get(processedType); + if (listenerList == null) { + return; + } + + for (let i = 0; i < listenerList.length; i++) { + const listener = listenerList[i]; + + if (listener.callback === callback) { + listener.removed = true; + listenerList.splice(i, 1); + return; + } + } + + // NOTE: there is no step to remove the event listener from the signal, if + // set, as per the spec. See https://github.com/whatwg/dom/issues/1346. + } + + dispatchEvent(event: Event): boolean { + if (!(event instanceof Event)) { + throw new TypeError( + "Failed to execute 'dispatchEvent' on 'EventTarget': parameter 1 is not of type 'Event'.", + ); + } + + if (getEventDispatchFlag(event)) { + throw new Error( + "Failed to execute 'dispatchEvent' on 'EventTarget': The event is already being dispatched.", + ); + } + + setIsTrusted(event, false); + + this.#dispatch(event); + + return !event.defaultPrevented; + } + + /** + * This internal version of `dispatchEvent` does not validate the input and + * does not reset the `isTrusted` flag, so it can be used for both trusted + * and not trusted events. + * + * Implements the "event dispatch" concept + * (see https://dom.spec.whatwg.org/#concept-event-dispatch). + */ + #dispatch(event: Event): void { + setEventDispatchFlag(event, true); + + const eventPath = this.#getEventPath(event); + setComposedPath(event, eventPath); + setTarget(event, this); + + for (let i = eventPath.length - 1; i >= 0; i--) { + if (getStopPropagationFlag(event)) { + break; + } + + const target = eventPath[i]; + setEventPhase( + event, + target === this ? Event.AT_TARGET : Event.CAPTURING_PHASE, + ); + target.#invoke(event, Event.CAPTURING_PHASE); + } + + for (const target of eventPath) { + if (getStopPropagationFlag(event)) { + break; + } + + // If the event does NOT bubble, we only dispatch the event to the + // target in the bubbling phase. + if (!event.bubbles && target !== this) { + break; + } + + setEventPhase( + event, + target === this ? Event.AT_TARGET : Event.BUBBLING_PHASE, + ); + target.#invoke(event, Event.BUBBLING_PHASE); + } + + setEventPhase(event, Event.NONE); + setCurrentTarget(event, null); + setComposedPath(event, []); + + setEventDispatchFlag(event, false); + setStopImmediatePropagationFlag(event, false); + setStopPropagationFlag(event, false); + } + + /** + * Builds the event path for an event about to be dispatched in this target + * (see https://dom.spec.whatwg.org/#event-path). + * + * The return value is also set as `composedPath` for the event. + */ + #getEventPath(event: Event): $ReadOnlyArray { + const path = []; + // eslint-disable-next-line consistent-this + let target: EventTarget | null = this; + + while (target != null) { + path.push(target); + // $FlowExpectedError[prop-missing] + target = target[EVENT_TARGET_GET_THE_PARENT_KEY](); + } + + return path; + } + + /** + * Implements the event listener invoke concept + * (see https://dom.spec.whatwg.org/#concept-event-listener-invoke). + */ + #invoke(event: Event, eventPhase: EventPhase) { + const listenerMap = + eventPhase === Event.CAPTURING_PHASE + ? this.#captureListeners + : this.#listeners; + + setCurrentTarget(event, this); + + // This is a copy so listeners added during dispatch are NOT executed. + const listenerList = listenerMap.get(event.type)?.slice(); + if (listenerList == null) { + return; + } + + for (const listener of listenerList) { + if (listener.removed) { + continue; + } + + if (listener.once) { + this.removeEventListener( + event.type, + listener.callback, + eventPhase === Event.CAPTURING_PHASE, + ); + } + + if (listener.passive) { + setInPassiveListenerFlag(event, true); + } + + const currentEvent = global.event; + global.event = event; + + const callback = listener.callback; + + try { + if (typeof callback === 'function') { + callback.call(this, event); + // $FlowExpectedError[method-unbinding] + } else if (typeof callback.handleEvent === 'function') { + callback.handleEvent(event); + } + } catch (error) { + // TODO: replace with `reportError` when it's available. + console.error(error); + } + + if (listener.passive) { + setInPassiveListenerFlag(event, false); + } + + global.event = currentEvent; + + if (getStopImmediatePropagationFlag(event)) { + break; + } + } + } + + /** + * This a "protected" method to be overridden by a subclass to allow event + * propagation. + * + * Should implement the "get the parent" algorithm + * (see https://dom.spec.whatwg.org/#get-the-parent). + */ + // $FlowExpectedError[unsupported-syntax] + [EVENT_TARGET_GET_THE_PARENT_KEY](): EventTarget | null { + return null; + } + + /** + * This is "protected" method to dispatch trusted events. + */ + // $FlowExpectedError[unsupported-syntax] + [INTERNAL_DISPATCH_METHOD_KEY](event: Event): void { + this.#dispatch(event); + } +} + +function validateCallback(callback: EventListener, methodName: string): void { + if (typeof callback !== 'function' && typeof callback !== 'object') { + throw new TypeError( + `Failed to execute '${methodName}' on 'EventTarget': parameter 2 is not of type 'Object'.`, + ); + } +} + +const EVENT_DISPATCH_FLAG = Symbol('Event.dispatch'); + +function getEventDispatchFlag(event: Event): boolean { + // $FlowExpectedError[prop-missing] + return event[EVENT_DISPATCH_FLAG]; +} + +function setEventDispatchFlag(event: Event, value: boolean): void { + // $FlowExpectedError[prop-missing] + event[EVENT_DISPATCH_FLAG] = value; +} diff --git a/packages/react-native/src/private/webapis/dom/events/__tests__/Event-itest.js b/packages/react-native/src/private/webapis/dom/events/__tests__/Event-itest.js new file mode 100644 index 00000000000000..f4bec85d17ad83 --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/events/__tests__/Event-itest.js @@ -0,0 +1,151 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + * @fantom_flags enableAccessToHostTreeInFabric:true + */ + +import '../../../../../../Libraries/Core/InitializeCore.js'; + +import Event from '../Event'; +import {setInPassiveListenerFlag} from '../internals/EventInternals'; + +describe('Event', () => { + it('should throw an error if type is not passed', () => { + expect(() => { + // $FlowExpectedError[incompatible-call] + return new Event(); + }).toThrow( + "Failed to construct 'Event': 1 argument required, but only 0 present.", + ); + }); + + it('should throw an error if the given options is not an object, null or undefined', () => { + expect(() => { + // $FlowExpectedError[incompatible-call] + return new Event('custom', 1); + }).toThrow( + "Failed to construct 'Event': The provided value is not of type 'EventInit'.", + ); + + expect(() => { + // $FlowExpectedError[incompatible-call] + return new Event('custom', '1'); + }).toThrow( + "Failed to construct 'Event': The provided value is not of type 'EventInit'.", + ); + + expect(() => { + return new Event('custom', null); + }).not.toThrow(); + + expect(() => { + return new Event('custom', undefined); + }).not.toThrow(); + + expect(() => { + return new Event('custom', {}); + }).not.toThrow(); + }); + + it('should have default values for as a non-dispatched event', () => { + const event = new Event('custom'); + + expect(event.currentTarget).toBe(null); + expect(event.defaultPrevented).toBe(false); + expect(event.eventPhase).toBe(Event.NONE); + expect(event.isTrusted).toBe(false); + expect(event.target).toBe(null); + expect(event.composedPath()).toEqual([]); + }); + + it('should initialize the event with the given options', () => { + const eventWithDefaults = new Event('custom'); + + expect(eventWithDefaults.type).toBe('custom'); + expect(eventWithDefaults.bubbles).toBe(false); + expect(eventWithDefaults.cancelable).toBe(false); + expect(eventWithDefaults.composed).toBe(false); + + const eventWithAllOptionsSet = new Event('custom', { + bubbles: true, + cancelable: true, + composed: true, + }); + + expect(eventWithAllOptionsSet.type).toBe('custom'); + expect(eventWithAllOptionsSet.bubbles).toBe(true); + expect(eventWithAllOptionsSet.cancelable).toBe(true); + expect(eventWithAllOptionsSet.composed).toBe(true); + }); + + it('should set the timestamp with the current high resolution time', () => { + const lowerBoundTimestamp = performance.now(); + const event = new Event('type'); + const upperBoundTimestamp = performance.now(); + + expect(event.timeStamp).toBeGreaterThanOrEqual(lowerBoundTimestamp); + expect(event.timeStamp).toBeLessThanOrEqual(upperBoundTimestamp); + }); + + describe('preventDefault', () => { + it('does nothing with non-cancelable events', () => { + const event = new Event('custom', { + cancelable: false, + }); + + expect(event.defaultPrevented).toBe(false); + + event.preventDefault(); + + expect(event.defaultPrevented).toBe(false); + }); + + it('cancels cancelable events', () => { + const event = new Event('custom', { + cancelable: true, + }); + + expect(event.defaultPrevented).toBe(false); + + event.preventDefault(); + + expect(event.defaultPrevented).toBe(true); + }); + + it('does not cancel events with the "in passive listener" flag set, and logs an error', () => { + const event = new Event('custom', { + cancelable: true, + }); + + expect(event.defaultPrevented).toBe(false); + + setInPassiveListenerFlag(event, true); + + const previousConsoleError = console.error; + const mockConsoleError = jest.fn(); + try { + // $FlowExpectedError[cannot-write] + console.error = mockConsoleError; + event.preventDefault(); + } finally { + // $FlowExpectedError[cannot-write] + console.error = previousConsoleError; + } + + expect(event.defaultPrevented).toBe(false); + + expect(mockConsoleError).toHaveBeenCalledTimes(1); + const reportedError = mockConsoleError.mock.lastCall[0]; + expect(reportedError).toBeInstanceOf(Error); + expect(reportedError.message).toBe( + 'Unable to preventDefault inside passive event listener invocation.', + ); + }); + }); +}); diff --git a/packages/react-native/src/private/webapis/dom/events/__tests__/EventTarget-itest.js b/packages/react-native/src/private/webapis/dom/events/__tests__/EventTarget-itest.js new file mode 100644 index 00000000000000..6d82f51e6890d3 --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/events/__tests__/EventTarget-itest.js @@ -0,0 +1,930 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + * @fantom_flags enableAccessToHostTreeInFabric:true + */ + +import '../../../../../../Libraries/Core/InitializeCore.js'; + +import Event from '../Event'; +import EventTarget from '../EventTarget'; +import { + EVENT_TARGET_GET_THE_PARENT_KEY, + dispatchTrustedEvent, +} from '../internals/EventTargetInternals'; + +let listenerCallOrder = 0; + +function resetListenerCallOrder() { + listenerCallOrder = 0; +} + +type EventRecordingListener = JestMockFn<[Event], void> & { + eventData?: { + callOrder: number, + composedPath: $ReadOnlyArray, + currentTarget: Event['currentTarget'], + eventPhase: Event['eventPhase'], + target: Event['target'], + }, + ... +}; + +function createListener( + implementation?: Event => void, +): EventRecordingListener { + // $FlowExpectedError[prop-missing] + const listener: EventRecordingListener = jest.fn((event: Event) => { + listener.eventData = { + callOrder: listenerCallOrder++, + composedPath: event.composedPath(), + currentTarget: event.currentTarget, + eventPhase: event.eventPhase, + target: event.target, + }; + + if (implementation) { + implementation(event); + } + }); + + return listener; +} + +function createEventTargetHierarchyWithDepth( + depth: number, +): Array { + const targets = []; + + for (let i = 0; i < depth; i++) { + const target = new EventTarget(); + const parentTarget = targets[targets.length - 1]; + + // $FlowExpectedError[prop-missing] + target[EVENT_TARGET_GET_THE_PARENT_KEY] = () => parentTarget; + + targets.push(target); + } + + return targets; +} + +describe('EventTarget', () => { + describe('addEventListener', () => { + it('should throw an error if event or callback are NOT passed', () => { + const eventTarget = new EventTarget(); + + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.addEventListener(); + }).toThrow( + "Failed to execute 'addEventListener' on 'EventTarget': 2 arguments required, but only 0 present.", + ); + + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.addEventListener('custom'); + }).toThrow( + "Failed to execute 'addEventListener' on 'EventTarget': 2 arguments required, but only 1 present.", + ); + + expect(() => { + eventTarget.addEventListener('custom', () => {}); + }).not.toThrow(); + }); + + it('should throw an error if the callback is NOT a function or an object', () => { + const eventTarget = new EventTarget(); + + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.addEventListener('custom', 'foo'); + }).toThrow( + "Failed to execute 'addEventListener' on 'EventTarget': parameter 2 is not of type 'Object'.", + ); + + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.addEventListener('custom', Symbol('test')); + }).toThrow( + "Failed to execute 'addEventListener' on 'EventTarget': parameter 2 is not of type 'Object'.", + ); + + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.addEventListener('custom', true); + }).toThrow( + "Failed to execute 'addEventListener' on 'EventTarget': parameter 2 is not of type 'Object'.", + ); + + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.addEventListener('custom', 5); + }).toThrow( + "Failed to execute 'addEventListener' on 'EventTarget': parameter 2 is not of type 'Object'.", + ); + + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.addEventListener('custom', {}); + }).not.toThrow(); + + // It should work even if the `handleEvent` property is not a function. + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.addEventListener('custom', { + handleEvent: 5, + }); + }).not.toThrow(); + + expect(() => { + eventTarget.addEventListener('custom', { + handleEvent: () => {}, + }); + }).not.toThrow(); + + expect(() => { + eventTarget.addEventListener('custom', () => {}); + }).not.toThrow(); + }); + }); + + describe('removeEventListener', () => { + it('should throw an error if event or callback are NOT passed', () => { + const eventTarget = new EventTarget(); + + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.removeEventListener(); + }).toThrow( + "Failed to execute 'removeEventListener' on 'EventTarget': 2 arguments required, but only 0 present.", + ); + + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.removeEventListener('eventName'); + }).toThrow( + "Failed to execute 'removeEventListener' on 'EventTarget': 2 arguments required, but only 1 present.", + ); + + expect(() => { + eventTarget.removeEventListener('eventName', () => {}); + }).not.toThrow(); + }); + + it('should throw an error if the callback is NOT a function or an object', () => { + const eventTarget = new EventTarget(); + + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.removeEventListener('eventName', 'foo'); + }).toThrow( + "Failed to execute 'removeEventListener' on 'EventTarget': parameter 2 is not of type 'Object'.", + ); + + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.removeEventListener('eventName', Symbol('test')); + }).toThrow( + "Failed to execute 'removeEventListener' on 'EventTarget': parameter 2 is not of type 'Object'.", + ); + + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.removeEventListener('eventName', true); + }).toThrow( + "Failed to execute 'removeEventListener' on 'EventTarget': parameter 2 is not of type 'Object'.", + ); + + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.removeEventListener('eventName', 5); + }).toThrow( + "Failed to execute 'removeEventListener' on 'EventTarget': parameter 2 is not of type 'Object'.", + ); + + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.removeEventListener('eventName', {}); + }).not.toThrow(); + + // It should work even if the `handleEvent` property is not a function. + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.removeEventListener('eventName', { + handleEvent: 5, + }); + }).not.toThrow(); + + expect(() => { + eventTarget.removeEventListener('eventName', { + handleEvent: () => {}, + }); + }).not.toThrow(); + + expect(() => { + eventTarget.removeEventListener('eventName', () => {}); + }).not.toThrow(); + }); + }); + + describe('internal `dispatchTrustedEvent`', () => { + it('should set the `isTrusted` flag to `true`', () => { + const eventTarget = new EventTarget(); + + const listener = createListener(); + + eventTarget.addEventListener('custom', listener); + + const event = new Event('custom'); + + dispatchTrustedEvent(eventTarget, event); + + expect(event.isTrusted).toBe(true); + }); + }); + + describe('dispatchEvent', () => { + it('should throw an error if event is NOT passed', () => { + const eventTarget = new EventTarget(); + + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.dispatchEvent(); + }).toThrow( + "Failed to execute 'dispatchEvent' on 'EventTarget': parameter 1 is not of type 'Event'.", + ); + + expect(() => { + eventTarget.dispatchEvent(new Event('eventName')); + }).not.toThrow(); + }); + + it('should throw an error if the passed value is NOT an `Event` instance', () => { + const eventTarget = new EventTarget(); + + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.dispatchEvent('foo'); + }).toThrow( + "Failed to execute 'dispatchEvent' on 'EventTarget': parameter 1 is not of type 'Event'.", + ); + + expect(() => { + // $FlowExpectedError[incompatible-call] + eventTarget.dispatchEvent(true); + }).toThrow( + "Failed to execute 'dispatchEvent' on 'EventTarget': parameter 1 is not of type 'Event'.", + ); + }); + + it('works with listeners as functions and as objects with a `handleEvent` method', () => { + const eventTarget = new EventTarget(); + + const listenerFunction = createListener(); + const handleEventMethod = createListener(); + const listenerObject = { + handleEvent: handleEventMethod, + }; + + eventTarget.addEventListener('custom', listenerFunction); + eventTarget.addEventListener('custom', listenerObject); + + const event = new Event('custom'); + + eventTarget.dispatchEvent(event); + + expect(listenerFunction.mock.lastCall[0]).toBe(event); + expect(handleEventMethod.mock.lastCall[0]).toBe(event); + }); + + it('sets the global `event` value to the event while it is in dispatch', () => { + const eventTarget = new EventTarget(); + + let globalEventDuringDispatch; + let globalEventBeforeDispatch = Symbol('some value'); + + global.event = globalEventBeforeDispatch; + + const listener = createListener(() => { + globalEventDuringDispatch = global.event; + }); + + eventTarget.addEventListener('custom', listener); + + const event = new Event('custom'); + + eventTarget.dispatchEvent(event); + + expect(globalEventDuringDispatch).toBe(event); + expect(global.event).toBe(globalEventBeforeDispatch); + }); + + it('sets the `isTrusted` flag to `false`', () => { + const eventTarget = new EventTarget(); + + const listener = createListener(); + + eventTarget.addEventListener('custom', listener); + + const event = new Event('custom'); + + dispatchTrustedEvent(eventTarget, event); + + expect(event.isTrusted).toBe(true); + + eventTarget.dispatchEvent(event); + + expect(event.isTrusted).toBe(false); + }); + + it('should call listeners in the same target in the order in which they were added', () => { + const [node] = createEventTargetHierarchyWithDepth(1); + + // Listener setup + + resetListenerCallOrder(); + + const firstListener = createListener(); + const secondListener = createListener(); + const thirdListener = createListener(); + + node.addEventListener('custom', firstListener); + node.addEventListener('custom', secondListener); + node.addEventListener('custom', thirdListener); + + // Dispatch + + const event = new Event('custom'); + + node.dispatchEvent(event); + + expect(firstListener.eventData?.callOrder).toBe(0); + expect(secondListener.eventData?.callOrder).toBe(1); + expect(thirdListener.eventData?.callOrder).toBe(2); + }); + + describe('bubbling', () => { + it('should call listeners in the capturing phase, target phase and bubbling phase when dispatching events that bubble', () => { + const [parentTarget, childTarget, grandchildTarget] = + createEventTargetHierarchyWithDepth(3); + + // Listener setup + + resetListenerCallOrder(); + + const capturingListenerOnParent = createListener(); + const capturingListenerOnChild = createListener(); + const capturingListenerOnGrandchild = createListener(); + const bubblingListenerOnParent = createListener(); + const bubblingListenerOnChild = createListener(); + const bubblingListenerOnGrandchild = createListener(); + + parentTarget.addEventListener( + 'custom', + capturingListenerOnParent, + true, + ); + parentTarget.addEventListener('custom', bubblingListenerOnParent); + + childTarget.addEventListener('custom', capturingListenerOnChild, true); + childTarget.addEventListener('custom', bubblingListenerOnChild); + + grandchildTarget.addEventListener( + 'custom', + capturingListenerOnGrandchild, + true, + ); + grandchildTarget.addEventListener( + 'custom', + bubblingListenerOnGrandchild, + ); + + // Dispatch + + const event = new Event('custom', {bubbles: true}); + + const result = grandchildTarget.dispatchEvent(event); + + expect(result).toBe(true); + + expect(capturingListenerOnParent.eventData).toEqual({ + callOrder: 0, + composedPath: [grandchildTarget, childTarget, parentTarget], + currentTarget: parentTarget, + eventPhase: Event.CAPTURING_PHASE, + target: grandchildTarget, + }); + expect(capturingListenerOnParent.mock.contexts[0]).toBe(parentTarget); + + expect(capturingListenerOnChild.eventData).toEqual({ + callOrder: 1, + composedPath: [grandchildTarget, childTarget, parentTarget], + currentTarget: childTarget, + eventPhase: Event.CAPTURING_PHASE, + target: grandchildTarget, + }); + expect(capturingListenerOnChild.mock.contexts[0]).toBe(childTarget); + + expect(capturingListenerOnGrandchild.eventData).toEqual({ + callOrder: 2, + composedPath: [grandchildTarget, childTarget, parentTarget], + currentTarget: grandchildTarget, + eventPhase: Event.AT_TARGET, + target: grandchildTarget, + }); + expect(capturingListenerOnGrandchild.mock.contexts[0]).toBe( + grandchildTarget, + ); + + expect(bubblingListenerOnGrandchild.eventData).toEqual({ + callOrder: 3, + composedPath: [grandchildTarget, childTarget, parentTarget], + currentTarget: grandchildTarget, + eventPhase: Event.AT_TARGET, + target: grandchildTarget, + }); + expect(bubblingListenerOnGrandchild.mock.contexts[0]).toBe( + grandchildTarget, + ); + + expect(bubblingListenerOnChild.eventData).toEqual({ + callOrder: 4, + composedPath: [grandchildTarget, childTarget, parentTarget], + currentTarget: childTarget, + eventPhase: Event.BUBBLING_PHASE, + target: grandchildTarget, + }); + expect(bubblingListenerOnChild.mock.contexts[0]).toBe(childTarget); + + expect(bubblingListenerOnParent.eventData).toEqual({ + callOrder: 5, + composedPath: [grandchildTarget, childTarget, parentTarget], + currentTarget: parentTarget, + eventPhase: Event.BUBBLING_PHASE, + target: grandchildTarget, + }); + expect(bubblingListenerOnParent.mock.contexts[0]).toBe(parentTarget); + }); + + it('should call listeners in the capturing phase and target phase, but NOT in the bubbling phase when dispatching events that do NOT bubble', () => { + const [parentTarget, childTarget, grandchildTarget] = + createEventTargetHierarchyWithDepth(3); + + // Listener setup + + resetListenerCallOrder(); + + const capturingListenerOnParent = createListener(); + const capturingListenerOnChild = createListener(); + const capturingListenerOnGrandchild = createListener(); + const bubblingListenerOnParent = createListener(); + const bubblingListenerOnChild = createListener(); + const bubblingListenerOnGrandchild = createListener(); + + parentTarget.addEventListener( + 'custom', + capturingListenerOnParent, + true, + ); + parentTarget.addEventListener('custom', bubblingListenerOnParent); + + childTarget.addEventListener('custom', capturingListenerOnChild, true); + childTarget.addEventListener('custom', bubblingListenerOnChild); + + grandchildTarget.addEventListener( + 'custom', + capturingListenerOnGrandchild, + true, + ); + grandchildTarget.addEventListener( + 'custom', + bubblingListenerOnGrandchild, + ); + + // Dispatch + + const event = new Event('custom', {bubbles: false}); + + const result = grandchildTarget.dispatchEvent(event); + + expect(result).toBe(true); + + expect(capturingListenerOnParent.eventData).toEqual({ + callOrder: 0, + composedPath: [grandchildTarget, childTarget, parentTarget], + currentTarget: parentTarget, + eventPhase: Event.CAPTURING_PHASE, + target: grandchildTarget, + }); + expect(capturingListenerOnParent.mock.contexts[0]).toBe(parentTarget); + + expect(capturingListenerOnChild.eventData).toEqual({ + callOrder: 1, + composedPath: [grandchildTarget, childTarget, parentTarget], + currentTarget: childTarget, + eventPhase: Event.CAPTURING_PHASE, + target: grandchildTarget, + }); + expect(capturingListenerOnChild.mock.contexts[0]).toBe(childTarget); + + expect(capturingListenerOnGrandchild.eventData).toEqual({ + callOrder: 2, + composedPath: [grandchildTarget, childTarget, parentTarget], + currentTarget: grandchildTarget, + eventPhase: Event.AT_TARGET, + target: grandchildTarget, + }); + expect(capturingListenerOnGrandchild.mock.contexts[0]).toBe( + grandchildTarget, + ); + + expect(bubblingListenerOnGrandchild.eventData).toEqual({ + callOrder: 3, + composedPath: [grandchildTarget, childTarget, parentTarget], + currentTarget: grandchildTarget, + eventPhase: Event.AT_TARGET, + target: grandchildTarget, + }); + expect(bubblingListenerOnGrandchild.mock.contexts[0]).toBe( + grandchildTarget, + ); + + // NO bubbling phase calls + expect(bubblingListenerOnChild).not.toHaveBeenCalled(); + expect(bubblingListenerOnParent).not.toHaveBeenCalled(); + }); + + it('should restore event properties after dispatch', () => { + const [parentTarget, childTarget, grandchildTarget] = + createEventTargetHierarchyWithDepth(3); + + // Listener setup + + resetListenerCallOrder(); + + const capturingListenerOnParent = createListener(); + const capturingListenerOnChild = createListener(); + const capturingListenerOnGrandchild = createListener(); + const bubblingListenerOnParent = createListener(); + const bubblingListenerOnChild = createListener(); + const bubblingListenerOnGrandchild = createListener(event => { + event.preventDefault(); + }); + + parentTarget.addEventListener( + 'custom', + capturingListenerOnParent, + true, + ); + parentTarget.addEventListener('custom', bubblingListenerOnParent); + + childTarget.addEventListener('custom', capturingListenerOnChild, true); + childTarget.addEventListener('custom', bubblingListenerOnChild); + + grandchildTarget.addEventListener( + 'custom', + capturingListenerOnGrandchild, + true, + ); + grandchildTarget.addEventListener( + 'custom', + bubblingListenerOnGrandchild, + ); + + // Dispatch + + const event = new Event('custom', {bubbles: true, cancelable: true}); + + grandchildTarget.dispatchEvent(event); + + // Should be restored + expect(event.composedPath()).toEqual([]); + expect(event.currentTarget).toBe(null); + expect(event.eventPhase).toBe(Event.NONE); + + // Should be preserved + expect(event.target).toBe(grandchildTarget); + expect(event.defaultPrevented).toBe(true); + }); + }); + + describe('stopPropagation', () => { + it('should continue calling listeners in the same target, but NOT on parents', () => { + const [parentTarget, childTarget] = + createEventTargetHierarchyWithDepth(2); + + // Listener setup + + resetListenerCallOrder(); + + const parentListener = createListener(); + + const firstListener = createListener(); + const secondListener = createListener(event => { + event.stopPropagation(); + }); + const thirdListener = createListener(); + + parentTarget.addEventListener('custom', parentListener); + + childTarget.addEventListener('custom', firstListener); + childTarget.addEventListener('custom', secondListener); + childTarget.addEventListener('custom', thirdListener); + + // Dispatch + + const event = new Event('custom'); + + childTarget.dispatchEvent(event); + + resetListenerCallOrder(); + + expect(firstListener).toHaveBeenCalled(); + expect(secondListener).toHaveBeenCalled(); + expect(thirdListener).toHaveBeenCalled(); + expect(parentListener).not.toHaveBeenCalled(); + }); + }); + + describe('stopImmediatePropagation', () => { + it('should stop calling listeners on the same target as well', () => { + const [parentTarget, childTarget] = + createEventTargetHierarchyWithDepth(2); + + // Listener setup + + resetListenerCallOrder(); + + const parentListener = createListener(); + + const firstListener = createListener(); + const secondListener = createListener(event => { + event.stopImmediatePropagation(); + }); + const thirdListener = createListener(); + + parentTarget.addEventListener('custom', parentListener); + + childTarget.addEventListener('custom', firstListener); + childTarget.addEventListener('custom', secondListener); + childTarget.addEventListener('custom', thirdListener); + + // Dispatch + + const event = new Event('custom'); + + childTarget.dispatchEvent(event); + + resetListenerCallOrder(); + + expect(firstListener).toHaveBeenCalled(); + expect(secondListener).toHaveBeenCalled(); + expect(thirdListener).not.toHaveBeenCalled(); + + expect(parentListener).not.toHaveBeenCalled(); + }); + }); + + describe('preventDefault', () => { + it('should cancel cancelable events', () => { + const [node] = createEventTargetHierarchyWithDepth(1); + + // Listener setup + + resetListenerCallOrder(); + + const listener = createListener(event => { + event.preventDefault(); + }); + + node.addEventListener('custom', listener); + + // Dispatch + + const event = new Event('custom', {cancelable: true}); + + const result = node.dispatchEvent(event); + + expect(result).toBe(false); + + expect(event.defaultPrevented).toBe(true); + }); + + it('should NOT cancel cancelable event in passive listeners', () => { + const [node] = createEventTargetHierarchyWithDepth(1); + + // Listener setup + + resetListenerCallOrder(); + + const listener = createListener(event => { + event.preventDefault(); + }); + + node.addEventListener('custom', listener, {passive: true}); + + // Dispatch + + const event = new Event('custom', {cancelable: true}); + + const result = node.dispatchEvent(event); + + expect(result).toBe(true); + + expect(event.defaultPrevented).toBe(false); + }); + }); + + describe('events with `once`', () => { + it('should remove the listener after the first call', () => { + const [node] = createEventTargetHierarchyWithDepth(1); + + // Listener setup + + resetListenerCallOrder(); + + const listener = createListener(); + + node.addEventListener('custom', listener, {once: true}); + + // Dispatch + + const event = new Event('custom'); + + node.dispatchEvent(event); + + expect(listener).toHaveBeenCalledTimes(1); + + node.dispatchEvent(event); + + expect(listener).toHaveBeenCalledTimes(1); + }); + }); + + describe('events with `signal`', () => { + it('should remove the listener when the signal is aborted before registration', () => { + const [node] = createEventTargetHierarchyWithDepth(1); + + // Listener setup + + resetListenerCallOrder(); + + const listener = createListener(); + + const abortController = new AbortController(); + + abortController.abort(); + + node.addEventListener('custom', listener, { + signal: abortController.signal, + }); + + // Dispatch + + const event = new Event('custom'); + + node.dispatchEvent(event); + + expect(listener).not.toHaveBeenCalled(); + }); + + it('should remove the listener when the signal is aborted after registration', () => { + const [node] = createEventTargetHierarchyWithDepth(1); + + // Listener setup + + resetListenerCallOrder(); + + const listener = createListener(); + + const abortController = new AbortController(); + + node.addEventListener('custom', listener, { + signal: abortController.signal, + }); + + // Dispatch + + const event = new Event('custom'); + + node.dispatchEvent(event); + + expect(listener).toHaveBeenCalledTimes(1); + + abortController.abort(); + + node.dispatchEvent(event); + + expect(listener).toHaveBeenCalledTimes(1); + }); + }); + + describe('dispatching an event while the same event is being dispatched', () => { + it('should throw an error', () => { + const [node] = createEventTargetHierarchyWithDepth(1); + + // Listener setup + + resetListenerCallOrder(); + + const event = new Event('custom'); + + let errorWhenRedispatching: ?Error; + + const listener = createListener(() => { + try { + node.dispatchEvent(event); + } catch (error) { + errorWhenRedispatching = error; + } + }); + + node.addEventListener('custom', listener); + + node.dispatchEvent(event); + + expect(listener).toHaveBeenCalledTimes(1); + expect(errorWhenRedispatching).toBeInstanceOf(Error); + expect(errorWhenRedispatching?.message).toBe( + "Failed to execute 'dispatchEvent' on 'EventTarget': The event is already being dispatched.", + ); + }); + }); + + describe('adding listeners during dispatch', () => { + it('should NOT call listeners for a target and phase that were added during the dispatch of the event for that target and phase', () => { + const [parentTarget, childTarget] = + createEventTargetHierarchyWithDepth(2); + + // Listener setup + + resetListenerCallOrder(); + + const newParentBubblingListener = createListener(); + + const newChildBubblingListener = createListener(); + const newChildCapturingListener = createListener(); + + const childCapturingListener = createListener(() => { + // These should be called + childTarget.addEventListener('custom', newChildBubblingListener); + parentTarget.addEventListener('custom', newParentBubblingListener); + + // This should NOT be called + childTarget.addEventListener( + 'custom', + newChildCapturingListener, + true, + ); + }); + + childTarget.addEventListener('custom', childCapturingListener, true); + + // Dispatch + + const event = new Event('custom', {bubbles: true}); + + childTarget.dispatchEvent(event); + + expect(childCapturingListener).toHaveBeenCalled(); + expect(newChildCapturingListener).not.toHaveBeenCalled(); + expect(newChildBubblingListener).toHaveBeenCalled(); + expect(newParentBubblingListener).toHaveBeenCalled(); + }); + }); + + describe('removing listeners during dispatch', () => { + it('should NOT call them', () => { + const [node] = createEventTargetHierarchyWithDepth(1); + + // Listener setup + + resetListenerCallOrder(); + + const listener = createListener(() => { + node.removeEventListener('custom', listenerThatWillBeRemoved); + }); + const listenerThatWillBeRemoved = createListener(); + + node.addEventListener('custom', listener); + node.addEventListener('custom', listenerThatWillBeRemoved); + + // Dispatch + + const event = new Event('custom'); + + node.dispatchEvent(event); + + expect(listener).toHaveBeenCalledTimes(1); + expect(listenerThatWillBeRemoved).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/packages/react-native/src/private/webapis/dom/events/internals/EventInternals.js b/packages/react-native/src/private/webapis/dom/events/internals/EventInternals.js new file mode 100644 index 00000000000000..7192ca4e57c08a --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/events/internals/EventInternals.js @@ -0,0 +1,120 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + */ + +/** + * This method contains internal implementation details for the `Event` module + * and it is defined in a separate module to keep the exports in `Event` clean + * (only with public exports). + */ + +import type Event, {EventPhase} from '../Event'; +import type EventTarget from '../EventTarget'; + +export const COMPOSED_PATH_KEY: symbol = Symbol('composedPath'); +export const CURRENT_TARGET_KEY: symbol = Symbol('currentTarget'); +export const EVENT_PHASE_KEY: symbol = Symbol('eventPhase'); +export const IN_PASSIVE_LISTENER_FLAG_KEY: symbol = Symbol( + 'inPassiveListenerFlag', +); +export const IS_TRUSTED_KEY: symbol = Symbol('isTrusted'); +export const STOP_IMMEDIATE_PROPAGATION_FLAG_KEY: symbol = Symbol( + 'stopPropagationFlag', +); +export const STOP_PROPAGATION_FLAG_KEY: symbol = Symbol('stopPropagationFlag'); +export const TARGET_KEY: symbol = Symbol('target'); + +export function getCurrentTarget(event: Event): EventTarget | null { + // $FlowExpectedError[prop-missing] + return event[CURRENT_TARGET_KEY]; +} + +export function setCurrentTarget( + event: Event, + currentTarget: EventTarget | null, +): void { + // $FlowExpectedError[prop-missing] + event[CURRENT_TARGET_KEY] = currentTarget; +} + +export function getComposedPath(event: Event): $ReadOnlyArray { + // $FlowExpectedError[prop-missing] + return event[COMPOSED_PATH_KEY]; +} + +export function setComposedPath( + event: Event, + composedPath: $ReadOnlyArray, +): void { + // $FlowExpectedError[prop-missing] + event[COMPOSED_PATH_KEY] = composedPath; +} + +export function getEventPhase(event: Event): EventPhase { + // $FlowExpectedError[prop-missing] + return event[EVENT_PHASE_KEY]; +} + +export function setEventPhase(event: Event, eventPhase: EventPhase): void { + // $FlowExpectedError[prop-missing] + event[EVENT_PHASE_KEY] = eventPhase; +} + +export function getInPassiveListenerFlag(event: Event): boolean { + // $FlowExpectedError[prop-missing] + return event[IN_PASSIVE_LISTENER_FLAG_KEY]; +} + +export function setInPassiveListenerFlag(event: Event, value: boolean): void { + // $FlowExpectedError[prop-missing] + event[IN_PASSIVE_LISTENER_FLAG_KEY] = value; +} + +export function getIsTrusted(event: Event): boolean { + // $FlowExpectedError[prop-missing] + return event[IS_TRUSTED_KEY]; +} + +export function setIsTrusted(event: Event, isTrusted: boolean): void { + // $FlowExpectedError[prop-missing] + event[IS_TRUSTED_KEY] = isTrusted; +} + +export function getStopImmediatePropagationFlag(event: Event): boolean { + // $FlowExpectedError[prop-missing] + return event[STOP_IMMEDIATE_PROPAGATION_FLAG_KEY]; +} + +export function setStopImmediatePropagationFlag( + event: Event, + value: boolean, +): void { + // $FlowExpectedError[prop-missing] + event[STOP_IMMEDIATE_PROPAGATION_FLAG_KEY] = value; +} + +export function getStopPropagationFlag(event: Event): boolean { + // $FlowExpectedError[prop-missing] + return event[STOP_PROPAGATION_FLAG_KEY]; +} + +export function setStopPropagationFlag(event: Event, value: boolean): void { + // $FlowExpectedError[prop-missing] + event[STOP_PROPAGATION_FLAG_KEY] = value; +} + +export function getTarget(event: Event): EventTarget | null { + // $FlowExpectedError[prop-missing] + return event[TARGET_KEY]; +} + +export function setTarget(event: Event, target: EventTarget | null): void { + // $FlowExpectedError[prop-missing] + event[TARGET_KEY] = target; +} diff --git a/packages/react-native/src/private/webapis/dom/events/internals/EventTargetInternals.js b/packages/react-native/src/private/webapis/dom/events/internals/EventTargetInternals.js new file mode 100644 index 00000000000000..8061207d597263 --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/events/internals/EventTargetInternals.js @@ -0,0 +1,52 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + */ + +/** + * This method contains internal implementation details for the `EventTarget` + * module and it is defined in a separate module to keep the exports in + * the original module clean (only with public exports). + */ + +import type Event from '../Event'; +import type EventTarget from '../EventTarget'; + +import {setIsTrusted} from './EventInternals'; + +/** + * Use this symbol as key for a method to implement the "get the parent" + * algorithm in an `EventTarget` subclass. + */ +export const EVENT_TARGET_GET_THE_PARENT_KEY: symbol = Symbol( + 'EventTarget[get the parent]', +); + +/** + * This is only exposed to implement the method in `EventTarget`. + * Do NOT use this directly (use the `dispatchTrustedEvent` method instead). + */ +export const INTERNAL_DISPATCH_METHOD_KEY: symbol = Symbol( + 'EventTarget[dispatch]', +); + +/** + * Dispatches a trusted event to the given event target. + * + * This should only be used by the runtime to dispatch native events to + * JavaScript. + */ +export function dispatchTrustedEvent( + eventTarget: EventTarget, + event: Event, +): void { + setIsTrusted(event, true); + + // $FlowExpectedError[prop-missing] + return eventTarget[INTERNAL_DISPATCH_METHOD_KEY](event); +}