コンテンツにスキップ

フィルタポップオーバー

filter-popover は、ネイティブの popover と、結果領域をフィルタする htmx フォームの組み合わせです。ユーザーが Apply をクリックすると htmx が結果をスワップし、installClosePopover ビヘイビアが ポップオーバーを閉じます。

Filter を開いてステータスを選ぶか名前の一部を入力し、Apply を 押してみてください — htmx が api/recipes/filter-popover/ 配下のデモ エンドポイント(実際のアプリでは /items など自前の URL)へ GET を 送ってリストをスワップし、レスポンスが 2xx なので installClosePopover ビヘイビアがポップオーバーを閉じます。

<button class="hc-button" type="button" popovertarget="status-filter">
Status
</button>
<div id="status-filter" class="hc-popover" popover
data-side="bottom" data-align="start">
<form
data-hx-get="/items"
data-hx-include="this"
data-hx-target="#items-tbody"
data-hx-swap="innerHTML"
data-hx-push-url="true"
data-hc-close-popover-on-success>
<fieldset>
<legend>Status</legend>
<label><input type="checkbox" name="status" value="active"> Active</label>
<label><input type="checkbox" name="status" value="pending"> Pending</label>
<label><input type="checkbox" name="status" value="failed"> Failed</label>
</fieldset>
<div class="hc-toolbar">
<button class="hc-button" data-size="sm" type="reset">Reset</button>
<span data-hc-spacer="true"></span>
<button class="hc-button" data-size="sm" data-variant="primary"
type="submit">Apply</button>
</div>
</form>
</div>
<table class="hc-table">
<thead></thead>
<tbody id="items-tbody" data-hx-get="/items" data-hx-trigger="load"></tbody>
</table>
  1. ユーザーが Status をクリック → ブラウザがポップオーバーを開き ます(popovertarget + popover=auto)。
  2. ユーザーがチェックボックスを切り替えて Apply をクリック。
  3. フォームには data-hc-close-popover-on-successdata-hx-get="/items" があります。htmx がフォームデータつきの GET を 送信し、レスポンスを #items-tbody へスワップします。
  4. htmx:afterRequest が発火。installClosePopover ビヘイビアが オプトイン属性を確認し、リクエストが成功していれば、最寄りの [popover]hidePopover() を呼びます。
  5. data-hx-push-url="true" が URL を更新するため、フィルタはページ 再読み込みを生き延び、共有可能です。(上のライブデモが data-hx-push-url を省いているのは意図的です — ドキュメントページが 自分の URL を書き換えるわけにはいきません。)

キャンセルは外側をクリック(popover ネイティブのライトディスミス) するか Escape を押すだけ — JavaScript のフックは不要です。

フォームそのものがフィルタ状態です — すべてのコントロールがクエリ パラメータとして送られます。チェックボックスグループは name を繰り返し (?status=active&status=pending)、単一の <select>(ライブデモが そうです)は status を 1 値で送り、フリーテキスト欄は q=… を 加えます。サーバはフォームが宣言したものをそのまま読みます。

レスポンスはターゲットの innerHTML なので、フォームがターゲットに するコンテナに形を合わせる必要があります。上の <tbody id="items-tbody"> に対しては行を返します:

<tr id="item-12"></tr>
<tr id="item-15"></tr>

空の状態では、フィルタが実行されたことがユーザーに伝わるよう、明示的な 空状態マークアップを返します:

<tr>
<td colspan="3">
<p class="hc-field__message">No items match this filter.</p>
</td>
</tr>

ターゲットが素の <div> の場合(スキャフォールドとライブデモが そうです)は、代わりにリストやメッセージのフラグメントを返して ください — <tr> は table の外では HTML パーサに捨てられます。

適用そのもののトーストフィードバック(フィルタでは滅多に不要)には、 HX-Trigger: {"hc:toast":{…}} ヘッダーを含めます。

リクエストレスポンス
GET /items?status=active&status=pending&q=…(Apply)200 + ターゲットの innerHTML フラグメント — ポップオーバーは閉じます
フィルタに該当なし200 + 明示的な空状態フラグメント
失敗非 2xx — スワップされず、テーブルは行を保ち、ポップオーバーは開いたままです

htmx ≥ 2 は非 2xx レスポンスをスワップしないため、テーブルは直前の行を 保ちます — そして installClosePopover は成功時にしか閉じないので、 ポップオーバーは開いたままになり、ユーザーはやり直せます。明示的に 知らせるには、エラーレスポンスに HX-Trigger: {"hc:toast":{…}} ヘッダーを付けてください (トーストレシピを参照)。

  • ポップオーバーは popovertarget つきの本物の <button> で開かれ ます。フォーカス、ライトディスミス、Escape はブラウザが処理します。
  • ポップオーバーは自動的にメニューになるわけではありません。この フィルタフォームに role="menu" を設定するのは誤りです — これは フォームです。デフォルトの role のままにしてください。
  • 各 fieldset には見える <legend> を与えてください。legend の代わりに aria-label を使ったチェックボックス型フィルタは、視線で追いにくく なります。
  • ポップオーバーが閉じた後のトリガーに aria-current (例:「Status: 2 selected」)を付けると良い合図になります。件数は フィルタ状態からサーバサイドで描画してください。

プログレッシブエンハンスメント

Section titled “プログレッシブエンハンスメント”
  • JavaScript なしでは、ポップオーバーはただの <div> です — ページに 見えてしまいます。リセット CSS に [popover]:not(:popover-open) { display: none; } を足し、非 JS フォールバック(別 URL のフルページフィルタフォーム)を検討して ください。
  • hc.behaviors.js なしでも htmx のスワップは起こりますが、 ポップオーバーは開いたままです。これは無害です — ユーザーが手動で 閉じられます。