Skip to the code
cf.

Idle timer

MediumTiming

Write createIdleTimer({ target, events, timeout, onIdle, onActive }). It tells you when the user has stopped using the page for timeout ms, for example to warn them before they're signed out, and when they come back.

target is an EventTarget, such as document. events is an array of event types, such as ["mousemove", "keydown"]. An event of one of those types on target is activity. Other events aren't.

The user starts out active. After timeout ms without activity, it calls onIdle(), once, and the user is idle.

Activity while the user is idle calls onActive(), once, and the user is active again. Any activity, whether the user is idle or not, starts the timeout ms over.

It returns an object with two methods. isIdle() returns true while the user is idle, and false otherwise. stop() stops watching for good: it removes its event listeners and leaves no timer running, so onIdle and onActive are never called again.

const idle = createIdleTimer({
  target: document,
  events: ["mousemove", "keydown", "pointerdown"],
  timeout: 60000,
  onIdle: () => console.log("idle"),
  onActive: () => console.log("back"),
});
// a minute without input: logs "idle"
idle.isIdle(); // true
// then a key press: logs "back"
idle.isIdle(); // false
// another minute without input: logs "idle" again
idle.stop(); // nothing is logged after this

What the tests check

  1. calls onIdle once, after timeout ms without activity
    const clock = useFakeTimers();
    const onIdle = spy();
    const idle = createIdleTimer({ target: new EventTarget(), events: ["keydown"], timeout: 1000, onIdle, onActive: spy() });
    expect(idle.isIdle()).toBe(false);
    clock.tick(999);
    expect(onIdle).toHaveBeenCalledTimes(0);
    clock.tick(1);
    expect(onIdle).toHaveBeenCalledTimes(1);
    expect(idle.isIdle()).toBe(true);
    clock.tick(5000);
    expect(onIdle).toHaveBeenCalledTimes(1);
    
  2. activity starts the wait over
    const clock = useFakeTimers();
    const target = new EventTarget();
    const onIdle = spy();
    createIdleTimer({ target, events: ["keydown", "mousemove"], timeout: 1000, onIdle, onActive: spy() });
    clock.tick(800);
    target.dispatchEvent(new Event("keydown"));
    clock.tick(800);
    target.dispatchEvent(new Event("mousemove"));
    clock.tick(999);
    expect(onIdle).toHaveBeenCalledTimes(0);
    clock.tick(1);
    expect(onIdle).toHaveBeenCalledTimes(1);
    
  3. only the listed event types count as activity
    const clock = useFakeTimers();
    const target = new EventTarget();
    const onIdle = spy();
    createIdleTimer({ target, events: ["keydown"], timeout: 1000, onIdle, onActive: spy() });
    clock.tick(500);
    target.dispatchEvent(new Event("click"));
    clock.tick(500);
    expect(onIdle).toHaveBeenCalledTimes(1);
    
  4. activity while idle calls onActive once
    const clock = useFakeTimers();
    const target = new EventTarget();
    const onActive = spy();
    const idle = createIdleTimer({ target, events: ["keydown"], timeout: 1000, onIdle: spy(), onActive });
    target.dispatchEvent(new Event("keydown"));
    expect(onActive).toHaveBeenCalledTimes(0);
    clock.tick(1000);
    target.dispatchEvent(new Event("keydown"));
    target.dispatchEvent(new Event("keydown"));
    expect(onActive).toHaveBeenCalledTimes(1);
    expect(idle.isIdle()).toBe(false);
    
  5. goes idle again after the user comes back
    const clock = useFakeTimers();
    const target = new EventTarget();
    const log = [];
    const idle = createIdleTimer({
      target,
      events: ["keydown"],
      timeout: 1000,
      onIdle: () => log.push("idle"),
      onActive: () => log.push("active"),
    });
    clock.tick(4000);
    target.dispatchEvent(new Event("keydown"));
    clock.tick(999);
    expect(log).toEqual(["idle", "active"]);
    clock.tick(1);
    expect(log).toEqual(["idle", "active", "idle"]);
    expect(idle.isIdle()).toBe(true);
    
  6. stop() removes the timer and the listeners
    const clock = useFakeTimers();
    const target = new EventTarget();
    const onIdle = spy();
    const idle = createIdleTimer({ target, events: ["keydown"], timeout: 1000, onIdle, onActive: spy() });
    clock.tick(500);
    idle.stop();
    expect(clock.pending()).toBe(0);
    target.dispatchEvent(new Event("keydown"));
    expect(clock.pending()).toBe(0);
    clock.tick(5000);
    expect(onIdle).not.toHaveBeenCalled();
    
  7. after stop(), activity no longer calls onActive
    const clock = useFakeTimers();
    const target = new EventTarget();
    const onActive = spy();
    const idle = createIdleTimer({ target, events: ["keydown", "mousemove"], timeout: 1000, onIdle: spy(), onActive });
    clock.tick(1000);
    idle.stop();
    target.dispatchEvent(new Event("keydown"));
    target.dispatchEvent(new Event("mousemove"));
    expect(onActive).not.toHaveBeenCalled();
    expect(clock.pending()).toBe(0);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).