forked from facebook/react-native
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement Event and EventTarget (facebook#48429)
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
1 parent
41ef7fd
commit cb1f636
Showing
6 changed files
with
1,816 additions
and
0 deletions.
There are no files selected for viewing
177 changes: 177 additions & 0 deletions
177
packages/react-native/src/private/webapis/dom/events/Event.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<EventTarget> { | ||
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']; |
386 changes: 386 additions & 0 deletions
386
packages/react-native/src/private/webapis/dom/events/EventTarget.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string, Array<EventListenerRegistration>> = new Map(); | ||
#captureListeners: Map<string, Array<EventListenerRegistration>> = 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<EventTarget> { | ||
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; | ||
} |
151 changes: 151 additions & 0 deletions
151
packages/react-native/src/private/webapis/dom/events/__tests__/Event-itest.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.', | ||
); | ||
}); | ||
}); | ||
}); |
930 changes: 930 additions & 0 deletions
930
packages/react-native/src/private/webapis/dom/events/__tests__/EventTarget-itest.js
Large diffs are not rendered by default.
Oops, something went wrong.
120 changes: 120 additions & 0 deletions
120
packages/react-native/src/private/webapis/dom/events/internals/EventInternals.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<EventTarget> { | ||
// $FlowExpectedError[prop-missing] | ||
return event[COMPOSED_PATH_KEY]; | ||
} | ||
|
||
export function setComposedPath( | ||
event: Event, | ||
composedPath: $ReadOnlyArray<EventTarget>, | ||
): 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; | ||
} |
52 changes: 52 additions & 0 deletions
52
packages/react-native/src/private/webapis/dom/events/internals/EventTargetInternals.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} |