Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[BUG] Keyboard press leaves a listener on every focus and on every Enter press (duplicated pointerup)

Open Beginner friendly
#3,935 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
74/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
javascript
Domain
frontend

Research direction

Start in motion-dom's gestures/press/utils/keyboard.mjs, in enableKeyboardPress, where the keydown, keyup and blur listeners are added and the blur listener that removes keydown is never removed. The issue includes a self-contained Node script that counts live listeners and dispatched pointerup events; run it before and after the change. Done when the blur count stays at one after repeated focus/blur cycles and each Enter press yields one pointerup.

Written by the indexing model from the issue text.

Description

2. Describe the bug

The keyboard side of the press gesture (motion-dom, gestures/press/utils/keyboard.mjs, enableKeyboardPress) adds event listeners that are never removed while the element stays mounted:

  • On every focus, it adds a blur listener that removes the keydown listener, but the blur listener itself is never removed. Each focus/blur cycle leaves one more blur listener on the element.
  • On every Enter keydown, it adds a keyup listener and a blur listener, and neither is ever removed. After k Enter presses, one Enter keyup dispatches k synthetic pointerup events, and one blur dispatches k pointercancel events.

They are only released when the gesture's AbortSignal fires, which happens on unmount. So any long-lived focusable element with whileTap or press(), such as a toolbar button, accumulates listeners for as long as the page is open.

Present in motion-dom 14.2.0 (latest) and in 13.x:

const enableKeyboardPress = (focusEvent, eventOptions) => {
    const element = focusEvent.currentTarget;
    if (!element)
        return;
    const handleKeydown = filterEvents(() => {
        if (isPressing.has(element))
            return;
        firePointerEvent(element, "down");
        const handleKeyup = filterEvents(() => {
            firePointerEvent(element, "up");
        });
        const handleBlur = () => firePointerEvent(element, "cancel");
        element.addEventListener("keyup", handleKeyup, eventOptions);   // never removed
        element.addEventListener("blur", handleBlur, eventOptions);     // never removed
    });
    element.addEventListener("keydown", handleKeydown, eventOptions);
    element.addEventListener("blur", () => element.removeEventListener("keydown", handleKeydown), eventOptions);  // never removed
};

3. Reproduction

The bug is in the module itself, so this self-contained script reproduces it with nothing but motion-dom. It runs as an .mjs file in Node 18+, and the body also runs in any browser sandbox.

import { enableKeyboardPress } from "motion-dom/dist/es/gestures/press/utils/keyboard.mjs";

globalThis.PointerEvent ??= class extends Event {};
const element = new EventTarget();
const live = new Map();
const add = element.addEventListener.bind(element), remove = element.removeEventListener.bind(element);
element.addEventListener = (type, fn, o) => { live.set(fn, type); add(type, fn, o); };
element.removeEventListener = (type, fn, o) => { live.delete(fn); remove(type, fn, o); };
const count = () => [...live.values()].reduce((out, t) => ({ ...out, [t]: (out[t] ?? 0) + 1 }), {});

let ups = 0;
element.addEventListener("pointerup", () => ups++);
for (let i = 0; i < 3; i++) {                       // three focus/blur cycles, as the gesture does on "focus"
    enableKeyboardPress({ currentTarget: element }, {});
    element.dispatchEvent(new Event("blur"));
}
console.log(count());                                // { pointerup: 1, blur: 3 }: three blur listeners left

enableKeyboardPress({ currentTarget: element }, {});
for (let i = 0; i < 3; i++) {                       // three Enter presses while focused
    const down = new Event("keydown"); down.key = "Enter"; element.dispatchEvent(down);
    const up = new Event("keyup"); up.key = "Enter"; element.dispatchEvent(up);
}
console.log(count(), ups);                           // keyup: 3, blur: 7, and 6 pointerup events where 3 were pressed

The same behaviour shows in a React app. Give a <motion.button whileTap={{ scale: 0.95 }}> focus with Tab and move away a few times, then look at getEventListeners(button) in Chrome DevTools: the blur listeners grow by one per focus.

4. Steps to reproduce

  1. Render any element with whileTap (or call press() on it).
  2. Focus and blur it several times, or press Enter on it several times.
  3. Count its listeners: blur grows by one per focus, keyup and blur by one per Enter press, and Enter keyup fires duplicated pointerup events.

5. Expected behavior

Each focus's listeners are released at its blur, and each press's keyup and blur listeners are released when that press ends. A suggested fix:

const handleKeydown = filterEvents(() => {
    if (isPressing.has(element))
        return;
    firePointerEvent(element, "down");
    const end = () => {
        element.removeEventListener("keyup", handleKeyup);
        element.removeEventListener("blur", handleBlur);
    };
    const handleKeyup = filterEvents(() => { end(); firePointerEvent(element, "up"); });
    const handleBlur = () => { end(); firePointerEvent(element, "cancel"); };
    element.addEventListener("keyup", handleKeyup, eventOptions);
    element.addEventListener("blur", handleBlur, eventOptions);
});
element.addEventListener("keydown", handleKeydown, eventOptions);
element.addEventListener("blur", () => element.removeEventListener("keydown", handleKeydown), { ...eventOptions, once: true });

7. Environment details

  • motion 13.4 / motion-dom 13.3.0 in our app; the same code is in motion-dom 14.2.0.
  • Seen in our app with headless Edge (Chromium) on Windows 11. The script above runs in Node 24. With the suggested fix, the same script
    leaves no blur listener after the focus/blur cycles and fires 3 pointerup events for 3 presses.

It does not affect CPU in practice, and everything is released on unmount. But a long-lived button gains a listener on every focus, and keyboard users get duplicated pointerup/pointercancel events.

Dominant language
TypeScript
Stars
34k
Forks
1.4k
Avg merge
22h 49m
Merged PRs (30d)
81

Getting set up

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from motiondivision/motion

All issues in motiondivision/motion

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.