From 501a384df268e6de7d04e273e3d37a081422d190 Mon Sep 17 00:00:00 2001 From: zergzorg Date: Fri, 22 May 2026 14:32:05 +0300 Subject: [PATCH 1/2] Pause ticker while panel is hidden --- src/hooks/use-now-ticker.test.ts | 86 +++++++++++++++++++++++++++++++- src/hooks/use-now-ticker.ts | 33 +++++++++++- 2 files changed, 116 insertions(+), 3 deletions(-) diff --git a/src/hooks/use-now-ticker.test.ts b/src/hooks/use-now-ticker.test.ts index 68945f42..89e3818d 100644 --- a/src/hooks/use-now-ticker.test.ts +++ b/src/hooks/use-now-ticker.test.ts @@ -4,6 +4,10 @@ import { useNowTicker } from "./use-now-ticker" describe("useNowTicker", () => { afterEach(() => { + Object.defineProperty(document, "hidden", { + configurable: true, + value: false, + }) vi.useRealTimers() }) @@ -29,10 +33,90 @@ describe("useNowTicker", () => { expect(result.current).toBe(Date.parse("2026-02-03T00:00:00.000Z")) act(() => { - vi.setSystemTime(new Date("2026-02-03T00:00:05.000Z")) vi.advanceTimersByTime(5_000) }) expect(result.current).toBe(Date.parse("2026-02-03T00:00:00.000Z")) }) + + it("pauses ticks while the document is hidden", () => { + vi.useFakeTimers() + vi.setSystemTime(new Date("2026-02-03T00:00:00.000Z")) + Object.defineProperty(document, "hidden", { + configurable: true, + value: true, + }) + + const { result } = renderHook(() => useNowTicker({ intervalMs: 1000 })) + expect(result.current).toBe(Date.parse("2026-02-03T00:00:00.000Z")) + + act(() => { + vi.advanceTimersByTime(5_000) + }) + expect(result.current).toBe(Date.parse("2026-02-03T00:00:00.000Z")) + + const visibleNow = Date.now() + act(() => { + Object.defineProperty(document, "hidden", { + configurable: true, + value: false, + }) + document.dispatchEvent(new Event("visibilitychange")) + }) + expect(result.current).toBe(visibleNow) + }) + + it("stops an active ticker when the document becomes hidden", () => { + vi.useFakeTimers() + vi.setSystemTime(new Date("2026-02-03T00:00:00.000Z")) + + const { result } = renderHook(() => useNowTicker({ intervalMs: 1000 })) + + act(() => { + vi.advanceTimersByTime(1000) + }) + expect(result.current).toBe(Date.parse("2026-02-03T00:00:01.000Z")) + + act(() => { + Object.defineProperty(document, "hidden", { + configurable: true, + value: true, + }) + document.dispatchEvent(new Event("visibilitychange")) + }) + + act(() => { + vi.advanceTimersByTime(5_000) + }) + expect(result.current).toBe(Date.parse("2026-02-03T00:00:01.000Z")) + + const visibleNow = Date.now() + act(() => { + Object.defineProperty(document, "hidden", { + configurable: true, + value: false, + }) + document.dispatchEvent(new Event("visibilitychange")) + }) + expect(result.current).toBe(visibleNow) + }) + + it("keeps ticking while hidden when pauseWhenHidden is false", () => { + vi.useFakeTimers() + vi.setSystemTime(new Date("2026-02-03T00:00:00.000Z")) + Object.defineProperty(document, "hidden", { + configurable: true, + value: true, + }) + + const { result } = renderHook(() => + useNowTicker({ intervalMs: 1000, pauseWhenHidden: false }) + ) + + act(() => { + vi.advanceTimersByTime(1000) + }) + + expect(result.current).toBe(Date.parse("2026-02-03T00:00:01.000Z")) + }) }) diff --git a/src/hooks/use-now-ticker.ts b/src/hooks/use-now-ticker.ts index 06b971c1..f602bfdd 100644 --- a/src/hooks/use-now-ticker.ts +++ b/src/hooks/use-now-ticker.ts @@ -4,19 +4,48 @@ type UseNowTickerOptions = { enabled?: boolean intervalMs?: number stopAfterMs?: number | null + pauseWhenHidden?: boolean resetKey?: unknown } +function isDocumentVisible() { + if (typeof document === "undefined") return true + return !document.hidden +} + export function useNowTicker({ enabled = true, intervalMs = 1000, stopAfterMs = null, + pauseWhenHidden = true, resetKey, }: UseNowTickerOptions = {}) { const [now, setNow] = useState(() => Date.now()) + const [documentVisible, setDocumentVisible] = useState(() => + pauseWhenHidden ? isDocumentVisible() : true + ) + + useEffect(() => { + if (!pauseWhenHidden || typeof document === "undefined") { + setDocumentVisible(true) + return undefined + } + + const handleVisibilityChange = () => { + const visible = isDocumentVisible() + setDocumentVisible(visible) + if (visible) { + setNow(Date.now()) + } + } + + handleVisibilityChange() + document.addEventListener("visibilitychange", handleVisibilityChange) + return () => document.removeEventListener("visibilitychange", handleVisibilityChange) + }, [pauseWhenHidden]) useEffect(() => { - if (!enabled) return undefined + if (!enabled || !documentVisible) return undefined setNow(Date.now()) const interval = window.setInterval(() => setNow(Date.now()), intervalMs) @@ -35,7 +64,7 @@ export function useNowTicker({ window.clearInterval(interval) window.clearTimeout(timeout) } - }, [enabled, intervalMs, stopAfterMs, resetKey]) + }, [enabled, intervalMs, stopAfterMs, resetKey, documentVisible]) return now } From be7d219de3a68e3e1369c4bba2ad4c06552e08ad Mon Sep 17 00:00:00 2001 From: zergzorg Date: Sun, 24 May 2026 10:00:41 +0300 Subject: [PATCH 2/2] Address ticker visibility review feedback --- src/hooks/use-now-ticker.test.ts | 40 ++++++++++++++++++++++++++++---- src/hooks/use-now-ticker.ts | 4 ++-- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/hooks/use-now-ticker.test.ts b/src/hooks/use-now-ticker.test.ts index 89e3818d..2c31c49a 100644 --- a/src/hooks/use-now-ticker.test.ts +++ b/src/hooks/use-now-ticker.test.ts @@ -1,13 +1,20 @@ import { renderHook, act } from "@testing-library/react" -import { afterEach, describe, expect, it, vi } from "vitest" +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest" import { useNowTicker } from "./use-now-ticker" describe("useNowTicker", () => { + let originalDocumentHiddenDescriptor: PropertyDescriptor | undefined + + beforeAll(() => { + originalDocumentHiddenDescriptor = Object.getOwnPropertyDescriptor(document, "hidden") + }) + afterEach(() => { - Object.defineProperty(document, "hidden", { - configurable: true, - value: false, - }) + if (originalDocumentHiddenDescriptor) { + Object.defineProperty(document, "hidden", originalDocumentHiddenDescriptor) + } else { + Reflect.deleteProperty(document, "hidden") + } vi.useRealTimers() }) @@ -66,6 +73,29 @@ describe("useNowTicker", () => { expect(result.current).toBe(visibleNow) }) + it("does not refresh when disabled and the document becomes visible", () => { + vi.useFakeTimers() + vi.setSystemTime(new Date("2026-02-03T00:00:00.000Z")) + Object.defineProperty(document, "hidden", { + configurable: true, + value: true, + }) + + const { result } = renderHook(() => useNowTicker({ enabled: false, intervalMs: 1000 })) + expect(result.current).toBe(Date.parse("2026-02-03T00:00:00.000Z")) + + vi.setSystemTime(new Date("2026-02-03T00:00:05.000Z")) + act(() => { + Object.defineProperty(document, "hidden", { + configurable: true, + value: false, + }) + document.dispatchEvent(new Event("visibilitychange")) + }) + + expect(result.current).toBe(Date.parse("2026-02-03T00:00:00.000Z")) + }) + it("stops an active ticker when the document becomes hidden", () => { vi.useFakeTimers() vi.setSystemTime(new Date("2026-02-03T00:00:00.000Z")) diff --git a/src/hooks/use-now-ticker.ts b/src/hooks/use-now-ticker.ts index f602bfdd..fd995bce 100644 --- a/src/hooks/use-now-ticker.ts +++ b/src/hooks/use-now-ticker.ts @@ -34,7 +34,7 @@ export function useNowTicker({ const handleVisibilityChange = () => { const visible = isDocumentVisible() setDocumentVisible(visible) - if (visible) { + if (visible && enabled) { setNow(Date.now()) } } @@ -42,7 +42,7 @@ export function useNowTicker({ handleVisibilityChange() document.addEventListener("visibilitychange", handleVisibilityChange) return () => document.removeEventListener("visibilitychange", handleVisibilityChange) - }, [pauseWhenHidden]) + }, [enabled, pauseWhenHidden]) useEffect(() => { if (!enabled || !documentVisible) return undefined