UNPKG

26 kBPlain TextView Raw
1/*
2 * Copyright 2020 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12
13// Portions of the code in this file are based on code from react.
14// Original licensing for the following can be found in the
15// NOTICE file in the root directory of this source tree.
16// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions
17
18import {disableTextSelection, restoreTextSelection} from './textSelection';
19import {DOMAttributes, FocusableElement, PointerType, PressEvents} from '@react-types/shared';
20import {focusWithoutScrolling, isVirtualClick, isVirtualPointerEvent, mergeProps, useGlobalListeners, useSyncRef} from '@react-aria/utils';
21import {PressResponderContext} from './context';
22import {RefObject, useContext, useEffect, useMemo, useRef, useState} from 'react';
23
24export interface PressProps extends PressEvents {
25 /** Whether the target is in a controlled press state (e.g. an overlay it triggers is open). */
26 isPressed?: boolean,
27 /** Whether the press events should be disabled. */
28 isDisabled?: boolean,
29 /** Whether the target should not receive focus on press. */
30 preventFocusOnPress?: boolean,
31 /**
32 * Whether press events should be canceled when the pointer leaves the target while pressed.
33 * By default, this is `false`, which means if the pointer returns back over the target while
34 * still pressed, onPressStart will be fired again. If set to `true`, the press is canceled
35 * when the pointer leaves the target and onPressStart will not be fired if the pointer returns.
36 */
37 shouldCancelOnPointerExit?: boolean,
38 /** Whether text selection should be enabled on the pressable element. */
39 allowTextSelectionOnPress?: boolean
40}
41
42export interface PressHookProps extends PressProps {
43 /** A ref to the target element. */
44 ref?: RefObject<Element>
45}
46
47interface 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
59interface EventBase {
60 currentTarget: EventTarget,
61 shiftKey: boolean,
62 ctrlKey: boolean,
63 metaKey: boolean,
64 altKey: boolean
65}
66
67export interface PressResult {
68 /** Whether the target is currently pressed. */
69 isPressed: boolean,
70 /** Props to spread on the target element. */
71 pressProps: DOMAttributes
72}
73
74function usePressResponderContext(props: PressHookProps): PressHookProps {
75 // Consume context from <PressResponder> and merge with props.
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 * Handles press interactions across mouse, touch, keyboard, and screen readers.
89 * It normalizes behavior across browsers and platforms, and handles many nuances
90 * of dealing with pointer and keyboard events.
91 */
92export 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 // eslint-disable-next-line @typescript-eslint/no-unused-vars
105 ref: _, // Removing `ref` from `domProps` because TypeScript is dumb
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 // If the event is repeating, it may have started on a different element
237 // after which focus moved to the current element. Ignore these events and
238 // only handle the first key down event.
239 if (!state.isPressed && !e.repeat) {
240 state.target = e.currentTarget;
241 state.isPressed = true;
242 triggerPressStart(e, 'keyboard');
243
244 // Focus may move before the key up event, so register the event on the document
245 // instead of the same element where the key down event occurred.
246 addGlobalListener(document, 'keyup', onKeyUp, false);
247 }
248 } else if (e.key === 'Enter' && isHTMLAnchorLink(e.currentTarget)) {
249 // If the target is a link, we won't have handled this above because we want the default
250 // browser behavior to open the link when pressing Enter. But we still need to prevent
251 // default so that elements above do not also handle it (e.g. table row).
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 // If triggered from a screen reader or by using element.click(),
272 // trigger as if it were a keyboard click.
273 if (!state.ignoreClickAfterPress && !state.ignoreEmulatedMouseEvents && (state.pointerType === 'virtual' || isVirtualClick(e.nativeEvent))) {
274 // Ensure the element receives focus (VoiceOver on iOS does not do this)
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 // If the target is a link, trigger the click method to open the URL,
303 // but defer triggering pressEnd until onClick event handler.
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 // Only handle left clicks, and ignore events that bubbled through portals.
313 if (e.button !== 0 || !e.currentTarget.contains(e.target as Element)) {
314 return;
315 }
316
317 // iOS safari fires pointer events from VoiceOver with incorrect coordinates/target.
318 // Ignore and let the onClick handler take care of it instead.
319 // https://bugs.webkit.org/show_bug.cgi?id=222627
320 // https://bugs.webkit.org/show_bug.cgi?id=223202
321 if (isVirtualPointerEvent(e.nativeEvent)) {
322 state.pointerType = 'virtual';
323 return;
324 }
325
326 // Due to browser inconsistencies, especially on mobile browsers, we prevent
327 // default on pointer down and handle focusing the pressable element ourselves.
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 // Chrome and Firefox on touch Windows devices require mouse down events
364 // to be canceled in addition to pointer events, or an extra asynchronous
365 // focus event will be fired.
366 if (shouldPreventDefault(e.currentTarget as Element)) {
367 e.preventDefault();
368 }
369
370 e.stopPropagation();
371 }
372 };
373
374 pressProps.onPointerUp = (e) => {
375 // iOS fires pointerup with zero width and height, so check the pointerType recorded during pointerdown.
376 if (!e.currentTarget.contains(e.target as Element) || state.pointerType === 'virtual') {
377 return;
378 }
379
380 // Only handle left clicks
381 // Safari on iOS sometimes fires pointerup events, even
382 // when the touch isn't over the target, so double check.
383 if (e.button === 0 && isOverTarget(e, e.currentTarget)) {
384 triggerPressUp(e, state.pointerType || e.pointerType);
385 }
386 };
387
388 // Safari on iOS < 13.2 does not implement pointerenter/pointerleave events correctly.
389 // Use pointer move events instead to implement our own hit testing.
390 // See https://bugs.webkit.org/show_bug.cgi?id=199803
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 // Safari does not call onPointerCancel when a drag starts, whereas Chrome and Firefox do.
439 cancel(e);
440 };
441 } else {
442 pressProps.onMouseDown = (e) => {
443 // Only handle left clicks
444 if (e.button !== 0 || !e.currentTarget.contains(e.target as Element)) {
445 return;
446 }
447
448 // Due to browser inconsistencies, especially on mobile browsers, we prevent
449 // default on mouse down and handle focusing the pressable element ourselves.
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 // Only handle left clicks
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 // Due to browser inconsistencies, especially on mobile browsers, we prevent default
551 // on the emulated mouse event and handle focusing the pressable element ourselves.
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 // Remove user-select: none in case component unmounts immediately after pressStart
654 // eslint-disable-next-line arrow-body-style
655 useEffect(() => {
656 return () => {
657 if (!allowTextSelectionOnPress) {
658 // eslint-disable-next-line react-hooks/exhaustive-deps
659 restoreTextSelection(ref.current.target);
660 }
661 };
662 }, [allowTextSelectionOnPress]);
663
664 return {
665 isPressed: isPressedProp || isPressed,
666 pressProps: mergeProps(domProps, pressProps)
667 };
668}
669
670function isHTMLAnchorLink(target: Element): boolean {
671 return target.tagName === 'A' && target.hasAttribute('href');
672}
673
674function isValidKeyboardEvent(event: KeyboardEvent, currentTarget: Element): boolean {
675 const {key, code} = event;
676 const element = currentTarget as HTMLElement;
677 const role = element.getAttribute('role');
678 // Accessibility for keyboards. Space and Enter only.
679 // "Spacebar" is for IE 11
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 // A link with a valid href should be handled natively,
686 // unless it also has role='button' and was triggered using Space.
687 (!isHTMLAnchorLink(element) || (role === 'button' && key !== 'Enter')) &&
688 // An element with role='link' should only trigger with Enter key
689 !(role === 'link' && key !== 'Enter')
690 );
691}
692
693function getTouchFromEvent(event: TouchEvent): Touch | null {
694 const {targetTouches} = event;
695 if (targetTouches.length > 0) {
696 return targetTouches[0];
697 }
698 return null;
699}
700
701function 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
715function 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
725interface Rect {
726 top: number,
727 right: number,
728 bottom: number,
729 left: number
730}
731
732interface EventPoint {
733 clientX: number,
734 clientY: number,
735 width?: number,
736 height?: number,
737 radiusX?: number,
738 radiusY?: number
739}
740
741function 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
753function areRectanglesOverlapping(a: Rect, b: Rect) {
754 // check if they cannot overlap on x axis
755 if (a.left > b.right || b.left > a.right) {
756 return false;
757 }
758 // check if they cannot overlap on y axis
759 if (a.top > b.bottom || b.top > a.bottom) {
760 return false;
761 }
762 return true;
763}
764
765function isOverTarget(point: EventPoint, target: Element) {
766 let rect = target.getBoundingClientRect();
767 let pointRect = getPointClientRect(point);
768 return areRectanglesOverlapping(rect, pointRect);
769}
770
771function shouldPreventDefault(target: Element) {
772 // We cannot prevent default if the target is a draggable element.
773 return !(target instanceof HTMLElement) || !target.draggable;
774}
775
776function 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
788const nonTextInputTypes = new Set([
789 'checkbox',
790 'radio',
791 'range',
792 'color',
793 'file',
794 'image',
795 'button',
796 'submit',
797 'reset'
798]);
799
800function isValidInputKey(target: HTMLInputElement, key: string) {
801 // Only space should toggle checkboxes and radios, not enter.
802 return target.type === 'checkbox' || target.type === 'radio'
803 ? key === ' '
804 : nonTextInputTypes.has(target.type);
805}