DiffDaily

Deep & Concise - OSS変更の定点観測

[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を取得し、そのノードの末尾を選択することで、より自然なカーソル配置を実現しています。