Skip to content

Commit 6a4fc49

Browse files
committed
Implement button press state machine on web
1 parent 233649d commit 6a4fc49

3 files changed

Lines changed: 153 additions & 2 deletions

File tree

packages/react-native-gesture-handler/src/web/handlers/NativeViewGestureHandler.ts

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Platform } from 'react-native';
22

33
import { type ActionType } from '../../ActionType';
4+
import type { ButtonEvent } from '../../specs/RNGestureHandlerButtonNativeComponent';
45
import { State } from '../../State';
56
import type { NativeHandlerData } from '../../v3/hooks/gestures/native/NativeTypes';
67
import type { HandlerData } from '../../v3/types';
@@ -17,6 +18,7 @@ import type {
1718
} from '../interfaces';
1819
import { NativeGestureRole } from '../interfaces';
1920
import type { GestureHandlerDelegate } from '../tools/GestureHandlerDelegate';
21+
import { ButtonEventName, dispatchButtonEvent } from '../tools/ButtonEvents';
2022
import {
2123
dispatchGestureLifecycleEvent,
2224
GestureLifecycleEvent,
@@ -39,6 +41,12 @@ export default class NativeViewGestureHandler extends GestureHandler {
3941

4042
private lastActiveHandlerData: HandlerData<NativeHandlerData> | null = null;
4143

44+
private hasLongPressHandler = false;
45+
private longPressDuration = -1;
46+
private longPressDetected = false;
47+
private lastEventWasInside = false;
48+
private pendingLongPress: ReturnType<typeof setTimeout> | null = null;
49+
4250
public constructor(
4351
delegate: GestureHandlerDelegate<unknown, IGestureHandler>
4452
) {
@@ -90,6 +98,12 @@ export default class NativeViewGestureHandler extends GestureHandler {
9098
if (config.yieldsToContinuousGestures !== undefined) {
9199
this.yieldsToContinuousGestures = config.yieldsToContinuousGestures;
92100
}
101+
if (config.hasLongPressHandler !== undefined) {
102+
this.hasLongPressHandler = config.hasLongPressHandler;
103+
}
104+
if (config.longPressDuration !== undefined) {
105+
this.longPressDuration = config.longPressDuration;
106+
}
93107

94108
const view = this.delegate.view as HTMLElement;
95109
this.restoreViewStyles(view);
@@ -165,9 +179,33 @@ export default class NativeViewGestureHandler extends GestureHandler {
165179
}
166180
}
167181

168-
protected override onPointerLeave(): void {
182+
protected override onPointerLeave(event: AdaptedEvent): void {
183+
this.tracker.track(event);
184+
185+
if (
186+
this.role === NativeGestureRole.Button &&
187+
!this.shouldCancelWhenOutside &&
188+
this.lastEventWasInside
189+
) {
190+
this.dispatchButtonEvent(ButtonEventName.PressOut);
191+
this.clearLongPressTimer();
192+
}
193+
169194
if (this.state === State.BEGAN || this.state === State.ACTIVE) {
170-
this.cancel();
195+
super.onPointerLeave(event);
196+
}
197+
}
198+
199+
protected override onPointerEnter(event: AdaptedEvent): void {
200+
this.tracker.track(event);
201+
super.onPointerEnter(event);
202+
203+
if (
204+
this.role === NativeGestureRole.Button &&
205+
(this.state === State.BEGAN || this.state === State.ACTIVE) &&
206+
!this.lastEventWasInside
207+
) {
208+
this.dispatchButtonEvent(ButtonEventName.PressIn);
171209
}
172210
}
173211

@@ -234,6 +272,7 @@ export default class NativeViewGestureHandler extends GestureHandler {
234272
}
235273

236274
public override detach(): void {
275+
this.clearLongPressTimer();
237276
super.detach();
238277
this.role = null;
239278
}
@@ -282,6 +321,88 @@ export default class NativeViewGestureHandler extends GestureHandler {
282321
);
283322
}
284323

324+
protected override onStateChange(newState: State): void {
325+
if (this.role !== NativeGestureRole.Button) {
326+
return;
327+
}
328+
329+
if (newState === State.BEGAN) {
330+
if (!this.getButtonEventData().pointerInside) {
331+
return;
332+
}
333+
334+
this.dispatchButtonEvent(ButtonEventName.PressIn);
335+
this.longPressDetected = false;
336+
337+
if (this.hasLongPressHandler && this.longPressDuration >= 0) {
338+
this.pendingLongPress = setTimeout(() => {
339+
this.pendingLongPress = null;
340+
this.longPressDetected = true;
341+
this.dispatchButtonEvent(ButtonEventName.LongPress);
342+
}, this.longPressDuration);
343+
}
344+
return;
345+
}
346+
347+
if (
348+
newState !== State.END &&
349+
newState !== State.FAILED &&
350+
newState !== State.CANCELLED
351+
) {
352+
return;
353+
}
354+
355+
const endedInside = this.lastEventWasInside;
356+
357+
if (endedInside) {
358+
this.dispatchButtonEvent(ButtonEventName.PressOut);
359+
}
360+
this.clearLongPressTimer();
361+
362+
if (newState === State.END && !this.longPressDetected && endedInside) {
363+
this.dispatchButtonEvent(ButtonEventName.Press);
364+
}
365+
366+
this.dispatchButtonEvent(ButtonEventName.InteractionFinished);
367+
this.longPressDetected = false;
368+
}
369+
370+
private dispatchButtonEvent(name: ButtonEventName): void {
371+
if (name === ButtonEventName.PressIn) {
372+
this.lastEventWasInside = true;
373+
} else if (name === ButtonEventName.PressOut) {
374+
this.lastEventWasInside = false;
375+
}
376+
377+
dispatchButtonEvent(
378+
this.delegate.view as HTMLElement | null,
379+
name,
380+
this.getButtonEventData()
381+
);
382+
}
383+
384+
private getButtonEventData(): ButtonEvent {
385+
const absolute = this.tracker.getAbsoluteCoordsAverage();
386+
const relative = this.tracker.getRelativeCoordsAverage();
387+
388+
return {
389+
pointerInside: this.delegate.isPointerInBounds(absolute),
390+
x: relative.x,
391+
y: relative.y,
392+
absoluteX: absolute.x,
393+
absoluteY: absolute.y,
394+
numberOfPointers: this.tracker.trackedPointersCount,
395+
pointerType: this.pointerType,
396+
};
397+
}
398+
399+
private clearLongPressTimer(): void {
400+
if (this.pendingLongPress !== null) {
401+
clearTimeout(this.pendingLongPress);
402+
this.pendingLongPress = null;
403+
}
404+
}
405+
285406
protected override transformNativeEvent(): Record<string, unknown> {
286407
const absolute = this.tracker.getAbsoluteCoordsAverage();
287408
const relative = this.tracker.getRelativeCoordsAverage();
@@ -317,7 +438,15 @@ export default class NativeViewGestureHandler extends GestureHandler {
317438
}
318439

319440
public override reset(): void {
441+
this.clearLongPressTimer();
320442
super.reset();
321443
this.lastActiveHandlerData = null;
444+
this.lastEventWasInside = false;
445+
this.longPressDetected = false;
446+
}
447+
448+
public override onDestroy(): void {
449+
this.clearLongPressTimer();
450+
super.onDestroy();
322451
}
323452
}

packages/react-native-gesture-handler/src/web/interfaces.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ export interface Config extends Record<string, ConfigArgs> {
9191
shouldActivateOnStart?: boolean;
9292
disallowInterruption?: boolean;
9393
yieldsToContinuousGestures?: boolean;
94+
hasLongPressHandler?: boolean;
95+
longPressDuration?: number;
9496
direction?: Directions;
9597
enableTrackpadTwoFingerGesture?: boolean;
9698
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { ButtonEvent } from '../../specs/RNGestureHandlerButtonNativeComponent';
2+
3+
export const ButtonEventName = {
4+
Press: 'gh:buttonPress',
5+
PressIn: 'gh:buttonPressIn',
6+
PressOut: 'gh:buttonPressOut',
7+
LongPress: 'gh:buttonLongPress',
8+
InteractionFinished: 'gh:buttonInteractionFinished',
9+
} as const;
10+
11+
export type ButtonEventName =
12+
(typeof ButtonEventName)[keyof typeof ButtonEventName];
13+
14+
export function dispatchButtonEvent(
15+
view: HTMLElement | null | undefined,
16+
name: ButtonEventName,
17+
event: ButtonEvent
18+
): void {
19+
view?.dispatchEvent(new CustomEvent(name, { detail: event }));
20+
}

0 commit comments

Comments
 (0)