fix(reader): filter Magic Mouse wheel events to stop accidental page turns (#4195)
A touch-surface mouse like the Magic Mouse emits a flood of tiny, low- magnitude wheel events — plus an inertial momentum tail — for a single physical gesture, and even a light brush of the surface produces spurious deltas. The previous 100ms trailing debounce collapsed bursts but did not filter by magnitude, so isolated micro-touches and the momentum tail each turned a page, cascading into continuous accidental page turns in paginated mode. Add a wheel gesture detector that accumulates normalized wheel travel and only flips once it crosses a deliberate-intent threshold, then swallows the rest of the stream (the momentum tail) until the wheel goes idle — so one physical gesture flips exactly one page, mirroring native readers. Closes #4117 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '@/services/constants';
|
||||
import { createWheelGestureDetector } from '@/app/reader/utils/wheelGesture';
|
||||
import { dispatchTouchInterceptors, TouchDetail } from './useTouchInterceptor';
|
||||
|
||||
export const useMouseEvent = (
|
||||
@@ -11,36 +11,48 @@ export const useMouseEvent = (
|
||||
handlePageFlip: (msg: MessageEvent | React.MouseEvent<HTMLDivElement, MouseEvent>) => void,
|
||||
) => {
|
||||
const { hoveredBookKey } = useReaderStore();
|
||||
// Keep the latest handlePageFlip in a ref so the debounced wrapper (created
|
||||
// once via useMemo) always invokes the most recent closure. Without this
|
||||
// ref, the empty-deps useMemo would freeze the first-render handler and any
|
||||
// state captured in subsequent re-renders would be invisible to wheel-driven
|
||||
// page flips.
|
||||
// Keep the latest handlePageFlip in a ref so the wheel-driven flip path
|
||||
// always invokes the most recent closure, independent of when listeners
|
||||
// were registered.
|
||||
const handlePageFlipRef = useRef(handlePageFlip);
|
||||
useEffect(() => {
|
||||
handlePageFlipRef.current = handlePageFlip;
|
||||
}, [handlePageFlip]);
|
||||
const debounceFlip = useMemo(
|
||||
() =>
|
||||
debounce(
|
||||
(msg: MessageEvent | React.MouseEvent<HTMLDivElement, MouseEvent>) =>
|
||||
handlePageFlipRef.current(msg),
|
||||
100,
|
||||
),
|
||||
[],
|
||||
);
|
||||
// Filters the raw wheel stream so a touch-surface mouse (e.g. Magic Mouse)
|
||||
// — which emits a flood of tiny events plus an inertial momentum tail for
|
||||
// one physical gesture — flips exactly one page instead of cascading
|
||||
// through several. See wheelGesture.ts.
|
||||
const wheelDetectorRef = useRef<ReturnType<typeof createWheelGestureDetector> | null>(null);
|
||||
if (!wheelDetectorRef.current) {
|
||||
wheelDetectorRef.current = createWheelGestureDetector();
|
||||
}
|
||||
const handleMouseEvent = (msg: MessageEvent | React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
if (msg instanceof MessageEvent) {
|
||||
if (msg.data && msg.data.bookKey === bookKey) {
|
||||
if (msg.data.type === 'iframe-wheel') {
|
||||
if (msg.data.ctrlKey) {
|
||||
// Pinch/ctrl-wheel zoom is not a page-turn gesture — drop any
|
||||
// travel accumulated so far so it can't bleed into a later flip.
|
||||
wheelDetectorRef.current!.reset();
|
||||
if (msg.data.deltaY > 0) {
|
||||
eventDispatcher.dispatch('zoom-out', { factor: Math.abs(msg.data.deltaY) / 100 });
|
||||
} else if (msg.data.deltaY < 0) {
|
||||
eventDispatcher.dispatch('zoom-in', { factor: Math.abs(msg.data.deltaY) / 100 });
|
||||
}
|
||||
} else {
|
||||
debounceFlip(msg);
|
||||
const flip = wheelDetectorRef.current!.feed({
|
||||
deltaX: msg.data.deltaX ?? 0,
|
||||
deltaY: msg.data.deltaY ?? 0,
|
||||
deltaMode: msg.data.deltaMode ?? 0,
|
||||
timeStamp: Date.now(),
|
||||
});
|
||||
if (flip) {
|
||||
handlePageFlipRef.current(
|
||||
new MessageEvent('message', {
|
||||
data: { ...msg.data, deltaX: flip.deltaX, deltaY: flip.deltaY },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handlePageFlip(msg);
|
||||
|
||||
Reference in New Issue
Block a user