@mentionプロンプトでカンマキーによる選択を有効化
@mentionプロンプトが開いている状態でカンマキーを押すと、フォーカス中のオプションが選択されるようになりました。カンマはリテラルテキストとして保持されたまま、メンション補完が完了します。
背景
これまで、@mentionプロンプトが開いている状態でカンマキーを押すと、補完がキャンセルされてカンマがそのままテキストとして入力されていました。しかし、「@peter,」のように人名の後にカンマを続けて入力する記法はBasecampの既存動作として定着しており、ユーザーが期待する挙動と一致していませんでした。
PR本文によると、この変更はBasecampの既存動作に合わせるものと明示されています。エスケープキーによるキャンセルは既に実装されていたため、カンマキーの処理を同じキーダウンハンドラに追加する形で自然に実装できる状態でした。
技術的な変更
src/elements/prompt.js の LexicalPromptElement クラスにカンマキーのキーダウンハンドラが追加されました。既存のEscapeハンドラと並置する形で実装されています。
変更前:
import { $createTextNode, $isTextNode, COMMAND_PRIORITY_CRITICAL, KEY_ARROW_DOWN_COMMAND, ... } from "lexical"
変更後:
import { $createTextNode, $getSelection, $isRangeSelection, $isTextNode, COMMAND_PRIORITY_CRITICAL, KEY_ARROW_DOWN_COMMAND, ... } from "lexical"
importに $getSelection と $isRangeSelection が追加され、カンマ入力後のテキスト挿入にLexicalのSelection APIを使用しています。
カンマキーの処理は以下のように実装されました:
} else if (event.key === ",") {
event.preventDefault()
event.stopPropagation()
this.#optionWasSelected()
this.#editor.update(() => {
const selection = $getSelection()
if ($isRangeSelection(selection)) {
selection.insertText(",")
}
})
}
event.preventDefault() でブラウザのデフォルト入力を抑止した上で、#optionWasSelected() でメンション補完を確定し、続けて selection.insertText(",") でカンマをエディタに挿入しています。これにより、「選択の確定」と「カンマの保持」が順序保証された形で実行されます。
また、app/assets/javascript/lexxy.js には #getNextNodeFromTextEnd と #getPreviousNodeFromTextStart のバグ修正も含まれています。instanceof Fi による型チェックが Li(nextSibling) 関数呼び出しに置き換えられ、兄弟ノードがnullでない場合に null を返す処理が追加されました。#getPreviousNodeFromTextStart では parent.getPreviousSibling() が parent ? parent.getPreviousSibling() : null に修正され、nullアクセスを防ぐ安全策が追加されています。
さらに、Contents#insertDOM に #unwrapPlaceholderAnchors 呼び出しが追加されています。レンダリング済みビューからコピーされたコンテンツに含まれる <a href="#"> のような意味のないアンカーをアンラップし、テキストコンテンツをプレーンテキストとして貼り付けられるようにする処理です。
設計判断
event.preventDefault() → #optionWasSelected() → selection.insertText(",") の順序 が重要な設計ポイントです。ブラウザのデフォルト入力を先に止め、メンション確定の後でLexicalのAPIを通じてカンマを挿入することで、エディタの状態管理をLexicalのトランザクションに委ねています。直接DOMに書き込む代わりにLexical APIを使う一貫した方針が維持されています。
Enter・Tab・Spaceキーが既にLexical Commandsで処理されているのに対し、カンマキーはEscapeキーと同じネイティブキーダウンイベントのハンドラで処理されています。PR内の既存コメント「Arrow keys are now handled via Lexical commands with HIGH priority」が示すように、コマンドシステムを通じた処理とネイティブイベント処理が共存する設計になっており、カンマはEscapeと同様にポップオーバー層のUIロジックとして扱われています。
まとめ
カンマキーハンドラの追加という最小限の変更で、Basecampの既存UXに合わせた@mention確定フローが実現されました。event.preventDefault() でデフォルト入力を制御しつつLexical APIでカンマを再挿入するパターンは、エディタの状態管理をフレームワークに一本化する設計原則の適用例として参照できます。