並べ替えリスト
サーバが順序を所有するリスト — カンバン列、優先順リスト、ピン留め
ダッシュボード — には、「誰が決めるか」に忠実なクライアントサイド
並べ替えが必要です。installSortable ビヘイビアは DOM ノードを動かして
確定した順序を報告するだけで、ネットワークは htmx が所有します。
各アイテムが自分の hidden input を持つため、アイテムを動かせば input も
動き、新しい順序は追加の管理コードゼロでリクエスト 1 本に
シリアライズされます — すべてマークアップから、インライン JS なしで。
別名: ドラッグ&ドロップ並べ替え。
下のプレビューはそのままライブです — 並べ替えはクライアントサイド なので、ハンドルをドラッグする(またはフォーカスして Space、続けて矢印キー)とリストが並び替わります。結果の 永続化は htmx 属性ひとそろい(下記)で完了します。
- リリースノートを公開
- オープン PR をレビュー
- ロードマップを更新
<ul class="hc-stack" data-hc-sortable> <li class="hc-item" data-hc-sortable-id="notes"> <button type="button" class="hc-button" data-variant="ghost" data-size="sm" data-hc-sortable-handle>⠿</button> <span class="hc-item__title">リリースノートを公開</span> <input type="hidden" name="order[]" value="notes"> </li> <!-- … 他のアイテム … --></ul>動作:
data-hc-sortableがコンテナをマークし、その要素子が並べ替え対象の アイテムになります。- すべての
data-hc-sortable-handleはインストール時(および htmx で スワップインされたとき)に準備されます:touch-action: none、aria-pressed="false"、そして⠿のような記号だけのハンドルには 既定のaria-label(i18n キーsortable.handle)。 - ポインタドラッグは 4px のしきい値を超えてから始まるので、普通の クリックは素通りします。アイテムはポインタ位置に追従してライブに 並び替わり、行/列レイアウトはジオメトリから自動判定されます。
- 確定した並べ替えは共有の
role="status"ライブリージョンで アナウンスされ、バブリングするhc:sortchangeイベントを ディスパッチします — 順序が実際に変わったときだけ。
順序の永続化
Section titled “順序の永続化”htmx をイベントに向けるだけです。シリアライズは hidden input が 担います:
<ul class="hc-stack" data-hc-sortable data-hx-post="/items/order" data-hx-trigger="hc:sortchange" data-hx-include="this" data-hx-swap="none"> …</ul>リクエストボディは新しい DOM 順で id を列挙します —
order[]=b&order[]=a&order[]=c — <li> を動かせばその hidden input も
動くからです。応答は 204 No Content(確認用に
HX-Trigger トーストを
添えても)か、順序が計算済みラベルに影響する場合は再レンダリングした
コンテナを返してスワップします。リストを <form> で包んで送信ボタンを
置けば、htmx なしでも同じように動きます。
hidden input の代わりにイベントを読む構成も可能です:
list.addEventListener('hc:sortchange', ({ detail }) => { // detail = { item, from, to, order: ['b', 'a', 'c'] }});order は各アイテムの data-hc-sortable-id(なければ id、それも
なければ null)を列挙します。
ハンドルは本物の <button> — それ自体がキーボードインターフェース
です:
| キー | 状態 | 動作 |
|---|---|---|
| Space / Enter | — | アイテムをつかむ(data-grabbed="true"、aria-pressed="true"、アナウンス) |
| ↑ / ← | つかみ中 | アイテムを上/前へ移動(アナウンス) |
| ↓ / → | つかみ中 | アイテムを下/後ろへ移動(アナウンス) |
| Space / Enter | つかみ中 | ドロップ — 確定して hc:sortchange を発火 |
| Escape | つかみ中/ドラッグ中 | キャンセルして元の位置に復元 |
ハンドルのフォーカスが外れると現在位置で確定します。つかむ・移動・
ドロップ・キャンセルはすべて視覚的に隠された role="status"
リージョンでアナウンスされます — sortable.* キーは
setMessages() で翻訳
できます(ja は locales/ja に同梱)。
ドラッグは何もしなくても「ドラッグの手応え」があります: 移動中の
アイテムはポインタに追従してシャドウで浮き上がり(ハンドルは
grab / grabbing カーソル)、押し退けられた兄弟は新しいスロットへ
スライドし、ドロップで定位置に収まります。モーションは
モーションスケール
(--hc-motion-duration-fast)に乗り、prefers-reduced-motion では
すべてスキップされます。DOM の順序が唯一の真実で、アニメーション層は
それを決して変えません。
状態は属性にあるので、既定のリフトは素のセレクタで上書きできます — トグルするクラスはありません:
/* 例: 組み込みのリフトに加えて移動中のアイテムを減光する */[data-dragging='true'],[data-grabbed='true'] { opacity: 0.6;}| 属性 | 対象 | 意味 |
|---|---|---|
data-dragging="true" | アイテム | ポインタドラッグが進行中 |
data-grabbed="true" | アイテム | キーボードのつかみがアクティブ |
aria-pressed | ハンドル | 支援技術へつかみ状態を伝える |
プログレッシブエンハンスメント
Section titled “プログレッシブエンハンスメント”ビヘイビアなしではリストはサーバ順で描画され、ハンドルは何もしない ボタンになります — 内容は読める・完全なまま。並べ替えは エンハンスメントであり、依存ではありません。JS なしの並べ替え フォールバックが必要なら、サーバが処理するアイテムごとの 「上へ/下へ」送信ボタンを追加してください — このレシピとそのまま 共存します。
アクセシビリティ
Section titled “アクセシビリティ”- 視覚上の順序は常に DOM の順序そのもの — スクリーンリーダーの 読み上げ順が見た目とずれることはありません。
- ハンドルはフォーカス可能な要素であること。機械契約(
checks.json)は ハンドルが<button>でない場合に警告します。 touch-action: noneはハンドルだけに適用 — リストの他の場所からは ページが普通にスクロールします。