1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | import {disableTextSelection, restoreTextSelection} from './textSelection';
|
19 | import {focusWithoutScrolling, mergeProps, useGlobalListeners, useSyncRef} from '@react-aria/utils';
|
20 | import {HTMLAttributes, RefObject, useContext, useEffect, useMemo, useRef, useState} from 'react';
|
21 | import {isVirtualClick} from './utils';
|
22 | import {PointerType, PressEvents} from '@react-types/shared';
|
23 | import {PressResponderContext} from './context';
|
24 |
|
25 | export interface PressProps extends PressEvents {
|
26 |
|
27 | isPressed?: boolean,
|
28 |
|
29 | isDisabled?: boolean,
|
30 |
|
31 | preventFocusOnPress?: boolean,
|
32 | |
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 | shouldCancelOnPointerExit?: boolean,
|
39 |
|
40 | allowTextSelectionOnPress?: boolean
|
41 | }
|
42 |
|
43 | export interface PressHookProps extends PressProps {
|
44 |
|
45 | ref?: RefObject<HTMLElement>
|
46 | }
|
47 |
|
48 | interface PressState {
|
49 | isPressed: boolean,
|
50 | ignoreEmulatedMouseEvents: boolean,
|
51 | ignoreClickAfterPress: boolean,
|
52 | didFirePressStart: boolean,
|
53 | activePointerId: any,
|
54 | target: HTMLElement | null,
|
55 | isOverTarget: boolean,
|
56 | pointerType: PointerType,
|
57 | userSelect?: string
|
58 | }
|
59 |
|
60 | interface EventBase {
|
61 | currentTarget: EventTarget,
|
62 | shiftKey: boolean,
|
63 | ctrlKey: boolean,
|
64 | metaKey: boolean,
|
65 | altKey: boolean
|
66 | }
|
67 |
|
68 | export interface PressResult {
|
69 |
|
70 | isPressed: boolean,
|
71 |
|
72 | pressProps: HTMLAttributes<HTMLElement>
|
73 | }
|
74 |
|
75 | function usePressResponderContext(props: PressHookProps): PressHookProps {
|
76 |
|
77 | let context = useContext(PressResponderContext);
|
78 | if (context) {
|
79 | let {register, ...contextProps} = context;
|
80 | props = mergeProps(contextProps, props) as PressHookProps;
|
81 | register();
|
82 | }
|
83 | useSyncRef(context, props.ref);
|
84 |
|
85 | return props;
|
86 | }
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 | export function usePress(props: PressHookProps): PressResult {
|
94 | let {
|
95 | onPress,
|
96 | onPressChange,
|
97 | onPressStart,
|
98 | onPressEnd,
|
99 | onPressUp,
|
100 | isDisabled,
|
101 | isPressed: isPressedProp,
|
102 | preventFocusOnPress,
|
103 | shouldCancelOnPointerExit,
|
104 | allowTextSelectionOnPress,
|
105 |
|
106 | ref: _,
|
107 | ...domProps
|
108 | } = usePressResponderContext(props);
|
109 | let propsRef = useRef<PressHookProps>(null);
|
110 | propsRef.current = {onPress, onPressChange, onPressStart, onPressEnd, onPressUp, isDisabled, shouldCancelOnPointerExit};
|
111 |
|
112 | let [isPressed, setPressed] = useState(false);
|
113 | let ref = useRef<PressState>({
|
114 | isPressed: false,
|
115 | ignoreEmulatedMouseEvents: false,
|
116 | ignoreClickAfterPress: false,
|
117 | didFirePressStart: false,
|
118 | activePointerId: null,
|
119 | target: null,
|
120 | isOverTarget: false,
|
121 | pointerType: null
|
122 | });
|
123 |
|
124 | let {addGlobalListener, removeAllGlobalListeners} = useGlobalListeners();
|
125 |
|
126 | let pressProps = useMemo(() => {
|
127 | let state = ref.current;
|
128 | let triggerPressStart = (originalEvent: EventBase, pointerType: PointerType) => {
|
129 | let {onPressStart, onPressChange, isDisabled} = propsRef.current;
|
130 | if (isDisabled || state.didFirePressStart) {
|
131 | return;
|
132 | }
|
133 |
|
134 | if (onPressStart) {
|
135 | onPressStart({
|
136 | type: 'pressstart',
|
137 | pointerType,
|
138 | target: originalEvent.currentTarget as HTMLElement,
|
139 | shiftKey: originalEvent.shiftKey,
|
140 | metaKey: originalEvent.metaKey,
|
141 | ctrlKey: originalEvent.ctrlKey,
|
142 | altKey: originalEvent.altKey
|
143 | });
|
144 | }
|
145 |
|
146 | if (onPressChange) {
|
147 | onPressChange(true);
|
148 | }
|
149 |
|
150 | state.didFirePressStart = true;
|
151 | setPressed(true);
|
152 | };
|
153 |
|
154 | let triggerPressEnd = (originalEvent: EventBase, pointerType: PointerType, wasPressed = true) => {
|
155 | let {onPressEnd, onPressChange, onPress, isDisabled} = propsRef.current;
|
156 | if (!state.didFirePressStart) {
|
157 | return;
|
158 | }
|
159 |
|
160 | state.ignoreClickAfterPress = true;
|
161 | state.didFirePressStart = false;
|
162 |
|
163 | if (onPressEnd) {
|
164 | onPressEnd({
|
165 | type: 'pressend',
|
166 | pointerType,
|
167 | target: originalEvent.currentTarget as HTMLElement,
|
168 | shiftKey: originalEvent.shiftKey,
|
169 | metaKey: originalEvent.metaKey,
|
170 | ctrlKey: originalEvent.ctrlKey,
|
171 | altKey: originalEvent.altKey
|
172 | });
|
173 | }
|
174 |
|
175 | if (onPressChange) {
|
176 | onPressChange(false);
|
177 | }
|
178 |
|
179 | setPressed(false);
|
180 |
|
181 | if (onPress && wasPressed && !isDisabled) {
|
182 | onPress({
|
183 | type: 'press',
|
184 | pointerType,
|
185 | target: originalEvent.currentTarget as HTMLElement,
|
186 | shiftKey: originalEvent.shiftKey,
|
187 | metaKey: originalEvent.metaKey,
|
188 | ctrlKey: originalEvent.ctrlKey,
|
189 | altKey: originalEvent.altKey
|
190 | });
|
191 | }
|
192 | };
|
193 |
|
194 | let triggerPressUp = (originalEvent: EventBase, pointerType: PointerType) => {
|
195 | let {onPressUp, isDisabled} = propsRef.current;
|
196 | if (isDisabled) {
|
197 | return;
|
198 | }
|
199 |
|
200 | if (onPressUp) {
|
201 | onPressUp({
|
202 | type: 'pressup',
|
203 | pointerType,
|
204 | target: originalEvent.currentTarget as HTMLElement,
|
205 | shiftKey: originalEvent.shiftKey,
|
206 | metaKey: originalEvent.metaKey,
|
207 | ctrlKey: originalEvent.ctrlKey,
|
208 | altKey: originalEvent.altKey
|
209 | });
|
210 | }
|
211 | };
|
212 |
|
213 | let cancel = (e: EventBase) => {
|
214 | if (state.isPressed) {
|
215 | if (state.isOverTarget) {
|
216 | triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
|
217 | }
|
218 | state.isPressed = false;
|
219 | state.isOverTarget = false;
|
220 | state.activePointerId = null;
|
221 | state.pointerType = null;
|
222 | removeAllGlobalListeners();
|
223 | if (!allowTextSelectionOnPress) {
|
224 | restoreTextSelection(state.target);
|
225 | }
|
226 | }
|
227 | };
|
228 |
|
229 | let pressProps: HTMLAttributes<HTMLElement> = {
|
230 | onKeyDown(e) {
|
231 | if (isValidKeyboardEvent(e.nativeEvent) && e.currentTarget.contains(e.target as HTMLElement)) {
|
232 | if (shouldPreventDefaultKeyboard(e.target as Element)) {
|
233 | e.preventDefault();
|
234 | }
|
235 | e.stopPropagation();
|
236 |
|
237 |
|
238 |
|
239 |
|
240 | if (!state.isPressed && !e.repeat) {
|
241 | state.target = e.currentTarget as HTMLElement;
|
242 | state.isPressed = true;
|
243 | triggerPressStart(e, 'keyboard');
|
244 |
|
245 |
|
246 |
|
247 | addGlobalListener(document, 'keyup', onKeyUp, false);
|
248 | }
|
249 | }
|
250 | },
|
251 | onKeyUp(e) {
|
252 | if (isValidKeyboardEvent(e.nativeEvent) && !e.repeat && e.currentTarget.contains(e.target as HTMLElement)) {
|
253 | triggerPressUp(createEvent(state.target, e), 'keyboard');
|
254 | }
|
255 | },
|
256 | onClick(e) {
|
257 | if (e && !e.currentTarget.contains(e.target as HTMLElement)) {
|
258 | return;
|
259 | }
|
260 |
|
261 | if (e && e.button === 0) {
|
262 | e.stopPropagation();
|
263 | if (isDisabled) {
|
264 | e.preventDefault();
|
265 | }
|
266 |
|
267 |
|
268 |
|
269 | if (!state.ignoreClickAfterPress && !state.ignoreEmulatedMouseEvents && (state.pointerType === 'virtual' || isVirtualClick(e.nativeEvent))) {
|
270 |
|
271 | if (!isDisabled && !preventFocusOnPress) {
|
272 | focusWithoutScrolling(e.currentTarget);
|
273 | }
|
274 |
|
275 | triggerPressStart(e, 'virtual');
|
276 | triggerPressUp(e, 'virtual');
|
277 | triggerPressEnd(e, 'virtual');
|
278 | }
|
279 |
|
280 | state.ignoreEmulatedMouseEvents = false;
|
281 | state.ignoreClickAfterPress = false;
|
282 | }
|
283 | }
|
284 | };
|
285 |
|
286 | let onKeyUp = (e: KeyboardEvent) => {
|
287 | if (state.isPressed && isValidKeyboardEvent(e)) {
|
288 | if (shouldPreventDefaultKeyboard(e.target as Element)) {
|
289 | e.preventDefault();
|
290 | }
|
291 | e.stopPropagation();
|
292 |
|
293 | state.isPressed = false;
|
294 | let target = e.target as HTMLElement;
|
295 | triggerPressEnd(createEvent(state.target, e), 'keyboard', state.target.contains(target));
|
296 | removeAllGlobalListeners();
|
297 |
|
298 |
|
299 |
|
300 | if (state.target.contains(target) && isHTMLAnchorLink(state.target) || state.target.getAttribute('role') === 'link') {
|
301 | state.target.click();
|
302 | }
|
303 | }
|
304 | };
|
305 |
|
306 | if (typeof PointerEvent !== 'undefined') {
|
307 | pressProps.onPointerDown = (e) => {
|
308 |
|
309 | if (e.button !== 0 || !e.currentTarget.contains(e.target as HTMLElement)) {
|
310 | return;
|
311 | }
|
312 |
|
313 |
|
314 |
|
315 |
|
316 |
|
317 | if (isVirtualPointerEvent(e.nativeEvent)) {
|
318 | state.pointerType = 'virtual';
|
319 | return;
|
320 | }
|
321 |
|
322 |
|
323 |
|
324 | if (shouldPreventDefault(e.currentTarget as HTMLElement)) {
|
325 | e.preventDefault();
|
326 | }
|
327 |
|
328 | state.pointerType = e.pointerType;
|
329 |
|
330 | e.stopPropagation();
|
331 | if (!state.isPressed) {
|
332 | state.isPressed = true;
|
333 | state.isOverTarget = true;
|
334 | state.activePointerId = e.pointerId;
|
335 | state.target = e.currentTarget;
|
336 |
|
337 | if (!isDisabled && !preventFocusOnPress) {
|
338 | focusWithoutScrolling(e.currentTarget);
|
339 | }
|
340 |
|
341 | if (!allowTextSelectionOnPress) {
|
342 | disableTextSelection(state.target);
|
343 | }
|
344 |
|
345 | triggerPressStart(e, state.pointerType);
|
346 |
|
347 | addGlobalListener(document, 'pointermove', onPointerMove, false);
|
348 | addGlobalListener(document, 'pointerup', onPointerUp, false);
|
349 | addGlobalListener(document, 'pointercancel', onPointerCancel, false);
|
350 | }
|
351 | };
|
352 |
|
353 | pressProps.onMouseDown = (e) => {
|
354 | if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
355 | return;
|
356 | }
|
357 |
|
358 | if (e.button === 0) {
|
359 |
|
360 |
|
361 |
|
362 | if (shouldPreventDefault(e.currentTarget as HTMLElement)) {
|
363 | e.preventDefault();
|
364 | }
|
365 |
|
366 | e.stopPropagation();
|
367 | }
|
368 | };
|
369 |
|
370 | pressProps.onPointerUp = (e) => {
|
371 |
|
372 | if (!e.currentTarget.contains(e.target as HTMLElement) || state.pointerType === 'virtual') {
|
373 | return;
|
374 | }
|
375 |
|
376 |
|
377 |
|
378 |
|
379 | if (e.button === 0 && isOverTarget(e, e.currentTarget)) {
|
380 | triggerPressUp(e, state.pointerType || e.pointerType);
|
381 | }
|
382 | };
|
383 |
|
384 |
|
385 |
|
386 |
|
387 | let onPointerMove = (e: PointerEvent) => {
|
388 | if (e.pointerId !== state.activePointerId) {
|
389 | return;
|
390 | }
|
391 |
|
392 | if (isOverTarget(e, state.target)) {
|
393 | if (!state.isOverTarget) {
|
394 | state.isOverTarget = true;
|
395 | triggerPressStart(createEvent(state.target, e), state.pointerType);
|
396 | }
|
397 | } else if (state.isOverTarget) {
|
398 | state.isOverTarget = false;
|
399 | triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
|
400 | if (propsRef.current.shouldCancelOnPointerExit) {
|
401 | cancel(e);
|
402 | }
|
403 | }
|
404 | };
|
405 |
|
406 | let onPointerUp = (e: PointerEvent) => {
|
407 | if (e.pointerId === state.activePointerId && state.isPressed && e.button === 0) {
|
408 | if (isOverTarget(e, state.target)) {
|
409 | triggerPressEnd(createEvent(state.target, e), state.pointerType);
|
410 | } else if (state.isOverTarget) {
|
411 | triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
|
412 | }
|
413 |
|
414 | state.isPressed = false;
|
415 | state.isOverTarget = false;
|
416 | state.activePointerId = null;
|
417 | state.pointerType = null;
|
418 | removeAllGlobalListeners();
|
419 | if (!allowTextSelectionOnPress) {
|
420 | restoreTextSelection(state.target);
|
421 | }
|
422 | }
|
423 | };
|
424 |
|
425 | let onPointerCancel = (e: PointerEvent) => {
|
426 | cancel(e);
|
427 | };
|
428 |
|
429 | pressProps.onDragStart = (e) => {
|
430 | if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
431 | return;
|
432 | }
|
433 |
|
434 |
|
435 | cancel(e);
|
436 | };
|
437 | } else {
|
438 | pressProps.onMouseDown = (e) => {
|
439 |
|
440 | if (e.button !== 0 || !e.currentTarget.contains(e.target as HTMLElement)) {
|
441 | return;
|
442 | }
|
443 |
|
444 |
|
445 |
|
446 | if (shouldPreventDefault(e.currentTarget as HTMLElement)) {
|
447 | e.preventDefault();
|
448 | }
|
449 |
|
450 | e.stopPropagation();
|
451 | if (state.ignoreEmulatedMouseEvents) {
|
452 | return;
|
453 | }
|
454 |
|
455 | state.isPressed = true;
|
456 | state.isOverTarget = true;
|
457 | state.target = e.currentTarget;
|
458 | state.pointerType = isVirtualClick(e.nativeEvent) ? 'virtual' : 'mouse';
|
459 |
|
460 | if (!isDisabled && !preventFocusOnPress) {
|
461 | focusWithoutScrolling(e.currentTarget);
|
462 | }
|
463 |
|
464 | triggerPressStart(e, state.pointerType);
|
465 |
|
466 | addGlobalListener(document, 'mouseup', onMouseUp, false);
|
467 | };
|
468 |
|
469 | pressProps.onMouseEnter = (e) => {
|
470 | if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
471 | return;
|
472 | }
|
473 |
|
474 | e.stopPropagation();
|
475 | if (state.isPressed && !state.ignoreEmulatedMouseEvents) {
|
476 | state.isOverTarget = true;
|
477 | triggerPressStart(e, state.pointerType);
|
478 | }
|
479 | };
|
480 |
|
481 | pressProps.onMouseLeave = (e) => {
|
482 | if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
483 | return;
|
484 | }
|
485 |
|
486 | e.stopPropagation();
|
487 | if (state.isPressed && !state.ignoreEmulatedMouseEvents) {
|
488 | state.isOverTarget = false;
|
489 | triggerPressEnd(e, state.pointerType, false);
|
490 | if (propsRef.current.shouldCancelOnPointerExit) {
|
491 | cancel(e);
|
492 | }
|
493 | }
|
494 | };
|
495 |
|
496 | pressProps.onMouseUp = (e) => {
|
497 | if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
498 | return;
|
499 | }
|
500 |
|
501 | if (!state.ignoreEmulatedMouseEvents && e.button === 0) {
|
502 | triggerPressUp(e, state.pointerType);
|
503 | }
|
504 | };
|
505 |
|
506 | let onMouseUp = (e: MouseEvent) => {
|
507 |
|
508 | if (e.button !== 0) {
|
509 | return;
|
510 | }
|
511 |
|
512 | state.isPressed = false;
|
513 | removeAllGlobalListeners();
|
514 |
|
515 | if (state.ignoreEmulatedMouseEvents) {
|
516 | state.ignoreEmulatedMouseEvents = false;
|
517 | return;
|
518 | }
|
519 |
|
520 | if (isOverTarget(e, state.target)) {
|
521 | triggerPressEnd(createEvent(state.target, e), state.pointerType);
|
522 | } else if (state.isOverTarget) {
|
523 | triggerPressEnd(createEvent(state.target, e), state.pointerType, false);
|
524 | }
|
525 |
|
526 | state.isOverTarget = false;
|
527 | };
|
528 |
|
529 | pressProps.onTouchStart = (e) => {
|
530 | if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
531 | return;
|
532 | }
|
533 |
|
534 | e.stopPropagation();
|
535 | let touch = getTouchFromEvent(e.nativeEvent);
|
536 | if (!touch) {
|
537 | return;
|
538 | }
|
539 | state.activePointerId = touch.identifier;
|
540 | state.ignoreEmulatedMouseEvents = true;
|
541 | state.isOverTarget = true;
|
542 | state.isPressed = true;
|
543 | state.target = e.currentTarget;
|
544 | state.pointerType = 'touch';
|
545 |
|
546 |
|
547 |
|
548 | if (!isDisabled && !preventFocusOnPress) {
|
549 | focusWithoutScrolling(e.currentTarget);
|
550 | }
|
551 |
|
552 | if (!allowTextSelectionOnPress) {
|
553 | disableTextSelection(state.target);
|
554 | }
|
555 |
|
556 | triggerPressStart(e, state.pointerType);
|
557 |
|
558 | addGlobalListener(window, 'scroll', onScroll, true);
|
559 | };
|
560 |
|
561 | pressProps.onTouchMove = (e) => {
|
562 | if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
563 | return;
|
564 | }
|
565 |
|
566 | e.stopPropagation();
|
567 | if (!state.isPressed) {
|
568 | return;
|
569 | }
|
570 |
|
571 | let touch = getTouchById(e.nativeEvent, state.activePointerId);
|
572 | if (touch && isOverTarget(touch, e.currentTarget)) {
|
573 | if (!state.isOverTarget) {
|
574 | state.isOverTarget = true;
|
575 | triggerPressStart(e, state.pointerType);
|
576 | }
|
577 | } else if (state.isOverTarget) {
|
578 | state.isOverTarget = false;
|
579 | triggerPressEnd(e, state.pointerType, false);
|
580 | if (propsRef.current.shouldCancelOnPointerExit) {
|
581 | cancel(e);
|
582 | }
|
583 | }
|
584 | };
|
585 |
|
586 | pressProps.onTouchEnd = (e) => {
|
587 | if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
588 | return;
|
589 | }
|
590 |
|
591 | e.stopPropagation();
|
592 | if (!state.isPressed) {
|
593 | return;
|
594 | }
|
595 |
|
596 | let touch = getTouchById(e.nativeEvent, state.activePointerId);
|
597 | if (touch && isOverTarget(touch, e.currentTarget)) {
|
598 | triggerPressUp(e, state.pointerType);
|
599 | triggerPressEnd(e, state.pointerType);
|
600 | } else if (state.isOverTarget) {
|
601 | triggerPressEnd(e, state.pointerType, false);
|
602 | }
|
603 |
|
604 | state.isPressed = false;
|
605 | state.activePointerId = null;
|
606 | state.isOverTarget = false;
|
607 | state.ignoreEmulatedMouseEvents = true;
|
608 | if (!allowTextSelectionOnPress) {
|
609 | restoreTextSelection(state.target);
|
610 | }
|
611 | removeAllGlobalListeners();
|
612 | };
|
613 |
|
614 | pressProps.onTouchCancel = (e) => {
|
615 | if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
616 | return;
|
617 | }
|
618 |
|
619 | e.stopPropagation();
|
620 | if (state.isPressed) {
|
621 | cancel(e);
|
622 | }
|
623 | };
|
624 |
|
625 | let onScroll = (e: Event) => {
|
626 | if (state.isPressed && (e.target as HTMLElement).contains(state.target)) {
|
627 | cancel({
|
628 | currentTarget: state.target,
|
629 | shiftKey: false,
|
630 | ctrlKey: false,
|
631 | metaKey: false,
|
632 | altKey: false
|
633 | });
|
634 | }
|
635 | };
|
636 |
|
637 | pressProps.onDragStart = (e) => {
|
638 | if (!e.currentTarget.contains(e.target as HTMLElement)) {
|
639 | return;
|
640 | }
|
641 |
|
642 | cancel(e);
|
643 | };
|
644 | }
|
645 |
|
646 | return pressProps;
|
647 | }, [addGlobalListener, isDisabled, preventFocusOnPress, removeAllGlobalListeners, allowTextSelectionOnPress]);
|
648 |
|
649 |
|
650 |
|
651 | useEffect(() => {
|
652 | return () => {
|
653 | if (!allowTextSelectionOnPress) {
|
654 | restoreTextSelection(ref.current.target);
|
655 | }
|
656 | };
|
657 | }, [allowTextSelectionOnPress]);
|
658 |
|
659 | return {
|
660 | isPressed: isPressedProp || isPressed,
|
661 | pressProps: mergeProps(domProps, pressProps)
|
662 | };
|
663 | }
|
664 |
|
665 | function isHTMLAnchorLink(target: HTMLElement): boolean {
|
666 | return target.tagName === 'A' && target.hasAttribute('href');
|
667 | }
|
668 |
|
669 | function isValidKeyboardEvent(event: KeyboardEvent): boolean {
|
670 | const {key, code, target} = event;
|
671 | const element = target as HTMLElement;
|
672 | const {tagName, isContentEditable} = element;
|
673 | const role = element.getAttribute('role');
|
674 |
|
675 |
|
676 | return (
|
677 | (key === 'Enter' || key === ' ' || key === 'Spacebar' || code === 'Space') &&
|
678 | (tagName !== 'INPUT' &&
|
679 | tagName !== 'TEXTAREA' &&
|
680 | isContentEditable !== true) &&
|
681 |
|
682 |
|
683 | (!isHTMLAnchorLink(element) || (role === 'button' && key !== 'Enter')) &&
|
684 |
|
685 | !(role === 'link' && key !== 'Enter')
|
686 | );
|
687 | }
|
688 |
|
689 | function getTouchFromEvent(event: TouchEvent): Touch | null {
|
690 | const {targetTouches} = event;
|
691 | if (targetTouches.length > 0) {
|
692 | return targetTouches[0];
|
693 | }
|
694 | return null;
|
695 | }
|
696 |
|
697 | function getTouchById(
|
698 | event: TouchEvent,
|
699 | pointerId: null | number
|
700 | ): null | Touch {
|
701 | const changedTouches = event.changedTouches;
|
702 | for (let i = 0; i < changedTouches.length; i++) {
|
703 | const touch = changedTouches[i];
|
704 | if (touch.identifier === pointerId) {
|
705 | return touch;
|
706 | }
|
707 | }
|
708 | return null;
|
709 | }
|
710 |
|
711 | function createEvent(target: HTMLElement, e: EventBase): EventBase {
|
712 | return {
|
713 | currentTarget: target,
|
714 | shiftKey: e.shiftKey,
|
715 | ctrlKey: e.ctrlKey,
|
716 | metaKey: e.metaKey,
|
717 | altKey: e.altKey
|
718 | };
|
719 | }
|
720 |
|
721 | interface Rect {
|
722 | top: number,
|
723 | right: number,
|
724 | bottom: number,
|
725 | left: number
|
726 | }
|
727 |
|
728 | interface EventPoint {
|
729 | clientX: number,
|
730 | clientY: number,
|
731 | width?: number,
|
732 | height?: number,
|
733 | radiusX?: number,
|
734 | radiusY?: number
|
735 | }
|
736 |
|
737 | function getPointClientRect(point: EventPoint): Rect {
|
738 | let offsetX = (point.width / 2) || point.radiusX || 0;
|
739 | let offsetY = (point.height / 2) || point.radiusY || 0;
|
740 |
|
741 | return {
|
742 | top: point.clientY - offsetY,
|
743 | right: point.clientX + offsetX,
|
744 | bottom: point.clientY + offsetY,
|
745 | left: point.clientX - offsetX
|
746 | };
|
747 | }
|
748 |
|
749 | function areRectanglesOverlapping(a: Rect, b: Rect) {
|
750 |
|
751 | if (a.left > b.right || b.left > a.right) {
|
752 | return false;
|
753 | }
|
754 |
|
755 | if (a.top > b.bottom || b.top > a.bottom) {
|
756 | return false;
|
757 | }
|
758 | return true;
|
759 | }
|
760 |
|
761 | function isOverTarget(point: EventPoint, target: HTMLElement) {
|
762 | let rect = target.getBoundingClientRect();
|
763 | let pointRect = getPointClientRect(point);
|
764 | return areRectanglesOverlapping(rect, pointRect);
|
765 | }
|
766 |
|
767 | function shouldPreventDefault(target: HTMLElement) {
|
768 |
|
769 | return !target.draggable;
|
770 | }
|
771 |
|
772 | function shouldPreventDefaultKeyboard(target: Element) {
|
773 | return !((target.tagName === 'INPUT' || target.tagName === 'BUTTON') && (target as HTMLButtonElement | HTMLInputElement).type === 'submit');
|
774 | }
|
775 |
|
776 | function isVirtualPointerEvent(event: PointerEvent) {
|
777 |
|
778 |
|
779 |
|
780 |
|
781 |
|
782 | return (
|
783 | (event.width === 0 && event.height === 0) ||
|
784 | (event.width === 1 &&
|
785 | event.height === 1 &&
|
786 | event.pressure === 0 &&
|
787 | event.detail === 0
|
788 | )
|
789 | );
|
790 | }
|