Fix prompt popover positioning after Turbo history navigation
Turbo のヒストリ復元直後に、エディタが再接続されるタイミングでカーソル矩形が一瞬 0x0 になるため、Selection#cursorPosition が { x: 0, y: 0 } を返してポップオーバーが画面左端に固定されていました。この PR では、矩形が信頼できない場合は null を返し、ポップオーバーのアンカー処理をスキップすることで定位誤りを解消します。
背景
Turbo がページスナップショットをキャッシュし、ブラウザの Back/Forward で復元する際、エディタは DOM 再接続後すぐに選択情報を取得しようとしますが、レイアウトがまだ確立していないため Selection#cursorPosition は誤って { x: 0, y: 0 } を返していました。その結果、#positionPopover が x = 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
#positionPopover は cursorPosition が null の場合は早期リターンし、メニューを表示しません。また、カーソル入力中にポップオーバーが未アンカー状態であれば、再度 #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 が新たに導入され、矩形が信頼できないときに cursorPosition が null になること、復元後にメニューが左端に固定されないことを自動検証しています。テストは PR 前後で失敗/成功が逆転し、修正の効果を示しています。
設計判断
信頼できない座標は null として上位層に委譲 する方針が採用されました。{ x: 0, y: 0 } のデフォルトは実装上の便宜上便利ですが、UI の誤定位という副作用が顕在化したため、情報欠如を明示的に示す 設計へシフトしています。この変更は既存のロジックに最小限の分岐を加えるだけで、他のエディタ機能への影響はありません。
また、ポップオーバーの表示条件に data-anchored 属性を導入した点は、状態駆動の可視制御 を実装することで CSS 側でも安全に非表示を保証できるというトレードオフを示しています。属性ベースの制御はテスト容易性とデバッグ性を向上させ、将来的な UI 変更にも柔軟に対応できます。
まとめ
Turbo 復元直後に発生するカーソル矩形の不確実性を null で表現し、ポップオーバーのアンカー処理をスキップすることで、メニューが画面左端に固定されるバグを根本的に解消しました。変更は Selection の getter、Prompt のアンカーアルゴリズム、スタイルシートの可視条件の三層にまたがり、既存機能との互換性を保ちつつ UI の安定性を向上させています。