[BUG] Keyboard press leaves a listener on every focus and on every Enter press (duplicated pointerup)
メンテナーはふだん 1 日以内に返信
関連するプルリクエストがすでにマージされています。
- #3953 @mattgperry による — マージ済み
評価
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 初心者へのやさしさ
- 74/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- javascript
- 領域
- frontend
調査の方向性
motion-dom の gestures/press/utils/keyboard.mjs、enableKeyboardPress から始めてください。ここでは keydown、keyup、blur のリスナーが追加されており、keydown を削除する blur リスナーが一度も削除されていません。issue には、有効なリスナー数と発火した pointerup イベント数を数える自己完結型の Node スクリプトが含まれているので、変更の前後で実行してください。完了条件は、focus/blur を繰り返した後も blur リスナーの数が 1 のままであり、Enter キーを押すたびに pointerup が 1 回だけ発生することです。
索引モデルが issue の本文から書いたものです。
説明
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 ablurlistener that removes thekeydownlistener, but theblurlistener itself is never removed. Each focus/blur cycle leaves one moreblurlistener on the element. - On every Enter
keydown, it adds akeyuplistener and ablurlistener, and neither is ever removed. After k Enter presses, one Enterkeyupdispatches k syntheticpointerupevents, and oneblurdispatches kpointercancelevents.
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
- Render any element with
whileTap(or callpress()on it). - Focus and blur it several times, or press Enter on it several times.
- Count its listeners:
blurgrows by one per focus,keyupandblurby one per Enter press, and Enterkeyupfires duplicatedpointerupevents.
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
motion13.4 /motion-dom13.3.0 in our app; the same code is inmotion-dom14.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 noblurlistener after the focus/blur cycles and fires 3pointerupevents 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.
- 主要言語
- TypeScript
- スター
- 34.6k
- フォーク
- 1.5k
- 平均マージ
- 1日 2時間
- マージ済み PR(30日)
- 118
環境構築
このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
motiondivision/motion のほかの issue
-
[BUG] inline border-radius stretched when using animateLayout()対応中かも @mattgperry が今日担当しました。 オープンbug
難易度 4/5 3〜5日 初心者へのやさしさ 18/100
motiondivision/motion#4040 ·
メンテナーはふだん 1 日以内に返信
-
[BUG] Memory leak in Safari: WAAPI onfinish keeps removed elements and their props alive対応中かも @mattgperry が 3 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 35/100
motiondivision/motion#4034 ·
メンテナーはふだん 1 日以内に返信
motiondivision/motion の issue をすべて見る
似ている issue
-
triage
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
メンテナーはふだん 1 日以内に返信
-
🎙️ task - drop the raw `./node_modules/.bin/rhachet roles boot` line from bhuild's feedback templateオープン
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
ehmpathy/rhachet-roles-bhuild#639 ·
メンテナーはふだん 1 日以内に返信
-
e2e-failure ready-to-code
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
redhat-developer/rhdh-plugin-export-overlays#4650 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
メンテナーはふだん 13 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
omdsh-dev/DSH-better-sidebar#1098 ·
メンテナーはふだん 2 日以内に返信