@tamagui/react-native-use-responder-events 1.0.1-beta.194

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/cjs/PressResponder.js +373 -0
  2. package/dist/cjs/PressResponder.js.map +7 -0
  3. package/dist/cjs/ResponderSystem.js +365 -0
  4. package/dist/cjs/ResponderSystem.js.map +7 -0
  5. package/dist/cjs/ResponderTouchHistoryStore.js +184 -0
  6. package/dist/cjs/ResponderTouchHistoryStore.js.map +7 -0
  7. package/dist/cjs/createResponderEvent.js +152 -0
  8. package/dist/cjs/createResponderEvent.js.map +7 -0
  9. package/dist/cjs/index.js +19 -0
  10. package/dist/cjs/index.js.map +7 -0
  11. package/dist/cjs/types.js +98 -0
  12. package/dist/cjs/types.js.map +7 -0
  13. package/dist/cjs/useResponderEvents.js +76 -0
  14. package/dist/cjs/useResponderEvents.js.map +7 -0
  15. package/dist/cjs/utils.js +169 -0
  16. package/dist/cjs/utils.js.map +7 -0
  17. package/dist/esm/PressResponder.js +349 -0
  18. package/dist/esm/PressResponder.js.map +7 -0
  19. package/dist/esm/ResponderSystem.js +338 -0
  20. package/dist/esm/ResponderSystem.js.map +7 -0
  21. package/dist/esm/ResponderTouchHistoryStore.js +160 -0
  22. package/dist/esm/ResponderTouchHistoryStore.js.map +7 -0
  23. package/dist/esm/createResponderEvent.js +130 -0
  24. package/dist/esm/createResponderEvent.js.map +7 -0
  25. package/dist/esm/index.js +2 -0
  26. package/dist/esm/index.js.map +7 -0
  27. package/dist/esm/types.js +55 -0
  28. package/dist/esm/types.js.map +7 -0
  29. package/dist/esm/useResponderEvents.js +45 -0
  30. package/dist/esm/useResponderEvents.js.map +7 -0
  31. package/dist/esm/utils.js +137 -0
  32. package/dist/esm/utils.js.map +7 -0
  33. package/package.json +30 -0
  34. package/src/ResponderSystem.ts +531 -0
  35. package/src/ResponderTouchHistoryStore.ts +213 -0
  36. package/src/createResponderEvent.ts +187 -0
  37. package/src/index.ts +1 -0
  38. package/src/types.ts +78 -0
  39. package/src/useResponderEvents.ts +72 -0
  40. package/src/utils.ts +192 -0
  41. package/types/PressResponder.d.ts +92 -0
  42. package/types/ResponderSystem.d.ts +47 -0
  43. package/types/ResponderTouchHistoryStore.d.ts +37 -0
  44. package/types/createResponderEvent.d.ts +36 -0
  45. package/types/index.d.ts +2 -0
  46. package/types/types.d.ts +52 -0
  47. package/types/useResponderEvents.d.ts +9 -0
  48. package/types/utils.d.ts +38 -0
@@ -0,0 +1,531 @@
1
+ /**
2
+ * Copyright (c) Nicolas Gallagher
3
+ * This source code is licensed under the MIT license found in the
4
+ * LICENSE file in the root directory of this source tree.
5
+ */
6
+
7
+ import type { ResponderEvent } from './createResponderEvent'
8
+ import createResponderEvent from './createResponderEvent'
9
+ import { ResponderTouchHistoryStore } from './ResponderTouchHistoryStore'
10
+ import { isCancelish, isEndish, isMoveish, isScroll, isSelectionChange, isStartish } from './types'
11
+ import { canUseDOM } from './utils'
12
+ import {
13
+ getLowestCommonAncestor,
14
+ getResponderPaths,
15
+ hasTargetTouches,
16
+ hasValidSelection,
17
+ isPrimaryPointerDown,
18
+ setResponderId,
19
+ } from './utils'
20
+
21
+ /* ------------ TYPES ------------ */
22
+
23
+ type ResponderId = string
24
+
25
+ type ActiveResponderInstance = {
26
+ id: ResponderId
27
+ idPath: Array<number>
28
+ node: any
29
+ }
30
+
31
+ type EmptyResponderInstance = {
32
+ id: null
33
+ idPath: null
34
+ node: null
35
+ }
36
+
37
+ type ResponderInstance = ActiveResponderInstance | EmptyResponderInstance
38
+
39
+ export type ResponderConfig = {
40
+ // Direct responder events dispatched directly to responder. Do not bubble.
41
+ onResponderEnd?: ((e: ResponderEvent) => void) | null
42
+ onResponderGrant?: ((e: ResponderEvent) => void | boolean) | null
43
+ onResponderMove?: ((e: ResponderEvent) => void) | null
44
+ onResponderRelease?: ((e: ResponderEvent) => void) | null
45
+ onResponderReject?: ((e: ResponderEvent) => void) | null
46
+ onResponderStart?: ((e: ResponderEvent) => void) | null
47
+ onResponderTerminate?: ((e: ResponderEvent) => void) | null
48
+ onResponderTerminationRequest?: ((e: ResponderEvent) => boolean) | null
49
+ // On pointer down, should this element become the responder?
50
+ onStartShouldSetResponder?: ((e: ResponderEvent) => boolean) | null
51
+ onStartShouldSetResponderCapture?: ((e: ResponderEvent) => boolean) | null
52
+ // On pointer move, should this element become the responder?
53
+ onMoveShouldSetResponder?: ((e: ResponderEvent) => boolean) | null
54
+ onMoveShouldSetResponderCapture?: ((e: ResponderEvent) => boolean) | null
55
+ // On scroll, should this element become the responder? Do no bubble
56
+ onScrollShouldSetResponder?: ((e: ResponderEvent) => boolean) | null
57
+ onScrollShouldSetResponderCapture?: ((e: ResponderEvent) => boolean) | null
58
+ // On text selection change, should this element become the responder?
59
+ onSelectionChangeShouldSetResponder?: ((e: ResponderEvent) => boolean) | null
60
+ onSelectionChangeShouldSetResponderCapture?: ((e: ResponderEvent) => boolean) | null
61
+ }
62
+
63
+ const emptyObject = {}
64
+
65
+ /* ------------ IMPLEMENTATION ------------ */
66
+
67
+ const startRegistration = [
68
+ 'onStartShouldSetResponderCapture',
69
+ 'onStartShouldSetResponder',
70
+ { bubbles: true },
71
+ ]
72
+ const moveRegistration = [
73
+ 'onMoveShouldSetResponderCapture',
74
+ 'onMoveShouldSetResponder',
75
+ { bubbles: true },
76
+ ]
77
+ const scrollRegistration = [
78
+ 'onScrollShouldSetResponderCapture',
79
+ 'onScrollShouldSetResponder',
80
+ { bubbles: false },
81
+ ]
82
+ const shouldSetResponderEvents = {
83
+ touchstart: startRegistration,
84
+ mousedown: startRegistration,
85
+ touchmove: moveRegistration,
86
+ mousemove: moveRegistration,
87
+ scroll: scrollRegistration,
88
+ }
89
+
90
+ const emptyResponder = { id: null, idPath: null, node: null }
91
+ const responderListenersMap = new Map()
92
+
93
+ let isEmulatingMouseEvents = false
94
+ let trackedTouchCount = 0
95
+ let currentResponder: ResponderInstance = {
96
+ id: null,
97
+ node: null,
98
+ idPath: null,
99
+ }
100
+ const responderTouchHistoryStore = new ResponderTouchHistoryStore()
101
+
102
+ function changeCurrentResponder(responder: ResponderInstance) {
103
+ currentResponder = responder
104
+ }
105
+
106
+ function getResponderConfig(id: ResponderId): ResponderConfig | any {
107
+ const config = responderListenersMap.get(id)
108
+ return config != null ? config : emptyObject
109
+ }
110
+
111
+ /**
112
+ * Process native events
113
+ *
114
+ * A single event listener is used to manage the responder system.
115
+ * All pointers are tracked in the ResponderTouchHistoryStore. Native events
116
+ * are interpreted in terms of the Responder System and checked to see if
117
+ * the responder should be transferred. Each host node that is attached to
118
+ * the Responder System has an ID, which is used to look up its associated
119
+ * callbacks.
120
+ */
121
+ function eventListener(domEvent: any) {
122
+ const eventType = domEvent.type
123
+ const eventTarget = domEvent.target
124
+
125
+ /**
126
+ * Manage emulated events and early bailout.
127
+ * Since PointerEvent is not used yet (lack of support in older Safari), it's
128
+ * necessary to manually manage the mess of browser touch/mouse events.
129
+ * And bailout early for termination events when there is no active responder.
130
+ */
131
+
132
+ // Flag when browser may produce emulated events
133
+ if (eventType === 'touchstart') {
134
+ isEmulatingMouseEvents = true
135
+ }
136
+ // Remove flag when browser will not produce emulated events
137
+ if (eventType === 'touchmove' || trackedTouchCount > 1) {
138
+ isEmulatingMouseEvents = false
139
+ }
140
+ // Ignore various events in particular circumstances
141
+ if (
142
+ // Ignore browser emulated mouse events
143
+ (eventType === 'mousedown' && isEmulatingMouseEvents) ||
144
+ (eventType === 'mousemove' && isEmulatingMouseEvents) ||
145
+ // Ignore mousemove if a mousedown didn't occur first
146
+ (eventType === 'mousemove' && trackedTouchCount < 1)
147
+ ) {
148
+ return
149
+ }
150
+ // Remove flag after emulated events are finished
151
+ if (isEmulatingMouseEvents && eventType === 'mouseup') {
152
+ if (trackedTouchCount === 0) {
153
+ isEmulatingMouseEvents = false
154
+ }
155
+ return
156
+ }
157
+
158
+ const isStartEvent = isStartish(eventType) && isPrimaryPointerDown(domEvent)
159
+ const isMoveEvent = isMoveish(eventType)
160
+ const isEndEvent = isEndish(eventType)
161
+ const isScrollEvent = isScroll(eventType)
162
+ const isSelectionChangeEvent = isSelectionChange(eventType)
163
+ const responderEvent = createResponderEvent(domEvent, responderTouchHistoryStore)
164
+
165
+ /**
166
+ * Record the state of active pointers
167
+ */
168
+
169
+ if (isStartEvent || isMoveEvent || isEndEvent) {
170
+ if (domEvent.touches) {
171
+ trackedTouchCount = domEvent.touches.length
172
+ } else {
173
+ if (isStartEvent) {
174
+ trackedTouchCount = 1
175
+ } else if (isEndEvent) {
176
+ trackedTouchCount = 0
177
+ }
178
+ }
179
+ responderTouchHistoryStore.recordTouchTrack(eventType, responderEvent.nativeEvent as any)
180
+ }
181
+
182
+ /**
183
+ * Responder System logic
184
+ */
185
+
186
+ let eventPaths: any = getResponderPaths(domEvent)
187
+ let wasNegotiated = false
188
+ let wantsResponder
189
+
190
+ // If an event occured that might change the current responder...
191
+ if (isStartEvent || isMoveEvent || (isScrollEvent && trackedTouchCount > 0)) {
192
+ // If there is already a responder, prune the event paths to the lowest common ancestor
193
+ // of the existing responder and deepest target of the event.
194
+ const currentResponderIdPath = currentResponder.idPath
195
+ const eventIdPath = eventPaths.idPath
196
+
197
+ if (currentResponderIdPath != null && eventIdPath != null) {
198
+ const lowestCommonAncestor = getLowestCommonAncestor(currentResponderIdPath, eventIdPath)
199
+ if (lowestCommonAncestor != null) {
200
+ const indexOfLowestCommonAncestor = eventIdPath.indexOf(lowestCommonAncestor)
201
+ // Skip the current responder so it doesn't receive unexpected "shouldSet" events.
202
+ const index =
203
+ indexOfLowestCommonAncestor + (lowestCommonAncestor === currentResponder.id ? 1 : 0)
204
+ eventPaths = {
205
+ idPath: eventIdPath.slice(index),
206
+ nodePath: eventPaths.nodePath.slice(index),
207
+ }
208
+ } else {
209
+ eventPaths = null
210
+ }
211
+ }
212
+
213
+ if (eventPaths != null) {
214
+ // If a node wants to become the responder, attempt to transfer.
215
+ wantsResponder = findWantsResponder(eventPaths, domEvent, responderEvent)
216
+ if (wantsResponder != null) {
217
+ // Sets responder if none exists, or negotates with existing responder.
218
+ attemptTransfer(responderEvent, wantsResponder)
219
+ wasNegotiated = true
220
+ }
221
+ }
222
+ }
223
+
224
+ // If there is now a responder, invoke its callbacks for the lifecycle of the gesture.
225
+ if (currentResponder.id != null && currentResponder.node != null) {
226
+ const { id, node } = currentResponder
227
+ const {
228
+ onResponderStart,
229
+ onResponderMove,
230
+ onResponderEnd,
231
+ onResponderRelease,
232
+ onResponderTerminate,
233
+ onResponderTerminationRequest,
234
+ } = getResponderConfig(id)
235
+
236
+ responderEvent.bubbles = false
237
+ responderEvent.cancelable = false
238
+ responderEvent.currentTarget = node
239
+
240
+ // Start
241
+ if (isStartEvent) {
242
+ if (onResponderStart != null) {
243
+ responderEvent.dispatchConfig.registrationName = 'onResponderStart'
244
+ onResponderStart(responderEvent)
245
+ }
246
+ }
247
+ // Move
248
+ else if (isMoveEvent) {
249
+ if (onResponderMove != null) {
250
+ responderEvent.dispatchConfig.registrationName = 'onResponderMove'
251
+ onResponderMove(responderEvent)
252
+ }
253
+ } else {
254
+ const isTerminateEvent =
255
+ isCancelish(eventType) ||
256
+ // native context menu
257
+ eventType === 'contextmenu' ||
258
+ // window blur
259
+ (eventType === 'blur' && eventTarget === window) ||
260
+ // responder (or ancestors) blur
261
+ (eventType === 'blur' && eventTarget.contains(node) && domEvent.relatedTarget !== node) ||
262
+ // native scroll without using a pointer
263
+ (isScrollEvent && trackedTouchCount === 0) ||
264
+ // native scroll on node that is parent of the responder (allow siblings to scroll)
265
+ (isScrollEvent && eventTarget.contains(node) && eventTarget !== node) ||
266
+ // native select/selectionchange on node
267
+ (isSelectionChangeEvent && hasValidSelection(domEvent))
268
+
269
+ const isReleaseEvent =
270
+ isEndEvent && !isTerminateEvent && !hasTargetTouches(node, domEvent.touches)
271
+
272
+ // End
273
+ if (isEndEvent) {
274
+ if (onResponderEnd != null) {
275
+ responderEvent.dispatchConfig.registrationName = 'onResponderEnd'
276
+ onResponderEnd(responderEvent)
277
+ }
278
+ }
279
+ // Release
280
+ if (isReleaseEvent) {
281
+ if (onResponderRelease != null) {
282
+ responderEvent.dispatchConfig.registrationName = 'onResponderRelease'
283
+ onResponderRelease(responderEvent)
284
+ }
285
+ changeCurrentResponder(emptyResponder)
286
+ }
287
+ // Terminate
288
+ if (isTerminateEvent) {
289
+ let shouldTerminate = true
290
+
291
+ // Responders can still avoid termination but only for these events.
292
+ if (
293
+ eventType === 'contextmenu' ||
294
+ eventType === 'scroll' ||
295
+ eventType === 'selectionchange'
296
+ ) {
297
+ // Only call this function is it wasn't already called during negotiation.
298
+ if (wasNegotiated) {
299
+ shouldTerminate = false
300
+ } else if (onResponderTerminationRequest != null) {
301
+ responderEvent.dispatchConfig.registrationName = 'onResponderTerminationRequest'
302
+ if (onResponderTerminationRequest(responderEvent) === false) {
303
+ shouldTerminate = false
304
+ }
305
+ }
306
+ }
307
+
308
+ if (shouldTerminate) {
309
+ if (onResponderTerminate != null) {
310
+ responderEvent.dispatchConfig.registrationName = 'onResponderTerminate'
311
+ onResponderTerminate(responderEvent)
312
+ }
313
+ changeCurrentResponder(emptyResponder)
314
+ isEmulatingMouseEvents = false
315
+ trackedTouchCount = 0
316
+ }
317
+ }
318
+ }
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Walk the event path to/from the target node. At each node, stop and call the
324
+ * relevant "shouldSet" functions for the given event type. If any of those functions
325
+ * call "stopPropagation" on the event, stop searching for a responder.
326
+ */
327
+ function findWantsResponder(eventPaths, domEvent, responderEvent) {
328
+ const shouldSetCallbacks = shouldSetResponderEvents[domEvent.type as any] // for Flow
329
+
330
+ if (shouldSetCallbacks != null) {
331
+ const { idPath, nodePath } = eventPaths
332
+
333
+ const shouldSetCallbackCaptureName = shouldSetCallbacks[0]
334
+ const shouldSetCallbackBubbleName = shouldSetCallbacks[1]
335
+ const { bubbles } = shouldSetCallbacks[2]
336
+
337
+ const check = function (id, node, callbackName) {
338
+ const config = getResponderConfig(id)
339
+ const shouldSetCallback = config[callbackName]
340
+ if (shouldSetCallback != null) {
341
+ responderEvent.currentTarget = node
342
+ if (shouldSetCallback(responderEvent) === true) {
343
+ // Start the path from the potential responder
344
+ const prunedIdPath = idPath.slice(idPath.indexOf(id))
345
+ return { id, node, idPath: prunedIdPath }
346
+ }
347
+ }
348
+ }
349
+
350
+ // capture
351
+ for (let i = idPath.length - 1; i >= 0; i--) {
352
+ const id = idPath[i]
353
+ const node = nodePath[i]
354
+ const result = check(id, node, shouldSetCallbackCaptureName)
355
+ if (result != null) {
356
+ return result
357
+ }
358
+ if (responderEvent.isPropagationStopped() === true) {
359
+ return
360
+ }
361
+ }
362
+
363
+ // bubble
364
+ if (bubbles) {
365
+ for (let i = 0; i < idPath.length; i++) {
366
+ const id = idPath[i]
367
+ const node = nodePath[i]
368
+ const result = check(id, node, shouldSetCallbackBubbleName)
369
+ if (result != null) {
370
+ return result
371
+ }
372
+ if (responderEvent.isPropagationStopped() === true) {
373
+ return
374
+ }
375
+ }
376
+ } else {
377
+ const id = idPath[0]
378
+ const node = nodePath[0]
379
+ const target = domEvent.target
380
+ if (target === node) {
381
+ return check(id, node, shouldSetCallbackBubbleName)
382
+ }
383
+ }
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Attempt to transfer the responder.
389
+ */
390
+ function attemptTransfer(responderEvent: ResponderEvent, wantsResponder: ActiveResponderInstance) {
391
+ const { id: currentId, node: currentNode } = currentResponder
392
+ const { id, node } = wantsResponder
393
+
394
+ const { onResponderGrant, onResponderReject } = getResponderConfig(id)
395
+
396
+ responderEvent.bubbles = false
397
+ responderEvent.cancelable = false
398
+ responderEvent.currentTarget = node
399
+
400
+ // Set responder
401
+ if (currentId == null) {
402
+ if (onResponderGrant != null) {
403
+ responderEvent.currentTarget = node
404
+ responderEvent.dispatchConfig.registrationName = 'onResponderGrant'
405
+ onResponderGrant(responderEvent)
406
+ }
407
+ changeCurrentResponder(wantsResponder)
408
+ }
409
+ // Negotiate with current responder
410
+ else {
411
+ const { onResponderTerminate, onResponderTerminationRequest } = getResponderConfig(currentId)
412
+
413
+ let allowTransfer = true
414
+ if (onResponderTerminationRequest != null) {
415
+ responderEvent.currentTarget = currentNode
416
+ responderEvent.dispatchConfig.registrationName = 'onResponderTerminationRequest'
417
+ if (onResponderTerminationRequest(responderEvent) === false) {
418
+ allowTransfer = false
419
+ }
420
+ }
421
+
422
+ if (allowTransfer) {
423
+ // Terminate existing responder
424
+ if (onResponderTerminate != null) {
425
+ responderEvent.currentTarget = currentNode
426
+ responderEvent.dispatchConfig.registrationName = 'onResponderTerminate'
427
+ onResponderTerminate(responderEvent)
428
+ }
429
+ // Grant next responder
430
+ if (onResponderGrant != null) {
431
+ responderEvent.currentTarget = node
432
+ responderEvent.dispatchConfig.registrationName = 'onResponderGrant'
433
+ onResponderGrant(responderEvent)
434
+ }
435
+ changeCurrentResponder(wantsResponder)
436
+ } else {
437
+ // Reject responder request
438
+ if (onResponderReject != null) {
439
+ responderEvent.currentTarget = node
440
+ responderEvent.dispatchConfig.registrationName = 'onResponderReject'
441
+ onResponderReject(responderEvent)
442
+ }
443
+ }
444
+ }
445
+ }
446
+
447
+ /* ------------ PUBLIC API ------------ */
448
+
449
+ /**
450
+ * Attach Listeners
451
+ *
452
+ * Use native events as ReactDOM doesn't have a non-plugin API to implement
453
+ * this system.
454
+ */
455
+ const documentEventsCapturePhase = ['blur', 'scroll']
456
+ const documentEventsBubblePhase = [
457
+ // mouse
458
+ 'mousedown',
459
+ 'mousemove',
460
+ 'mouseup',
461
+ 'dragstart',
462
+ // touch
463
+ 'touchstart',
464
+ 'touchmove',
465
+ 'touchend',
466
+ 'touchcancel',
467
+ // other
468
+ 'contextmenu',
469
+ 'select',
470
+ 'selectionchange',
471
+ ]
472
+ export function attachListeners() {
473
+ if (canUseDOM && window['__reactResponderSystemActive'] == null) {
474
+ window.addEventListener('blur', eventListener)
475
+ documentEventsBubblePhase.forEach((eventType) => {
476
+ document.addEventListener(eventType, eventListener)
477
+ })
478
+ documentEventsCapturePhase.forEach((eventType) => {
479
+ document.addEventListener(eventType, eventListener, true)
480
+ })
481
+ window['__reactResponderSystemActive'] = true
482
+ }
483
+ }
484
+
485
+ /**
486
+ * Register a node with the ResponderSystem.
487
+ */
488
+ export function addNode(id: ResponderId, node: any, config: ResponderConfig) {
489
+ setResponderId(node, id)
490
+ responderListenersMap.set(id, config)
491
+ }
492
+
493
+ /**
494
+ * Unregister a node with the ResponderSystem.
495
+ */
496
+ export function removeNode(id: ResponderId) {
497
+ if (currentResponder.id === id) {
498
+ terminateResponder()
499
+ }
500
+ if (responderListenersMap.has(id)) {
501
+ responderListenersMap.delete(id)
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Allow the current responder to be terminated from within components to support
507
+ * more complex requirements, such as use with other React libraries for working
508
+ * with scroll views, input views, etc.
509
+ */
510
+ export function terminateResponder() {
511
+ const { id, node } = currentResponder
512
+ if (id != null && node != null) {
513
+ const { onResponderTerminate } = getResponderConfig(id)
514
+ if (onResponderTerminate != null) {
515
+ const event = createResponderEvent({}, responderTouchHistoryStore)
516
+ event.currentTarget = node
517
+ onResponderTerminate(event)
518
+ }
519
+ changeCurrentResponder(emptyResponder)
520
+ }
521
+ isEmulatingMouseEvents = false
522
+ trackedTouchCount = 0
523
+ }
524
+
525
+ /**
526
+ * Allow unit tests to inspect the current responder in the system.
527
+ * FOR TESTING ONLY.
528
+ */
529
+ export function getResponderNode(): any {
530
+ return currentResponder.node
531
+ }