[basecamp/lexxy] エディタのフォーカス処理改善とautofocus属性のサポート

basecamp/lexxy

背景

Lexxyエディタにおいて、フォーカス時の挙動に問題がありました。特に空のエディタにフォーカスした際、テキストを入力すると意図しない改行が挿入されるバグが発生していました。また、HTML標準のautofocus属性がサポートされていませんでした。

主な変更内容

1. 空エディタのフォーカス処理の改善

空のエディタにフォーカスする際、ルートノードではなく最後のTextNodeの末尾にカーソルを配置するよう変更されました。

変更前:

placeCursorAtTheEnd() {
  this.editor.update(() => {
    $getRoot().selectEnd()
  })
}

変更後:

placeCursorAtTheEnd() {
  this.editor.update(() => {
    const root = $getRoot()
    const lastDescendant = root.getLastDescendant()

    if (lastDescendant && $isTextNode(lastDescendant)) {
      lastDescendant.selectEnd()
    } else {
      root.selectEnd()
    }
  })
}

この変更により、空のエディタでテキスト入力時に先頭に改行が挿入される問題が解決されました。ルートノードを直接選択すると、Lexicalの内部処理で新しいパラグラフノードが作成される際に予期しない改行が発生していましたが、TextNodeの末尾を選択することでこの問題を回避しています。

2. autofocus属性のサポート

HTML標準のautofocus属性がサポートされ、ページ読み込み時に自動的にエディタにフォーカスできるようになりました。

connectedCallback() {
  // ...
  this.#handleAutofocus()
  // ...
}

#handleAutofocus() {
  if (!document.querySelector(":focus")) {
    if (this.hasAttribute("autofocus") && document.querySelector("[autofocus]") === this) {
      this.focus()
    }
  }
}

実装のポイント:
- 他の要素がすでにフォーカスを持っている場合は何もしない
- 複数の要素にautofocusが指定されている場合、DOM順で最初の要素のみがフォーカスを取得
- これはHTML標準のautofocusの挙動に準拠

3. focus()メソッドの改良

focus()メソッドが、空のエディタの場合に自動的にカーソルを適切な位置に配置するよう改良されました。

focus() {
  this.editor.focus(() => this.#onFocus())
}

#onFocus() {
  if (this.isEmpty) {
    this.selection.placeCursorAtTheEnd()
  }
}

使用例

ERBテンプレートでの使用:

<%= form.rich_text_area :body, 
  placeholder: "Write something...",
  autofocus: true,
  attachments: true,
  markdown: true
%>

テストケース

新たにFocusTestが追加され、以下のシナリオがカバーされました:

test "text after focus doesn't add new line" do
  find_editor.focus
  find_editor.send "Hello there"

  assert_editor_html "<p>Hello there</p>"
end

test "autofocus attribute" do
  visit edit_post_path(posts(:empty), autofocus: true)
  assert_editor_has_focus
end

これらのテストにより、フォーカス時の改行挿入バグが修正されたこと、およびautofocus属性が正しく機能することが保証されています。

技術的な詳細

Lexicalエディタの内部では、ルートノードに対してselectEnd()を呼び出すと、新しいコンテンツを挿入する際に空のパラグラフノードが作成され、結果として改行が挿入されていました。この問題を解決するため、getLastDescendant()でツリーの最後のTextNodeを取得し、そのノードの末尾を選択することで、より自然なカーソル配置を実現しています。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

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

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

ガイドライン準拠 ✓ PASS

記事構成とDiffDaily Styleへの準拠状況

記事構成の必須3要素(Title, Context, Technical Detail)が明確に記載されています。また、コードブロック前後の空行やファイル名付きシンタックスハイライトといったカスタムMarkdown構文が正しく使用されており、ガイドラインを完全に遵守しています。

  • 記事構成(Title、Context、Technical Detail)
  • DiffDaily Styleガイド準拠
  • カスタムMarkdown活用
  • 対象読者への適合性
技術的整合性 ✓ PASS

技術的な正確性と表現の適切性

コードの変更点を正確に引用し、その技術的な意味合いを的確に解説しています。特に、Lexicalの`$getRoot().selectEnd()`の挙動と、それを回避するための`getLastDescendant()`の利用という核心部分の説明が論理的で正確です。テストコードの引用も変更の妥当性を示しており、技術的整合性は非常に高いです。

  • 技術用語の正確性
  • コード例の正確性
  • 説明の技術的正確性
PR内容との整合性 ✓ PASS

元のPR情報との一致度

PRのタイトル「Focus improvements」とコード変更の内容から、解決された課題(意図しない改行バグ、autofocus属性の欠如)を正確に導き出し、記事の背景として説明できています。PR情報に基づかない憶測やハルシネーションは見られず、PRの内容と記事の主張は完全に一致しています。

  • タイトル・説明の一致
  • Diff内容の正確な反映
  • 推測の排除