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