Android IMEのキーボード点滅を防ぐ、プロンプトナビゲーションのフォーカス管理改善

basecamp/lexxy

プロンプトメニューでのキー入力ごとに 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の標準パターンに従った実装が、特定プラットフォームの挙動問題の解決策にもなるという好例です。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
d09ee909

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という構成が明確で、ガイドラインに完全に準拠しています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```javascript:src/elements/prompt.js)の形式、フッターのPRリンク記法([PR #1059](URL))ともに正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

ARIAのパターンやフォーカス管理、Playwrightテストなど、専門知識を持つエンジニアを対象とした適切な技術レベルと表現で書かれています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクション、各パラグラフともに「総論→各論」の構造が徹底されています。各段落はトピックセンテンスで始まり、1段落1トピックが守られており、可読性が非常に高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内のコードブロックは、`src/elements/prompt.js`の変更前後を正確に反映しています。また、`src/editor/selection.js`から26行が削除されたという記述もDiff情報と一致しており、正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`aria-activedescendant`、`コンポジットウィジェット`、`Android IME`などの技術用語が、PRの文脈に沿って正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

「`listItem.focus()`がキーボード点滅の原因である」「`aria-activedescendant`の更新で十分」という説明は、PR Descriptionの内容と完全に一致しており、技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張(`preservingSelection`の削除、テストの追加など)は、提供されたPRのDescriptionやDiff情報によって裏付けられており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#1059)、削除されたコードの行数(26行)など、記事に含まれる数値や固有名詞はすべて正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルは、PRの「Prevent loss of editor focus during prompt navigation」という主題を、より具体的な現象(Android IMEの点滅)に触れつつ的確に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

「標準的なARIAパターン」といった一般知識に言及していますが、これはPRの変更内容を解説するための妥当な説明であり、PR情報にない外部知識の捏造は見られません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「これまでの実装」「変更前」といった時間表現は、PRの文脈と一致しており、歪曲や誤解を招く表現はありません。