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 | import {DOMAttributes, LongPressEvent} from '@react-types/shared';
|
14 | import {mergeProps, useDescription, useGlobalListeners} from '@react-aria/utils';
|
15 | import {usePress} from './usePress';
|
16 | import {useRef} from 'react';
|
17 |
|
18 | export interface LongPressProps {
|
19 | /** Whether long press events should be disabled. */
|
20 | isDisabled?: boolean,
|
21 | /** Handler that is called when a long press interaction starts. */
|
22 | onLongPressStart?: (e: LongPressEvent) => void,
|
23 | /**
|
24 | * Handler that is called when a long press interaction ends, either
|
25 | * over the target or when the pointer leaves the target.
|
26 | */
|
27 | onLongPressEnd?: (e: LongPressEvent) => void,
|
28 | /**
|
29 | * Handler that is called when the threshold time is met while
|
30 | * the press is over the target.
|
31 | */
|
32 | onLongPress?: (e: LongPressEvent) => void,
|
33 | /**
|
34 | * The amount of time in milliseconds to wait before triggering a long press.
|
35 | * @default 500ms
|
36 | */
|
37 | threshold?: number,
|
38 | /**
|
39 | * A description for assistive techology users indicating that a long press
|
40 | * action is available, e.g. "Long press to open menu".
|
41 | */
|
42 | accessibilityDescription?: string
|
43 | }
|
44 |
|
45 | export interface LongPressResult {
|
46 | /** Props to spread on the target element. */
|
47 | longPressProps: DOMAttributes
|
48 | }
|
49 |
|
50 | const DEFAULT_THRESHOLD = 500;
|
51 |
|
52 | /**
|
53 | * Handles long press interactions across mouse and touch devices. Supports a customizable time threshold,
|
54 | * accessibility description, and normalizes behavior across browsers and devices.
|
55 | */
|
56 | export function useLongPress(props: LongPressProps): LongPressResult {
|
57 | let {
|
58 | isDisabled,
|
59 | onLongPressStart,
|
60 | onLongPressEnd,
|
61 | onLongPress,
|
62 | threshold = DEFAULT_THRESHOLD,
|
63 | accessibilityDescription
|
64 | } = props;
|
65 |
|
66 | const timeRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
67 | let {addGlobalListener, removeGlobalListener} = useGlobalListeners();
|
68 |
|
69 | let {pressProps} = usePress({
|
70 | isDisabled,
|
71 | onPressStart(e) {
|
72 | e.continuePropagation();
|
73 | if (e.pointerType === 'mouse' || e.pointerType === 'touch') {
|
74 | if (onLongPressStart) {
|
75 | onLongPressStart({
|
76 | ...e,
|
77 | type: 'longpressstart'
|
78 | });
|
79 | }
|
80 |
|
81 | timeRef.current = setTimeout(() => {
|
82 | // Prevent other usePress handlers from also handling this event.
|
83 | e.target.dispatchEvent(new PointerEvent('pointercancel', {bubbles: true}));
|
84 | if (onLongPress) {
|
85 | onLongPress({
|
86 | ...e,
|
87 | type: 'longpress'
|
88 | });
|
89 | }
|
90 | timeRef.current = undefined;
|
91 | }, threshold);
|
92 |
|
93 | // Prevent context menu, which may be opened on long press on touch devices
|
94 | if (e.pointerType === 'touch') {
|
95 | let onContextMenu = e => {
|
96 | e.preventDefault();
|
97 | };
|
98 |
|
99 | addGlobalListener(e.target, 'contextmenu', onContextMenu, {once: true});
|
100 | addGlobalListener(window, 'pointerup', () => {
|
101 | // If no contextmenu event is fired quickly after pointerup, remove the handler
|
102 | // so future context menu events outside a long press are not prevented.
|
103 | setTimeout(() => {
|
104 | removeGlobalListener(e.target, 'contextmenu', onContextMenu);
|
105 | }, 30);
|
106 | }, {once: true});
|
107 | }
|
108 | }
|
109 | },
|
110 | onPressEnd(e) {
|
111 | if (timeRef.current) {
|
112 | clearTimeout(timeRef.current);
|
113 | }
|
114 |
|
115 | if (onLongPressEnd && (e.pointerType === 'mouse' || e.pointerType === 'touch')) {
|
116 | onLongPressEnd({
|
117 | ...e,
|
118 | type: 'longpressend'
|
119 | });
|
120 | }
|
121 | }
|
122 | });
|
123 |
|
124 | let descriptionProps = useDescription(onLongPress && !isDisabled ? accessibilityDescription : undefined);
|
125 |
|
126 | return {
|
127 | longPressProps: mergeProps(pressProps, descriptionProps)
|
128 | };
|
129 | }
|