Repository navigation
fix(frontend): v-hotkey.global で重なったモーダルの ESC が下のモーダルまで届かないように - #18090
Syogo-Suganoya wants to merge 1 commit into
Conversation
v-hotkey.global は要素ごとに document へ keydown リスナーを登録していたため、 同じキーを登録したものが登録順にすべて呼ばれ、stopPropagation() でも止まらなかった。 そのため、投稿フォームの上に出たダイアログで ESC を押すと、投稿フォームまで閉じていた。 v-hotkey.global の登録をまとめて管理し、後から登録されたものから順に照合して、 最初に処理したものより前のものには渡さないようにした。 非表示の要素や、閉じている途中で pointer-events: none の要素は対象外にする。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| export const makeHotkey = (keymap: Keymap, ignoreElements = IGNORE_ELEMENTS) => { | ||
| const actions = parseKeymap(keymap); | ||
| return (ev: KeyboardEvent) => { | ||
| return (ev: KeyboardEvent): boolean => { |
There was a problem hiding this comment.
z-index の挙動(#18066)
MkModal の中で最前面かどうかを判定して完結していたので、hotkey 側の返り値は不要だった。
hotkey の挙動(この PR)
振り分けを hotkey 側で行うため、「このキーを処理したか」を boolean で返す。
- いずれかのアクションを実行した →
true(ここで止める) - 入力欄にフォーカスがある / どのアクションにも一致しない →
false(次に渡す)
既存の呼び出し元(MkGrid など)は返り値を使っていないので、挙動は変わりません。
Generated by Claude Code
| }; | ||
|
|
||
| // v-hotkey.global の登録順に並べる(後ろほど後から登録されたもの) | ||
| const globalHotkeys: GlobalHotkey[] = []; |
There was a problem hiding this comment.
z-index の挙動(#18066)
表示中のモーダルのうち、z-index が最大のものを最前面とする。
hotkey の挙動(この PR)
v-hotkey.global が登録された順番で判定する。
配列の後ろほど後から開いたもの=上に出ているもの、として扱う。
モーダルは開くときにマウントされ、
os.claimZIndex() も開いた順に大きくなるので、
ほとんどの場合は z-index の順と一致します。
ずれるのは priority(low / middle / high)の帯をまたぐときだけです。
例えば high のメニューを開いたまま、後から middle のダイアログを開くと、
見た目はメニューが上なのに、判定はダイアログが先になります。
Generated by Claude Code
| const globalHotkeys: GlobalHotkey[] = []; | ||
|
|
||
| // 非表示の要素や、閉じている途中で操作を受け付けない要素(pointer-events: none)はキー入力も受け取らない | ||
| function isActive(el: HTMLElement) { |
There was a problem hiding this comment.
z-index の挙動(#18066)
表示中のモーダルの z-index を Set に持ち、showing / manualShowing の watch で出し入れする。
hotkey の挙動(この PR)
状態を持たず、キーが押された時点の DOM の状態で判定する。
getClientRects()が空 →v-showでdisplay: none(manualShowingで隠れているものなど)- インラインの
pointer-events: none→ 閉じ始めたMkModal/MkLightbox
どちらも #18066 の watch で外していたのと同じ状態なので、
閉じアニメーション中にもう一度 ESC を押すと下に届く挙動は同じです。
Generated by Claude Code
| for (let i = globalHotkeys.length - 1; i >= 0; i--) { | ||
| const { el, keyHandler } = globalHotkeys[i]; | ||
| if (!isActive(el)) continue; | ||
| if (keyHandler(ev)) return; |
There was a problem hiding this comment.
z-index の挙動(#18066)
最前面でないモーダルは、どのキーも処理しない。
hotkey の挙動(この PR)
上のものが処理しなかったキー(false が返ったもの)は、下のものに渡る。
例えば MkLightbox の上にダイアログが出ていて、
ダイアログが ESC しか登録していない場合、
キーは MkLightbox に届いて画像が切り替わる想定です。
Generated by Claude Code
| }, | ||
|
|
||
| beforeUnmount(el) { | ||
| const index = globalHotkeys.findIndex(x => x.el === el); |
There was a problem hiding this comment.
z-index の挙動(#18066)
閉じずにアンマウントされたとき Set に z-index が残らないよう、onUnmounted でも外す必要がある。
hotkey の挙動(この PR)
ディレクティブの beforeUnmount で配列から外すので、閉じたかどうかに関係なく必ず外れる。
配列が空になったら document のリスナーも外します。
通常の v-hotkey は従来どおり AbortController で外します。
Generated by Claude Code
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
ハンドラーの処理結果 packages/frontend/src/utility/hotkey.ts |
makeHotkeyが返すハンドラーは、無視対象や一致するアクションがない場合にfalseを返します。一致したアクションを実行した場合はtrueを返します。 |
グローバルホットキーの共有ディスパッチ packages/frontend/src/directives/hotkey.ts |
グローバルホットキーを共有リストで管理し、新しい登録から処理します。アクティブでない要素はスキップし、ハンドラーが処理した時点で終了します。非グローバル登録にはAbortControllerを使います。最後のグローバル登録を解除すると、共有documentリスナーも削除します。 |
Estimated code review effort: 2 (Simple) | ~10 minutes
Change: Bug fix
Sequence Diagram(s)
sequenceDiagram
participant document
participant onGlobalKeydown
participant globalHotkeys
participant makeHotkeyHandler
document->>onGlobalKeydown: keydownイベント
onGlobalKeydown->>globalHotkeys: 登録を新しい順に照合
onGlobalKeydown->>makeHotkeyHandler: アクティブ要素のハンドラーを呼び出す
makeHotkeyHandler-->>onGlobalKeydown: 処理結果のboolean
Possibly related PRs
- misskey-dev/misskey#18066:
MkModalでz-indexが最大のモーダルだけにESCを処理させます。本PRも重なったモーダルのESC処理を変更します。
Suggested labels: packages/frontend
Suggested reviewers: syuilo
Merge Risk: 
Minimal · up to c7ebb
-
No actionable merge-blocking issue remains in the reviewed hotkey change; normal checks can proceed.
Pre-merge checks: 
5
-
Full details: Description check
-
Explanation
PR説明は What と Why、Additional info、Checklist を含み、テンプレートの構造に沿っています。What では
#18066の z-index 方式からhotkey方式への変更点を比較表で説明しています。Why では、z-index に依存せず hotkey 側で直す方が良いという指摘に基づく理由を示しています。テストの追加とStorybook、CHANGELOG.md の更新は未チェックですが、必須ではない項目です。全体として概ね完成しています。
Full details: Docstring Coverage
-
Explanation
Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 2 functions across 2 files.
-
Finishing Touches
-
Generate unit tests (beta)
-
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
キー入力が届きます
登録を新しい順にたどります
処理したらそこで止まります
解除すればリスナーも消えます
ホットキーの道筋が整います
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #18090 +/- ##
========================================
Coverage 12.78% 12.79%
========================================
Files 227 228 +1
Lines 11181 11206 +25
Branches 3697 3698 +1
========================================
+ Hits 1430 1434 +4
- Misses 7664 7679 +15
- Partials 2087 2093 +6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
🖥 Frontend Diagnostics Report
Only metrics showing significant changes are displayed. Requests by resource type
V8 heap snapshot statistics
📦 Bundle StatsChunk size diff (13 updated, 0 added, 3 removed)
Startup chunk size (1 updated, 0 added, 0 removed)
Startup chunks are the Vite entry for
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
What
#18066 で
MkModal側に入れていた z-index による最前面判定をやめ、hotkey 側で直す形に作り直した。MkModal.vuedirectives/hotkey.ts,utility/hotkey.tsv-hotkey.globalから順に照合し、最初に処理したところで止めるMkModalのみv-hotkey.globalを使うもの(MkModal,MkLightbox)showingが false になった時点で判定から外すpointer-events: noneの要素は飛ばすWhy
#18066 で、z-index に依存せず hotkey 側で直す方が良いというコメントがあったため。
Additional info (optional)
スクリーンショットは #18066 のものと同じになる。
Checklist
🤖 Generated with Claude Code