代表的な面接トピック

フロントエンド面接:カスタムサイドバーはデバイスネイティブの閉じる要求にどう対応すべきか?

フロントエンド難しい
Offer.cc 編集チーム公開日 更新日

質問

未保存のフォームを保護しながら、デスクトップでは Escape、Android では戻るジェスチャーで閉じるドラッグ可能なモバイルサイドバーを設計してください。CloseWatcher、フォールバック、フォーカスの挙動について説明してください。

プロンプトとコンテキスト

ドラッグ可能なモバイルサイドバーを設計してください。デスクトップユーザーは Escape キーで閉じることを期待し、Android ユーザーは戻るジェスチャーまたはボタンを期待しており、未保存のフォームは確認を必要とする必要があります。CloseWatcher を使用して単一の閉じるフローを構築し、cancelcloserequestClose()destroy()、フォーカス、複数の watcher、非対応ブラウザ、および履歴の境界について説明してください。

MDN では、CloseWatcher をカスタムコンポーネントがデバイス固有の閉じるアクションに応答できるようにするためのインターフェースとして説明しています。HTML Standard でも、close-watcher のグループ化と履歴アクションの乱用に対する保護を定義しています。この記事は公開資料を統合したものであり、特定の企業固有の面接質問であることを主張するものではありません。

面接官がテストしていること

面接官は、あなたが閉じる要求(close request)と即時クローズを区別できるか、cancel フェーズでクローズを防止できるか、そして単一の close ハンドラーで UI のクリーンアップを担当し続けられるかを確認したいと考えています。優れた回答では、ユーザーアクティベーション、複数 watcher のグループ化、AbortSignal のライフタイム、フォーカスの復元、明示的なボタンのフォールバックに言及します。不十分な回答では keydown のみをリッスンします。

最初に明確にすべき質問

  • サイドバーはモーダル、非モーダル、あるいは履歴ナビゲーション状態に紐づいていますか?
  • 未保存のコンテンツは、すべての閉じる要求をブロックすべきですか、それとも特定のフィールドがダーティな場合のみですか?
  • 戻るジェスチャーはコンポーネントを閉じるべきですか、それとも前の履歴エントリに遷移すべきですか?
  • 対象ブラウザは CloseWatcher をサポートしていますか、また明示的な閉じるボタンへと機能を縮退させてもよいですか?

30秒の回答

「すべての閉じるエントリーポイントを閉じる要求として正規化します。サイドバーが開いたときに、AbortSignal を持つ CloseWatcher を作成します。cancel でフォームがダーティかどうかを確認し、ダーティであれば要求を防止して確認ダイアログを表示し、そうでなければ許可します。close でコンポーネントを非表示にし、フォーカスを復元してリソースをクリーンアップします。明示的な閉じるボタンも同じパスに従います。非対応ブラウザではボタンと限定的な Escape フォールバックを維持し、閉じられるコンポーネントがない場合はブラウザまたはルーターが戻るナビゲーションを所有します。」

ステップバイステップの解決策

2つのアクションを分離します。requestClose() はデバイスの閉じる要求をシミュレートして cancel を発火させます。それが防止されなければ、close が続きます。close()cancel を経由せずに直ちに close を発火させますが、destroy() は watcher を非アクティブ化するだけです。保存後、アンマウント時、またはルート離脱時には、意味を混同することなく、閉じる要求か強制クリーンアップかを明示的に選択します。

js
function openDrawer() {
  const controller = new AbortController();
  const watcher = new CloseWatcher({ signal: controller.signal });

  watcher.addEventListener("cancel", (event) => {
    if (!formIsDirty()) return;
    event.preventDefault();
    showDiscardConfirmation(() => watcher.close());
  });

  watcher.addEventListener("close", () => {
    hideDrawer();
    restoreFocusToTrigger();
    controller.abort();
  });

  return { watcher, controller };
}

確認ダイアログが再帰的に閉じられない watcher を作成してはなりません。破棄の明示的な確認の後、現在の watcher の close() を呼び出します。確認をキャンセルするとサイドバーは開いたままになります。保存後は、ダーティ状態をクリアして requestClose() を呼び出し、同じクリーンアップパスが実行されるようにします。

フォーカスの挙動はコンポーネントによって異なります。モーダルサイドバーは、わかりやすい見出しまたは最初のコントロールにフォーカスを移動し、閉じるときにトリガーに戻す必要があります。非モーダルパネルはフォーカスを奪うべきではありませんが、アクセス可能な閉じるボタンと可視状態が依然として必要です。閉じる要求は、CSS を切り替えるだけでなく、アクセシブルな名前、スクリム、スクロールロック、およびキーボードの順序を更新する必要があります。

複数の watcher には特別な境界があります。ユーザーアクティベーションがない場合、仕様により watcher をグループ化できるため、1つの閉じる要求で複数の watcher が閉じられる可能性があります。すべての小さなパネルに対して無条件に watcher を作成しないでください。watcher を所有する単一のトップレベルのクローズ可能コンポーネントを優先します。子はイベントを介してクローズを要求し、アンマウント時に destroy() を呼び出すか、関連付けられたシグナルを中止します。

戻るジェスチャーは単なるクリックではありません。プラットフォームはそれを履歴トラバーサルまたは閉じる要求として扱う可能性があり、ブラウザの close-watcher マネージャーがターゲットを選択します。アプリケーションは、インターセプト可能でありコンポーネントが開いている場合にのみ確認を行う必要があります。閉じられるコンポーネントがない場合は、通常の履歴の戻る動作が継続しなければなりません。1つのローカルフォームを保護するために popstate や戻るジェスチャーをグローバルにブロックしないでください。

機能検出によってコアパスを保護します。作成する前に window.CloseWatcher を確認してください。サポートされていない場合は、明示的な閉じるボタンを維持し、必要に応じて限定的な Escape リスナーを追加し、既存のルーターに戻る挙動を所有させます。カスタムリスナーが Android の戻るセマンティクスを完全に再現できると主張してはなりません。サポート状況、ブロックされた要求、確認後の破棄、およびフォーカス復元の失敗を測定します。

優れた回答例

すべてのサイドバーの閉じるエントリを閉じる要求として正規化します。開く際に、AbortSignal を持つ CloseWatcher を作成します。cancel は未保存の状態のみを確認します。ダーティな場合は preventDefault() を呼び出して確認を表示し、クリーンな場合は要求を許可します。破棄確認後、close() を呼び出します。保存後、ダーティ状態をクリアして requestClose() を呼び出します。close が単一の UI クリーンアップポイントとなり、パネルを非表示にし、トリガーにフォーカスを復元し、スクロールロックを解除し、watcher を終了します。

アクティブ化されていないインスタンスが誤ってグループ化されないよう watcher の数を制限します。アンマウントやルート変更によってそれらは破棄されます。モーダルコンポーネントと非モーダルコンポーネントには異なるフォーカスとスクリムのルールが適用され、コンポーネントが開いていない場合は戻るジェスチャーがブラウザの履歴に到達します。CloseWatcher のないブラウザでは、コアナビゲーションをブロックすることなく、明示的なボタンと限定的なキーボードフォールバックを維持します。メトリクスによって閉じる動作、確認、およびアクセシビリティの動作を検証します。

よくある間違い

  • 症状 → すべてのエントリーポイントで close() を使用する。失敗する理由 → 未保存の確認フェーズがスキップされる。修正方法 → ユーザーおよびプラットフォームの意図には requestClose() を使用し、強制クリーンアップには close() を使用する。
  • 症状 → Escape のみをリッスンする。失敗する理由 → Android の戻る動作やその他のデバイスの閉じるアクションが見落とされる。修正方法 → CloseWatcher を使用し、明示的なボタンを維持する。
  • 症状 → すべての子パネルに対して watcher を作成する。失敗する理由 → アクティブ化されていない watcher がグループ化される可能性がある。修正方法 → トップレベルのクローズ可能コンポーネントに1つのインスタンスを所有させる。
  • 症状 → 非表示ノードにフォーカスを残す。失敗する理由 → キーボードおよび支援技術のユーザーが位置を見失う。修正方法 → トリガーを保存し、close でフォーカスを復元する。
  • 症状 → 戻るイベントをグローバルにブロックする。失敗する理由 → コンポーネントが開いていないときに履歴ナビゲーションが壊れる。修正方法 → 確認が必要な場合にのみ、開いているコンポーネントの閉じる要求をブロックする。

フォローアップの質問と回答

requestClose()close() はいつ使い分けるべきですか?

cancel にクローズを防止する機会を与えるため、ユーザーまたはプラットフォームの意図には requestClose() を使用します。明示的な破棄確認の後、アンマウント時のクリーンアップ中、またはクローズが即時でなければならないときはいつでも close() を使用します。どちらも同じ close クリーンアップハンドラーに収束する必要があります。

未保存の確認ダイアログは独自の CloseWatcher を持つべきですか?

必ずしもそうではありません。親の watcher との再帰やグループ化されたクローズを避けるため、確認ダイアログには明示的な閉じるボタンとアクセス可能なフォーカスパスを提供します。親は確認後に close() を呼び出します。子を閉じると確認状態のみが変更されます。

複数の開いているパネルはどのように処理しますか?

スタックまたはトップレベルの所有権を定義します。1つの要求は、実際にクローズ可能な最上位のコンポーネントのみを閉じ、他のコンポーネントは開いたままにします。アクティブ化されていない watcher の暗黙的なグループ化をビジネススタックとして頼りにしないでください。アプリケーション状態で順序を記録しフォーカスを返します。

CloseWatcher がサポートされていない場合のフォールバックは何ですか?

明示的な閉じるボタンと基本的な Escape 処理を維持し、同じダーティ状態の確認、フォーカスの復元、およびクリーンアップ関数を再利用します。戻るジェスチャーや履歴をグローバルにインターセプトしないでください。機能強化を拡張するかどうかを決定する前に、縮退されたコホートを測定してください。

公開情報ソース

関連する質問