Skip to content

Commit 958b617

Browse files
necolastrueadm
authored andcommitted
Add delay props to Hover event module (#15325)
1 parent c3cc936 commit 958b617

File tree

3 files changed

+249
-42
lines changed

3 files changed

+249
-42
lines changed

packages/react-events/src/Hover.js

Lines changed: 103 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ type HoverProps = {
2020
};
2121

2222
type HoverState = {
23+
isActiveHovered: boolean,
2324
isHovered: boolean,
2425
isInHitSlop: boolean,
2526
isTouched: boolean,
27+
hoverStartTimeout: null | TimeoutID,
28+
hoverEndTimeout: null | TimeoutID,
2629
};
2730

2831
type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange';
@@ -60,65 +63,131 @@ function createHoverEvent(
6063
};
6164
}
6265

66+
function dispatchHoverChangeEvent(
67+
event: ResponderEvent,
68+
context: ResponderContext,
69+
props: HoverProps,
70+
state: HoverState,
71+
): void {
72+
const listener = () => {
73+
props.onHoverChange(state.isActiveHovered);
74+
};
75+
const syntheticEvent = createHoverEvent(
76+
'hoverchange',
77+
event.target,
78+
listener,
79+
);
80+
context.dispatchEvent(syntheticEvent, {discrete: true});
81+
}
82+
6383
function dispatchHoverStartEvents(
6484
event: ResponderEvent,
6585
context: ResponderContext,
6686
props: HoverProps,
87+
state: HoverState,
6788
): void {
6889
const {nativeEvent, target} = event;
6990
if (context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget)) {
7091
return;
7192
}
72-
if (props.onHoverStart) {
73-
const syntheticEvent = createHoverEvent(
74-
'hoverstart',
75-
target,
76-
props.onHoverStart,
77-
);
78-
context.dispatchEvent(syntheticEvent, {discrete: true});
93+
94+
state.isHovered = true;
95+
96+
if (state.hoverEndTimeout !== null) {
97+
clearTimeout(state.hoverEndTimeout);
98+
state.hoverEndTimeout = null;
7999
}
80-
if (props.onHoverChange) {
81-
const listener = () => {
82-
props.onHoverChange(true);
83-
};
84-
const syntheticEvent = createHoverEvent('hoverchange', target, listener);
85-
context.dispatchEvent(syntheticEvent, {discrete: true});
100+
101+
const dispatch = () => {
102+
state.isActiveHovered = true;
103+
104+
if (props.onHoverStart) {
105+
const syntheticEvent = createHoverEvent(
106+
'hoverstart',
107+
target,
108+
props.onHoverStart,
109+
);
110+
context.dispatchEvent(syntheticEvent, {discrete: true});
111+
}
112+
if (props.onHoverChange) {
113+
dispatchHoverChangeEvent(event, context, props, state);
114+
}
115+
};
116+
117+
if (!state.isActiveHovered) {
118+
const delay = calculateDelayMS(props.delayHoverStart, 0, 0);
119+
if (delay > 0) {
120+
state.hoverStartTimeout = context.setTimeout(() => {
121+
state.hoverStartTimeout = null;
122+
dispatch();
123+
}, delay);
124+
} else {
125+
dispatch();
126+
}
86127
}
87128
}
88129

89130
function dispatchHoverEndEvents(
90131
event: ResponderEvent,
91132
context: ResponderContext,
92133
props: HoverProps,
134+
state: HoverState,
93135
) {
94136
const {nativeEvent, target} = event;
95137
if (context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget)) {
96138
return;
97139
}
98-
if (props.onHoverEnd) {
99-
const syntheticEvent = createHoverEvent(
100-
'hoverend',
101-
target,
102-
props.onHoverEnd,
103-
);
104-
context.dispatchEvent(syntheticEvent, {discrete: true});
140+
141+
state.isHovered = false;
142+
143+
if (state.hoverStartTimeout !== null) {
144+
clearTimeout(state.hoverStartTimeout);
145+
state.hoverStartTimeout = null;
105146
}
106-
if (props.onHoverChange) {
107-
const listener = () => {
108-
props.onHoverChange(false);
109-
};
110-
const syntheticEvent = createHoverEvent('hoverchange', target, listener);
111-
context.dispatchEvent(syntheticEvent, {discrete: true});
147+
148+
const dispatch = () => {
149+
state.isActiveHovered = false;
150+
151+
if (props.onHoverEnd) {
152+
const syntheticEvent = createHoverEvent(
153+
'hoverend',
154+
target,
155+
props.onHoverEnd,
156+
);
157+
context.dispatchEvent(syntheticEvent, {discrete: true});
158+
}
159+
if (props.onHoverChange) {
160+
dispatchHoverChangeEvent(event, context, props, state);
161+
}
162+
};
163+
164+
if (state.isActiveHovered) {
165+
const delay = calculateDelayMS(props.delayHoverEnd, 0, 0);
166+
if (delay > 0) {
167+
state.hoverEndTimeout = context.setTimeout(() => {
168+
dispatch();
169+
}, delay);
170+
} else {
171+
dispatch();
172+
}
112173
}
113174
}
114175

176+
function calculateDelayMS(delay: ?number, min = 0, fallback = 0) {
177+
const maybeNumber = delay == null ? null : delay;
178+
return Math.max(min, maybeNumber != null ? maybeNumber : fallback);
179+
}
180+
115181
const HoverResponder = {
116182
targetEventTypes,
117183
createInitialState() {
118184
return {
185+
isActiveHovered: false,
119186
isHovered: false,
120187
isInHitSlop: false,
121188
isTouched: false,
189+
hoverStartTimeout: null,
190+
hoverEndTimeout: null,
122191
};
123192
},
124193
onEvent(
@@ -156,32 +225,30 @@ const HoverResponder = {
156225
state.isInHitSlop = true;
157226
return;
158227
}
159-
dispatchHoverStartEvents(event, context, props);
160-
state.isHovered = true;
228+
dispatchHoverStartEvents(event, context, props, state);
161229
}
162230
break;
163231
}
164232
case 'pointerout':
165233
case 'mouseout': {
166234
if (state.isHovered && !state.isTouched) {
167-
dispatchHoverEndEvents(event, context, props);
168-
state.isHovered = false;
235+
dispatchHoverEndEvents(event, context, props, state);
169236
}
170237
state.isInHitSlop = false;
171238
state.isTouched = false;
172239
break;
173240
}
241+
174242
case 'pointermove': {
175-
if (!state.isTouched) {
243+
if (state.isHovered && !state.isTouched) {
176244
if (state.isInHitSlop) {
177245
if (
178246
!context.isPositionWithinTouchHitTarget(
179247
(nativeEvent: any).x,
180248
(nativeEvent: any).y,
181249
)
182250
) {
183-
dispatchHoverStartEvents(event, context, props);
184-
state.isHovered = true;
251+
dispatchHoverStartEvents(event, context, props, state);
185252
state.isInHitSlop = false;
186253
}
187254
} else if (
@@ -191,17 +258,16 @@ const HoverResponder = {
191258
(nativeEvent: any).y,
192259
)
193260
) {
194-
dispatchHoverEndEvents(event, context, props);
195-
state.isHovered = false;
261+
dispatchHoverEndEvents(event, context, props, state);
196262
state.isInHitSlop = true;
197263
}
198264
}
199265
break;
200266
}
267+
201268
case 'pointercancel': {
202269
if (state.isHovered && !state.isTouched) {
203-
dispatchHoverEndEvents(event, context, props);
204-
state.isHovered = false;
270+
dispatchHoverEndEvents(event, context, props, state);
205271
state.isTouched = false;
206272
}
207273
break;

packages/react-events/src/Swipe.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ const SwipeResponder = {
104104
case 'mousedown':
105105
case 'pointerdown': {
106106
if (!state.isSwiping && !context.hasOwnership()) {
107-
let obj = event;
107+
let obj = nativeEvent;
108108
if (type === 'touchstart') {
109109
obj = (nativeEvent: any).targetTouches[0];
110110
state.touchId = obj.identifier;

0 commit comments

Comments
 (0)