Android IMEのキーボード点滅を防ぐ、プロンプトナビゲーションのフォーカス管理改善
プロンプトメニューでのキー入力ごとに listItem.focus() が呼ばれ、Android IMEがソフトキーボードを閉じて再表示する問題を修正しました。ARIAの仕組みを活用することで、フォーカスを移動させずに選択状態をアナウンスできることを明示的に設計に反映しています。
背景
Android IMEはエディタフォーカスの喪失に敏感で、フォーカスが移動するたびにソフトキーボードを再構築します。これまでの #selectOption() の実装では、メニュー項目を aria-selected でマークした後に listItem.focus() を呼び出し、その直後に preservingSelection() でエディタにフォーカスを戻すという手順を踏んでいました。この「フォーカスを移してすぐ戻す」パターンが、プロンプトフィルタ文字を入力するたびにキーボードの消滅と再表示を引き起こしていました。
問題の根本は、listItem.focus() がARIA上の「選択状態のアナウンス」に必須だという誤った前提にありました。スクリーンリーダーへの通知には aria-activedescendant 属性の更新で十分であり、フォーカス移動は不要です。
技術的な変更
変更の核心は、#selectOption() からフォーカス操作を完全に排除し、スクロールを呼び出し側が制御するよう責務を分離した点です。
変更前:
#selectOption(listItem) {
this.#clearListItemSelection()
listItem.toggleAttribute("aria-selected", true)
listItem.scrollIntoView({ block: "nearest", behavior: "smooth" })
listItem.focus()
// Preserve selection to prevent cursor jump
this.#selection.preservingSelection(() => {
this.#editorElement.focus()
})
this.#setEditorAssociationAttribute("aria-controls", this.popoverElement.id)
this.#setEditorAssociationAttribute("aria-activedescendant", listItem.id)
}
変更後:
#selectOption(listItem, { scrollIntoView = false } = {}) {
this.#clearListItemSelection()
listItem.toggleAttribute("aria-selected", true)
if (scrollIntoView) {
listItem.scrollIntoView({ block: "nearest", container: "nearest", behavior: "smooth" })
}
this.#setEditorAssociationAttribute("aria-controls", this.popoverElement.id)
this.#setEditorAssociationAttribute("aria-activedescendant", listItem.id)
}
フォーカス操作が不要になったことで、その受け皿として作られていた Selection.preservingSelection() も不要となり、src/editor/selection.js から26行が削除されました。スクロールは #moveSelectionDown() / #moveSelectionUp() の呼び出し側で { scrollIntoView: true } を渡すことで制御し、フィルタ入力中の自動選択では scrollIntoView を省略(デフォルト false)できる設計になっています。
あわせて、新しいブラウザテスト filter_keeps_editor_focused.test.js が追加されました。Playwrightを用いて「フィルタ文字を入力し続けてもエディタがアクティブ要素のままであること」と「aria-activedescendant が正しく選択済みオプションを指していること」の2点を検証しています。特に、1回目の副作用だけでなく2回目以降のキーストロークでも挙動が正しいことを確認する構成は、この種の回帰バグへの意識が反映されています。
設計判断
ARIAのパターンに沿った実装への正規化が今回の選択の骨子です。コンポジットウィジェット(リストボックスなど)では、フォーカスをオーナー要素(エディタ)に保ちつつ aria-activedescendant で論理的な選択をアナウンスするのが標準的なARIAパターンです。変更前の実装は listItem.focus() でこのパターンから外れ、preservingSelection() という補正コードを必要としていました。
scrollIntoView をオプション引数化した判断も注目に値します。初期表示時や絞り込みで選択が自動更新される場合にスクロールを強制しないことで、ユーザーが意図的にキー操作した場合だけスクロールが発生します。これは「副作用を明示的に要求側が宣言する」設計原則の適用です。
まとめ
listItem.focus() を取り除き aria-activedescendant に一本化したことで、フォーカス管理のコードパスが大幅に単純化され、Android IMEの問題と preservingSelection() という補正コードを同時に解消しました。ARIAの標準パターンに従った実装が、特定プラットフォームの挙動問題の解決策にもなるという好例です。