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