@@ -95,6 +95,28 @@ export type RendererInspectionConfig = $ReadOnly<{|
95
95
) => void ,
96
96
| } > ;
97
97
98
+ // TODO?: find a better place for this type to live
99
+ export type EventListenerOptions = $ReadOnly < { |
100
+ capture ? : boolean ,
101
+ once ? : boolean ,
102
+ passive ? : boolean ,
103
+ signal : mixed , // not yet implemented
104
+ | } > ;
105
+ export type EventListenerRemoveOptions = $ReadOnly < { |
106
+ capture ? : boolean ,
107
+ | } > ;
108
+
109
+ // TODO?: this will be changed in the future to be w3c-compatible and allow "EventListener" objects as well as functions.
110
+ export type EventListener = Function ;
111
+
112
+ type InternalEventListeners = {
113
+ [ string ] : { |
114
+ listener : EventListener ,
115
+ options : EventListenerOptions ,
116
+ invalidated : boolean ,
117
+ | } [ ] ,
118
+ } ;
119
+
98
120
// TODO: Remove this conditional once all changes have propagated.
99
121
if ( registerEventHandler ) {
100
122
/**
@@ -111,6 +133,7 @@ class ReactFabricHostComponent {
111
133
viewConfig : ViewConfig ;
112
134
currentProps : Props ;
113
135
_internalInstanceHandle : Object ;
136
+ _eventListeners : ?InternalEventListeners ;
114
137
115
138
constructor (
116
139
tag : number ,
@@ -193,6 +216,102 @@ class ReactFabricHostComponent {
193
216
194
217
return ;
195
218
}
219
+
220
+ // This API (addEventListener, removeEventListener) attempts to adhere to the
221
+ // w3 Level2 Events spec as much as possible, treating HostComponent as a DOM node.
222
+ //
223
+ // Unless otherwise noted, these methods should "just work" and adhere to the W3 specs.
224
+ // If they deviate in a way that is not explicitly noted here, you've found a bug!
225
+ //
226
+ // See:
227
+ // * https://www.w3.org/TR/DOM-Level-2-Events/events.html
228
+ // * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
229
+ // * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
230
+ //
231
+ // And notably, not implemented (yet?):
232
+ // * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent
233
+ //
234
+ //
235
+ // Deviations from spec/TODOs:
236
+ // (1) listener must currently be a function, we do not support EventListener objects yet.
237
+ // (2) we do not support the `signal` option / AbortSignal yet
238
+ addEventListener_unstable (
239
+ eventType : string ,
240
+ listener : EventListener ,
241
+ options : EventListenerOptions | boolean ,
242
+ ) {
243
+ if ( typeof eventType !== 'string' ) {
244
+ throw new Error ( 'addEventListener_unstable eventType must be a string' ) ;
245
+ }
246
+ if ( typeof listener !== 'function' ) {
247
+ throw new Error ( 'addEventListener_unstable listener must be a function' ) ;
248
+ }
249
+
250
+ // The third argument is either boolean indicating "captures" or an object.
251
+ const optionsObj =
252
+ typeof options === 'object' && options !== null ? options : { } ;
253
+ const capture =
254
+ ( typeof options === 'boolean' ? options : optionsObj . capture ) || false ;
255
+ const once = optionsObj . once || false ;
256
+ const passive = optionsObj . passive || false ;
257
+ const signal = null ; // TODO: implement signal/AbortSignal
258
+
259
+ const eventListeners : InternalEventListeners = this . _eventListeners || { } ;
260
+ if ( this . _eventListeners == null ) {
261
+ this . _eventListeners = eventListeners ;
262
+ }
263
+
264
+ const namedEventListeners = eventListeners [ eventType ] || [ ] ;
265
+ if ( eventListeners [ eventType ] == null ) {
266
+ eventListeners [ eventType ] = namedEventListeners ;
267
+ }
268
+
269
+ namedEventListeners . push ( {
270
+ listener : listener ,
271
+ invalidated : false ,
272
+ options : {
273
+ capture : capture ,
274
+ once : once ,
275
+ passive : passive ,
276
+ signal : signal ,
277
+ } ,
278
+ } ) ;
279
+ }
280
+
281
+ // See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
282
+ removeEventListener_unstable (
283
+ eventType : string ,
284
+ listener : EventListener ,
285
+ options : EventListenerRemoveOptions | boolean ,
286
+ ) {
287
+ // eventType and listener must be referentially equal to be removed from the listeners
288
+ // data structure, but in "options" we only check the `capture` flag, according to spec.
289
+ // That means if you add the same function as a listener with capture set to true and false,
290
+ // you must also call removeEventListener twice with capture set to true/false.
291
+ const optionsObj =
292
+ typeof options === 'object' && options !== null ? options : { } ;
293
+ const capture =
294
+ ( typeof options === 'boolean' ? options : optionsObj . capture ) || false ;
295
+
296
+ // If there are no event listeners or named event listeners, we can bail early - our
297
+ // job is already done.
298
+ const eventListeners = this . _eventListeners ;
299
+ if ( ! eventListeners ) {
300
+ return;
301
+ }
302
+ const namedEventListeners = eventListeners [ eventType ] ;
303
+ if ( ! namedEventListeners ) {
304
+ return ;
305
+ }
306
+
307
+ // TODO: optimize this path to make remove cheaper
308
+ eventListeners [ eventType ] = namedEventListeners . filter ( listenerObj => {
309
+ return ! (
310
+ listenerObj . listener === listener &&
311
+ listenerObj . options . capture === capture
312
+ ) ;
313
+ } ) ;
314
+ }
196
315
}
197
316
198
317
// eslint-disable-next-line no-unused-expressions
0 commit comments