fix(reader): preserve valid EPUB side clicks

Evaluate the long-hold threshold when the pointer is released so the double-click delay does not discard valid page turns.

Closes #1992
This commit is contained in:
neon
2026-10-30 00:44:06 +00:00
parent e8e3f4df19
commit fd1aac3074
2 changed files with 48 additions and 1 deletions
@@ -551,6 +551,53 @@ describe('useFoliateInput', () => {
}
})
it.each([
{ holdMs: 250, expectedNavigationCount: 1 },
{ holdMs: 500, expectedNavigationCount: 0 },
])('handles a $holdMs ms pointer hold before the delayed click-zone action', ({ holdMs, expectedNavigationCount }) => {
vi.useFakeTimers()
const next = vi.fn<() => void>()
const view: ViewLike = {
prev: vi.fn<() => void>(),
next,
getBoundingClientRect: () => ({ left: 0, width: 100 }) as DOMRect,
}
const input = useFoliateInput(() => view, undefined, vi.fn<() => void>(), vi.fn<() => void>())
const doc = makeDocTarget()
input.attachIframeClicks(doc)
const originalMaxTouchPoints = Object.getOwnPropertyDescriptor(navigator, 'maxTouchPoints')
const originalOntouchstart = Object.getOwnPropertyDescriptor(window, 'ontouchstart')
Object.defineProperty(navigator, 'maxTouchPoints', {
configurable: true,
get: () => 0,
})
Reflect.deleteProperty(window as unknown as Record<string, unknown>, 'ontouchstart')
const postMessage = vi.spyOn(window, 'postMessage').mockImplementation(() => {})
doc.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
vi.advanceTimersByTime(holdMs)
doc.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: 95, clientY: 100 }))
const message = postMessage.mock.calls[0]?.[0]
if (message) window.dispatchEvent(new MessageEvent('message', { data: message, origin: window.location.origin }))
vi.advanceTimersByTime(300)
expect(postMessage).toHaveBeenCalledTimes(expectedNavigationCount)
expect(next).toHaveBeenCalledTimes(expectedNavigationCount)
postMessage.mockRestore()
input.cleanup()
if (originalMaxTouchPoints) {
Object.defineProperty(navigator, 'maxTouchPoints', originalMaxTouchPoints)
}
if (originalOntouchstart) {
Object.defineProperty(window, 'ontouchstart', originalOntouchstart)
}
})
it('ignores a page-turn click message that arrives after annotation tap suppression starts', () => {
vi.useFakeTimers()
@@ -200,6 +200,7 @@ export function useFoliateInput(
'click',
(e: MouseEvent) => {
if (Date.now() - lastTouchTime < 500) return
if (!longHoldTimeout) return
const iframe = doc.defaultView?.frameElement as HTMLIFrameElement | null
if (!iframe) return
const rect = iframe.getBoundingClientRect()
@@ -264,7 +265,6 @@ export function useFoliateInput(
setTimeout(() => {
if (isTapNavigationSuppressed()) return
if (Date.now() - lastClickTime < DOUBLE_CLICK_MS) return
if (!longHoldTimeout) return
if (isNavigating) return
const isMobile = 'ontouchstart' in window || navigator.maxTouchPoints > 0