Skip to content

fix(frontend): v-hotkey.global で重なったモーダルの ESC が下のモーダルまで届かないように - #18090

Open
Syogo-Suganoya wants to merge 1 commit into
misskey-dev:developfrom
Syogo-Suganoya:fix-14388-hotkey-esc
Open

Syogo-Suganoya wants to merge 1 commit into
misskey-dev:developfrom
Syogo-Suganoya:fix-14388-hotkey-esc

Conversation

@Syogo-Suganoya

Copy link
Copy Markdown
Contributor

What

#18066 で MkModal 側に入れていた z-index による最前面判定をやめ、hotkey 側で直す形に作り直した。

#18066(z-index) この PR(hotkey)
変更箇所 MkModal.vue directives/hotkey.ts, utility/hotkey.ts
最前面の判定 表示中のモーダルのうち z-index が最大のもの 後から登録された v-hotkey.global から順に照合し、最初に処理したところで止める
対象 MkModal のみ v-hotkey.global を使うもの(MkModal, MkLightbox)
閉じている途中のもの showing が false になった時点で判定から外す pointer-events: none の要素は飛ばす
最前面以外のもの キーを処理しない 上のものが処理しなかったキーは受け取る

Why

#18066 で、z-index に依存せず hotkey 側で直す方が良いというコメントがあったため。

Additional info (optional)

スクリーンショットは #18066 のものと同じになる。

Checklist

  • Read the contribution guide
  • Test working in a local environment
  • (If needed) Add story of storybook
  • (If needed) Update CHANGELOG.md
  • (If possible) Add tests

🤖 Generated with Claude Code

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 => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

z-index の挙動(#18066)

MkModal の中で最前面かどうかを判定して完結していたので、hotkey 側の返り値は不要だった。

hotkey の挙動(この PR)

振り分けを hotkey 側で行うため、「このキーを処理したか」を boolean で返す。

  • いずれかのアクションを実行した → true(ここで止める)
  • 入力欄にフォーカスがある / どのアクションにも一致しない → false(次に渡す)

既存の呼び出し元(MkGrid など)は返り値を使っていないので、挙動は変わりません。


Generated by Claude Code

};

// v-hotkey.global の登録順に並べる(後ろほど後から登録されたもの)
const globalHotkeys: GlobalHotkey[] = [];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

z-index の挙動(#18066)

最前面でないモーダルは、どのキーも処理しない。

hotkey の挙動(この PR)

上のものが処理しなかったキー(false が返ったもの)は、下のものに渡る。


例えば MkLightbox の上にダイアログが出ていて、
ダイアログが ESC しか登録していない場合、
キーは MkLightbox に届いて画像が切り替わる想定です。


Generated by Claude Code

},

beforeUnmount(el) {
const index = globalHotkeys.findIndex(x => x.el === el);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

z-index の挙動(#18066)

閉じずにアンマウントされたとき Set に z-index が残らないよう、onUnmounted でも外す必要がある。

hotkey の挙動(この PR)

ディレクティブの beforeUnmount で配列から外すので、閉じたかどうかに関係なく必ず外れる。


配列が空になったら document のリスナーも外します。
通常の v-hotkey は従来どおり AbortController で外します。


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 3e3239f and c7ebb7e.

Files selected for processing: 2
  • packages/frontend/src/directives/hotkey.ts
  • packages/frontend/src/utility/hotkey.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Walkthrough

ホットキーハンドラーが処理結果をbooleanで返すようになりました。グローバルホットキーは共有リストとdocumentリスナーで処理します。非グローバルホットキーはAbortControllerで解除します。

Changes: ホットキーの処理

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
Loading

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: Passed 5
Check name Status Explanation
Title check Passed PRタイトルは、v-hotkey.global で重なったモーダルの ESC が下のモーダルまで届かないようにする、という主な変更を具体的に示しています。変更内容(directives/hotkey.ts と utility/hotkey.ts におけるグローバルホットキーの照合順と戻り値の変更)と整合しています。タイトルは短く、単一の文で表現されています。
Description check Passed PR説明は What と Why、Additional info、Checklist を含み、テンプレートの構造に沿っています。What では #18066 の z-index 方式からhotkey方式への変更点を比較表で説明しています。Why では、z-index に依存せず hotkey 側で直す方が良いという指摘に基づく理由を示しています。テストの追加とStorybook、CHANGELOG…
Docstring Coverage Passed 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. Documentat…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
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.

Share

キー入力が届きます
登録を新しい順にたどります
処理したらそこで止まります
解除すればリスナーも消えます
ホットキーの道筋が整います

@codecov

codecov Bot commented Oct 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 3.70370% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 12.79%. Comparing base (e74b11c) to head (c7ebb7e).
⚠️ Report is 8 commits behind head on develop.

Files with missing lines Patch % Lines
packages/frontend/src/directives/hotkey.ts 4.76% 14 Missing and 6 partials ⚠️
packages/frontend/src/utility/hotkey.ts 0.00% 4 Missing and 2 partials ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown
Contributor

🖥 Frontend Diagnostics Report

Metric @ Base @ Head Δ MAD
Requests 246
± 0
249
± 0
$\color{orange}{\text{+3}}$
$\color{orange}{\text{+1.2\%}}$
0
Encoded network 3.2 MB
± 7 B
3.3 MB
± 1 B
$\color{orange}{\text{+110 KB}}$
$\color{orange}{\text{+3.4\%}}$
7 B
Decoded body 2.9 MB
± 0 B
3 MB
± 0 B
$\color{orange}{\text{+109 KB}}$
$\color{orange}{\text{+3.7\%}}$
0 B
Same-origin encoded 3 MB
± 0 B
3.1 MB
± 0 B
$\color{orange}{\text{+110 KB}}$
$\color{orange}{\text{+3.6\%}}$
0 B
Script encoded 2 MB
± 0 B
2.1 MB
± 0 B
$\color{orange}{\text{+106 KB}}$
$\color{orange}{\text{+5.2\%}}$
0 B

Only metrics showing significant changes are displayed.

View details

Requests by resource type
Type Requests Encoded bytes
Base Head Δ Base Head Δ
Document 2 2 0 33 KB 33 KB $\color{green}{\text{-32 B}}$
Script 146 148 $\color{orange}{\text{+2}}$ 2 MB 2.1 MB $\color{orange}{\text{+106 KB}}$
Stylesheet 57 58 $\color{orange}{\text{+1}}$ 281 KB 284 KB $\color{orange}{\text{+3.5 KB}}$
Fetch 20 20 0 46 KB 46 KB $\color{green}{\text{-8 B}}$
Image 10 10 0 254 KB 254 KB $\color{green}{\text{-16 B}}$
Font 2 2 0 118 KB 118 KB 0 B
Other 9 9 0 421 KB 421 KB $\color{green}{\text{-8 B}}$
V8 heap snapshot statistics
Metric @ Base @ Head Δ MAD
$\color{gray}{\rule{8pt}{8pt}}$ Total 11 MB
± 14 KB
11 MB
± 5.2 KB
$\color{orange}{\text{+131 KB}}$
$\color{orange}{\text{+1.2\%}}$
15 KB
$\color{orange}{\rule{8pt}{8pt}}$ Code 2.8 MB 2.8 MB $\text{+38 KB}$ 5.7 KB
$\color{red}{\rule{8pt}{8pt}}$ Strings 1.3 MB 1.3 MB $\text{+21 KB}$ 303 B
$\color{cyan}{\rule{8pt}{8pt}}$ JS arrays 112 KB 112 KB $\text{+700 B}$ 56 B
$\color{green}{\rule{8pt}{8pt}}$ Typed arrays 0 B 0 B 0 B 0 B
$\color{yellow}{\rule{8pt}{8pt}}$ System objects 0 B 0 B 0 B 0 B
$\color{violet}{\rule{8pt}{8pt}}$ Other JS objs 2 MB 2 MB $\text{+17 KB}$ 1.6 KB
$\color{pink}{\rule{8pt}{8pt}}$ Other non-JS objs 4.9 MB 5 MB $\text{+66 KB}$ 6.3 KB

Download representative heap snapshot: base / head

📦 Bundle Stats

Chunk size diff (13 updated, 0 added, 3 removed)
Chunk Base Head Δ Δ (%)
(total) 6 MB 6 MB $\color{orange}{\text{+6.4 KB}}$ $\color{orange}{\text{+0.1\%}}$
src/components/MkSignupDialog.vue ja-JP/sQ48aJMKG-DHkPBa1T.js → ja-JP/sQ48aJMKG-C2LAWbaG.js
18 KB 50 B $\color{green}{\text{-18 KB}}$ $\color{green}{\text{-99.7\%}}$
src/components/MkVisitorDashboard.ActiveUsersChart.vue ja-JP/sQ48aJMKG-DZ9VOaz0.js
2 KB 0 B $\color{green}{\text{-2 KB}}$ $\color{green}{\text{( - )}}$
src/components/MkSigninDialog.vue ja-JP/sQ48aJMKG-C4-pkeIx.js → ja-JP/sQ48aJMKG-DvbGy63G.js
1.3 KB 50 B $\color{green}{\text{-1.2 KB}}$ $\color{green}{\text{-96\%}}$
src/ui/visitor.vue ja-JP/sQ48aJMKG-BVhsjP23.js → ja-JP/sQ48aJMKG-0cdfkijP.js
4.2 KB 3.1 KB $\color{green}{\text{-1.1 KB}}$ $\color{green}{\text{-26.5\%}}$
src/_boot_.ts ja-JP/sQ48aJMKG-CSmQ-iE9.js → ja-JP/sQ48aJMKG-TyIFaz7z.js
65 KB 65 KB $\text{+815 B}$ $\color{orange}{\text{+1.3\%}}$
src/ui/deck.vue ja-JP/sQ48aJMKG-qiOMDlwd.js → ja-JP/sQ48aJMKG-b_MAROaB.js
41 KB 41 KB $\text{-91 B}$ $\color{green}{\text{-0.2\%}}$
../frontend-shared/js/theme-code.ts ja-JP/sQ48aJMKG-r_Mx_e6A.js
57 B 0 B $\text{-57 B}$ $\color{green}{\text{( - )}}$
src/components/MkVisitorDashboard.vue ja-JP/sQ48aJMKG-CniDh_tY.js
50 B 0 B $\text{-50 B}$ $\color{green}{\text{( - )}}$
src/pages/settings/theme.vue ja-JP/sQ48aJMKG-D3j4ZJGx.js → ja-JP/sQ48aJMKG-DcPF-tx1.js
15 KB 15 KB $\text{-37 B}$ $\color{green}{\text{-0.2\%}}$
src/pages/install-extensions.vue ja-JP/sQ48aJMKG-B8H1REkJ.js → ja-JP/sQ48aJMKG-CvYTFj6p.js
14 KB 14 KB $\text{-37 B}$ $\color{green}{\text{-0.3\%}}$
src/pages/settings/theme.manage.vue ja-JP/sQ48aJMKG-UPgT7uMr.js → ja-JP/sQ48aJMKG-7m5ET11X.js
2.1 KB 2.1 KB $\text{-37 B}$ $\color{green}{\text{-1.7\%}}$
src/pages/settings/theme.install.vue ja-JP/sQ48aJMKG-DJ5qWBWW.js → ja-JP/sQ48aJMKG-BRN5ni-O.js
1.6 KB 1.5 KB $\text{-37 B}$ $\color{green}{\text{-2.4\%}}$
src/components/MkUpdated.vue ja-JP/sQ48aJMKG-xrBRLJus.js → ja-JP/sQ48aJMKG-D6fQaC77.js
1.6 KB 1.5 KB $\text{-24 B}$ $\color{green}{\text{-1.5\%}}$
src/pages/about.overview.vue ja-JP/sQ48aJMKG-CdJhnaPp.js → ja-JP/sQ48aJMKG-AU_bMmnM.js
5.3 KB 5.3 KB $\text{-8 B}$ $\color{green}{\text{-0.2\%}}$
(other generated chunks) 2 MB 2 MB $\color{orange}{\text{+28 KB}}$ $\color{orange}{\text{+1.4\%}}$
(other) 8.1 KB 8.1 KB 0 B 0%
Startup chunk size (1 updated, 0 added, 0 removed)
Chunk Base Head Δ Δ (%)
(total) 1 MB 1 MB $\color{orange}{\text{+42 KB}}$ $\color{orange}{\text{+4.2\%}}$
src/_boot_.ts ja-JP/sQ48aJMKG-CSmQ-iE9.js → ja-JP/sQ48aJMKG-TyIFaz7z.js
65 KB 65 KB $\text{+815 B}$ $\color{orange}{\text{+1.3\%}}$
(other generated chunks) 718 KB 760 KB $\color{orange}{\text{+41 KB}}$ $\color{orange}{\text{+5.8\%}}$
(other) 219 KB 219 KB 0 B 0%

Startup chunks are the Vite entry for src/_boot_.ts and its static imports.

Bundles Modules Entries Imports Size
Static Dynamic Rendered Gzip Brotli
Base 472 2,849 21 10,312 338 10 MB 2.8 MB 2.4 MB
Head 469 2,856 21 10,339 333 10 MB 2.8 MB 2.4 MB
Δ $\color{green}{\text{-3}}$ $\color{orange}{\text{+7}}$ 0 $\color{orange}{\text{+27}}$ $\color{green}{\text{-5}}$ $\color{orange}{\text{+20 KB}}$ $\color{orange}{\text{+7 KB}}$ $\color{orange}{\text{+6 KB}}$
Δ (%) $\color{green}{\text{-0.6\%}}$ $\color{orange}{\text{+0.2\%}}$ 0% $\color{orange}{\text{+0.3\%}}$ $\color{green}{\text{-1.5\%}}$ $\color{orange}{\text{+0.2\%}}$ $\color{orange}{\text{+0.2\%}}$ $\color{orange}{\text{+0.2\%}}$

Open treemap HTML

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

packages/frontend Client side specific issue/PR

Projects

Development

Successfully merging this pull request may close these issues.

1 participant