Fix prompt popover positioning after Turbo history navigation

basecamp/lexxy

Turbo のヒストリ復元直後に、エディタが再接続されるタイミングでカーソル矩形が一瞬 0x0 になるため、Selection#cursorPosition{ x: 0, y: 0 } を返してポップオーバーが画面左端に固定されていました。この PR では、矩形が信頼できない場合は null を返し、ポップオーバーのアンカー処理をスキップすることで定位誤りを解消します。

背景

Turbo がページスナップショットをキャッシュし、ブラウザの Back/Forward で復元する際、エディタは DOM 再接続後すぐに選択情報を取得しようとしますが、レイアウトがまだ確立していないため Selection#cursorPosition は誤って { x: 0, y: 0 } を返していました。その結果、#positionPopoverx = 0 に固定し、メニューが画面左端に固定されたままになるという定位回帰が QA に報告されました。従前の修正(#1121)でポップオーバー自体は Turbo キャッシュ前に除去されましたが、復元後の再表示位置が正しくなる保証がなくなっていたことが根本原因です。

技術的な変更

Selection の cursorPosition 判定変更

@@
-    let position = { x: 0, y: 0 }
+    let position = null
@@
-      if (!rect) return
+      if (this.#isRectUnreliable(rect)) return

変更前は矩形が取得できないときに { x: 0, y: 0 } を返し、変更後は矩形が信頼できなければ null を返すようになりました。これにより、呼び出し側は位置情報が取得できなかったことを正確に認識できます。

Prompt 要素のアンカー処理追加

@@
-    const { x, y, fontSize } = this.#selection.cursorPosition
+    const cursorPosition = this.#selection.cursorPosition
+    if (!cursorPosition) return
+    const { x, y, fontSize } = cursorPosition

#positionPopovercursorPositionnull の場合は早期リターンし、メニューを表示しません。また、カーソル入力中にポップオーバーが未アンカー状態であれば、再度 #positionPopover を呼び出すロジックが if (!this.popoverElement.hasAttribute("data-anchored")) で追加されています。

スタイルの可視条件拡張

@@
-:where(.lexxy-prompt-menu--visible) {
+:where(.lexxy-prompt-menu--visible[data-anchored]) {
   visibility: initial;
 }

ポップオーバーが data-anchored 属性を保持したときだけ可視になるよう変更し、未アンカー状態では画面に残らないようにしました。

テスト追加

Playwright テスト prompt_position_after_navigation.test.js が新たに導入され、矩形が信頼できないときに cursorPositionnull になること、復元後にメニューが左端に固定されないことを自動検証しています。テストは PR 前後で失敗/成功が逆転し、修正の効果を示しています。

設計判断

信頼できない座標は null として上位層に委譲 する方針が採用されました。{ x: 0, y: 0 } のデフォルトは実装上の便宜上便利ですが、UI の誤定位という副作用が顕在化したため、情報欠如を明示的に示す 設計へシフトしています。この変更は既存のロジックに最小限の分岐を加えるだけで、他のエディタ機能への影響はありません。

また、ポップオーバーの表示条件に data-anchored 属性を導入した点は、状態駆動の可視制御 を実装することで CSS 側でも安全に非表示を保証できるというトレードオフを示しています。属性ベースの制御はテスト容易性とデバッグ性を向上させ、将来的な UI 変更にも柔軟に対応できます。

まとめ

Turbo 復元直後に発生するカーソル矩形の不確実性を null で表現し、ポップオーバーのアンカー処理をスキップすることで、メニューが画面左端に固定されるバグを根本的に解消しました。変更は Selection の getter、Prompt のアンカーアルゴリズム、スタイルシートの可視条件の三層にまたがり、既存機能との互換性を保ちつつ UI の安定性を向上させています。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
45088539

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文→背景→技術的な変更→設計判断→まとめの構成が揃っており、各要素が明確に記述されています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライトが正しい形式で使用され、PRリンクも適切にマークダウンリンク化されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

エンジニア向けの専門的な内容で、初心者向けの余計な説明はありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論→結論の流れで構成され、段落はトピックセンテンスで始まり、1段落1トピックで適切な長さです。

Diff内容との照合 ⚠ WARNING

コードブロックとDiff内容の一致

src/editor/selection.js と src/elements/prompt.js の変更は概ね正確に反映されていますが、prompt.js の最初の変更(if (!this.popoverElement.hasAttribute("data-anchored")) { this.#positionPopover() })がコードブロックに含まれていません。本文で言及はあるものの、Diff全体をコードとして完全に提示していない点が軽微な欠落です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用された技術用語(Selection#cursorPosition、null、data-anchored、Turbo など)は PR と一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

根本原因・修正内容・検証手順の説明が PR の記載と合致しており、技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は PR タイトル、説明、Diff、テスト内容に裏付けられています。外部知識の付加はありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR 番号 #1144、ファイルパス、コードの行変更など数値情報は正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルは PR タイトルと完全に一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

LTS、EOL、リリース日程等の外部情報は一切含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「直後」等の時間表現は PR の記述と一致しています。