1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | import {DOMAttributes} from '@react-types/shared';
|
19 | import {HoverEvents} from '@react-types/shared';
|
20 | import {useEffect, useMemo, useRef, useState} from 'react';
|
21 |
|
22 | export interface HoverProps extends HoverEvents {
|
23 |
|
24 | isDisabled?: boolean
|
25 | }
|
26 |
|
27 | export interface HoverResult {
|
28 |
|
29 | hoverProps: DOMAttributes,
|
30 | isHovered: boolean
|
31 | }
|
32 |
|
33 |
|
34 |
|
35 |
|
36 | let globalIgnoreEmulatedMouseEvents = false;
|
37 | let hoverCount = 0;
|
38 |
|
39 | function setGlobalIgnoreEmulatedMouseEvents() {
|
40 | globalIgnoreEmulatedMouseEvents = true;
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 | setTimeout(() => {
|
47 | globalIgnoreEmulatedMouseEvents = false;
|
48 | }, 50);
|
49 | }
|
50 |
|
51 | function handleGlobalPointerEvent(e) {
|
52 | if (e.pointerType === 'touch') {
|
53 | setGlobalIgnoreEmulatedMouseEvents();
|
54 | }
|
55 | }
|
56 |
|
57 | function setupGlobalTouchEvents() {
|
58 | if (typeof document === 'undefined') {
|
59 | return;
|
60 | }
|
61 |
|
62 | if (typeof PointerEvent !== 'undefined') {
|
63 | document.addEventListener('pointerup', handleGlobalPointerEvent);
|
64 | } else {
|
65 | document.addEventListener('touchend', setGlobalIgnoreEmulatedMouseEvents);
|
66 | }
|
67 |
|
68 | hoverCount++;
|
69 | return () => {
|
70 | hoverCount--;
|
71 | if (hoverCount > 0) {
|
72 | return;
|
73 | }
|
74 |
|
75 | if (typeof PointerEvent !== 'undefined') {
|
76 | document.removeEventListener('pointerup', handleGlobalPointerEvent);
|
77 | } else {
|
78 | document.removeEventListener('touchend', setGlobalIgnoreEmulatedMouseEvents);
|
79 | }
|
80 | };
|
81 | }
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 | export function useHover(props: HoverProps): HoverResult {
|
88 | let {
|
89 | onHoverStart,
|
90 | onHoverChange,
|
91 | onHoverEnd,
|
92 | isDisabled
|
93 | } = props;
|
94 |
|
95 | let [isHovered, setHovered] = useState(false);
|
96 | let state = useRef({
|
97 | isHovered: false,
|
98 | ignoreEmulatedMouseEvents: false,
|
99 | pointerType: '',
|
100 | target: null
|
101 | }).current;
|
102 |
|
103 | useEffect(setupGlobalTouchEvents, []);
|
104 |
|
105 | let {hoverProps, triggerHoverEnd} = useMemo(() => {
|
106 | let triggerHoverStart = (event, pointerType) => {
|
107 | state.pointerType = pointerType;
|
108 | if (isDisabled || pointerType === 'touch' || state.isHovered || !event.currentTarget.contains(event.target)) {
|
109 | return;
|
110 | }
|
111 |
|
112 | state.isHovered = true;
|
113 | let target = event.currentTarget;
|
114 | state.target = target;
|
115 |
|
116 | if (onHoverStart) {
|
117 | onHoverStart({
|
118 | type: 'hoverstart',
|
119 | target,
|
120 | pointerType
|
121 | });
|
122 | }
|
123 |
|
124 | if (onHoverChange) {
|
125 | onHoverChange(true);
|
126 | }
|
127 |
|
128 | setHovered(true);
|
129 | };
|
130 |
|
131 | let triggerHoverEnd = (event, pointerType) => {
|
132 | state.pointerType = '';
|
133 | state.target = null;
|
134 |
|
135 | if (pointerType === 'touch' || !state.isHovered) {
|
136 | return;
|
137 | }
|
138 |
|
139 | state.isHovered = false;
|
140 | let target = event.currentTarget;
|
141 | if (onHoverEnd) {
|
142 | onHoverEnd({
|
143 | type: 'hoverend',
|
144 | target,
|
145 | pointerType
|
146 | });
|
147 | }
|
148 |
|
149 | if (onHoverChange) {
|
150 | onHoverChange(false);
|
151 | }
|
152 |
|
153 | setHovered(false);
|
154 | };
|
155 |
|
156 | let hoverProps: DOMAttributes = {};
|
157 |
|
158 | if (typeof PointerEvent !== 'undefined') {
|
159 | hoverProps.onPointerEnter = (e) => {
|
160 | if (globalIgnoreEmulatedMouseEvents && e.pointerType === 'mouse') {
|
161 | return;
|
162 | }
|
163 |
|
164 | triggerHoverStart(e, e.pointerType);
|
165 | };
|
166 |
|
167 | hoverProps.onPointerLeave = (e) => {
|
168 | if (!isDisabled && e.currentTarget.contains(e.target as Element)) {
|
169 | triggerHoverEnd(e, e.pointerType);
|
170 | }
|
171 | };
|
172 | } else {
|
173 | hoverProps.onTouchStart = () => {
|
174 | state.ignoreEmulatedMouseEvents = true;
|
175 | };
|
176 |
|
177 | hoverProps.onMouseEnter = (e) => {
|
178 | if (!state.ignoreEmulatedMouseEvents && !globalIgnoreEmulatedMouseEvents) {
|
179 | triggerHoverStart(e, 'mouse');
|
180 | }
|
181 |
|
182 | state.ignoreEmulatedMouseEvents = false;
|
183 | };
|
184 |
|
185 | hoverProps.onMouseLeave = (e) => {
|
186 | if (!isDisabled && e.currentTarget.contains(e.target as Element)) {
|
187 | triggerHoverEnd(e, 'mouse');
|
188 | }
|
189 | };
|
190 | }
|
191 | return {hoverProps, triggerHoverEnd};
|
192 | }, [onHoverStart, onHoverChange, onHoverEnd, isDisabled, state]);
|
193 |
|
194 | useEffect(() => {
|
195 |
|
196 |
|
197 | if (isDisabled) {
|
198 | triggerHoverEnd({currentTarget: state.target}, state.pointerType);
|
199 | }
|
200 |
|
201 | }, [isDisabled]);
|
202 |
|
203 | return {
|
204 | hoverProps,
|
205 | isHovered
|
206 | };
|
207 | }
|
208 |
|