ポインタドラッグ
stimeo--pointer-drag
正規化されたドラッグライフサイクル(start / move / end / cancel)とキーボード代替を提供する基盤プリミティブ。
stimeo--pointer-drag は「ドラッグを含むすべての UI」(sortable・スワイプ操作・ボトムシート・分割ペイン)が合成の土台にするプリミティブです。ポインタ/タッチ/マウスを 1 つのクリーンな信号に正規化し、開始しきい値(未満で離せばただのクリック)、軸ロック、pointer capture、軸から導出する touch-action(ドラッグ中にページがパンしない)、pointercancel / Esc のキャンセルという難所を一度だけ引き受けます。ドラッグが「何を意味するか」はあえて決めず、既定では要素を動かしも描画もしません — 消費側がイベントと data-dragging / data-grabbed フックに反応します。「ただ追従させたいだけ」の定番ケースは opt-in の follow が肩代わりし、move を CSS translate に適用、ドロップで確定、キャンセルで復元します(transform とは独立)。キーボード代替は組み込みです。ハンドルで Space / Enter がグラブ、矢印キーが keyboardStep px の合成 move(origin からの累積・軸フィルタ済み)、Space / Enter でドロップ、Esc でキャンセル。つまり本プリミティブに乗るだけで、すべての消費者がキーボード経路を無料で得ます。 delta は物理座標(client 座標系)の累積値です。handle ターゲットが無ければコントローラ要素自身がハンドルになります。
カードをポインタでドラッグするか、フォーカスして Space でグラブ、矢印キーで移動、もう一度 Space でドロップします(Esc で元の位置へ戻る)。カードの追従は follow 設定値が担い、このデモの JS はステータス行の表示だけです。
キーボード操作
| キー | 動作 |
|---|---|
| Space / Enter | ハンドルをグラブ、またはグラブ中ならドロップ(start / end)。 |
| ↑ / ↓ / ← / → | グラブ中、keyboardStep px の合成 move を発火(ロック軸の矢印は消費するが発火しない)。 |
| Esc | グラブをキャンセル。ポインタドラッグ中も中止できる。 |
<%# pointer-drag: the primitive normalizes the drag lifecycle (threshold, axis
lock, pointer capture, touch-action, cancel) and adds a keyboard alternative.
With the opt-in follow value it also moves the card itself — translate on
move, committed on drop, restored on cancel — so the consumer JS below only
mirrors the signal into the status line. %>
<div class="pointer-drag-demo">
<p class="pointer-drag-demo__hint"><%= t("components.pointer_drag.demo.hint") %></p>
<div class="pointer-drag-demo__area">
<button type="button" class="pointer-drag-demo__card" id="pointer-drag-card"
data-controller="stimeo--pointer-drag"
data-stimeo--pointer-drag-follow-value="true">
<svg class="demo-icon" viewBox="0 0 24 24" fill="currentColor" stroke="none"
aria-hidden="true">
<circle cx="9" cy="6" r="1.5"></circle><circle cx="15" cy="6" r="1.5"></circle>
<circle cx="9" cy="12" r="1.5"></circle><circle cx="15" cy="12" r="1.5"></circle>
<circle cx="9" cy="18" r="1.5"></circle><circle cx="15" cy="18" r="1.5"></circle>
</svg>
<%= t("components.pointer_drag.demo.card_label") %>
</button>
</div>
<p class="pointer-drag-demo__status" data-pointer-drag-status aria-live="polite"
data-moved-template="<%= t("components.pointer_drag.demo.moved_template") %>"
data-idle-text="<%= t("components.pointer_drag.demo.idle_text") %>"></p>
</div>
/*
* Presentation-only styles for the pointer-drag demo. The library emits the drag
* signal and flips data-dragging / data-grabbed; demo.js applies the transform.
* This CSS draws the play area and highlights the card while a session is live.
*/
.pointer-drag-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
align-items: flex-start;
}
.pointer-drag-demo__hint {
margin: 0;
color: var(--muted);
}
.pointer-drag-demo__area {
width: 100%;
min-height: 10rem;
padding: 1rem;
border: 1px dashed var(--border);
border-radius: 0.5rem;
/* The card translates freely; keep the play area its clipping context. */
overflow: hidden;
}
.pointer-drag-demo__card {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid var(--border-strong);
border-radius: 0.375rem;
background: var(--bg);
color: var(--fg);
font-size: 1rem;
cursor: grab;
transition: border-color 0.15s ease, color 0.15s ease;
}
.pointer-drag-demo__card:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* Live session: pointer drag (data-dragging) or keyboard grab (data-grabbed). */
.pointer-drag-demo__card[data-dragging],
.pointer-drag-demo__card[data-grabbed] {
border-color: var(--accent);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
cursor: grabbing;
}
.pointer-drag-demo__card .demo-icon {
width: 1rem;
height: 1rem;
color: var(--muted);
}
.pointer-drag-demo__status {
margin: 0;
min-height: 1.25rem;
font-size: 0.9rem;
color: var(--muted);
}
// pointer-drag emits the normalized signal (cumulative dx/dy from the origin —
// pointer drags and keyboard arrows alike) and, with the opt-in follow value,
// already moves the card: translate on move, committed on drop, snapped back on
// cancel. No positioning JS is left to write — this consumer only mirrors the
// live deltas into the status line. Status copy comes from the localized
// data-* templates (values only, never words).
const card = document.getElementById("pointer-drag-card");
const status = document.querySelector("[data-pointer-drag-status]");
const template = status?.dataset.movedTemplate || "{dx}, {dy}";
const idleText = status?.dataset.idleText || "";
const idle = () => {
if (status) status.textContent = idleText;
};
idle();
card?.addEventListener("stimeo--pointer-drag:move", (event) => {
const { dx, dy } = event.detail;
if (status) {
status.textContent = template.replace("{dx}", String(dx)).replace("{dy}", String(dy));
}
});
card?.addEventListener("stimeo--pointer-drag:end", idle);
card?.addEventListener("stimeo--pointer-drag:cancel", idle);
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--pointer-drag"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
handle
|
ドラッグハンドル(複数可・任意)。無ければコントローラ要素自身がハンドル。実 <button> を推奨。 |
data-stimeo--pointer-drag-target="handle" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
axis
|
許可する軸。x / y / both(既定)。ロック軸の delta は常に 0。 |
data-stimeo--pointer-drag-axis-value |
threshold
|
ドラッグ開始とみなす移動量(px・許可軸上)。未満で離せばクリックのまま。既定 3。 | data-stimeo--pointer-drag-threshold-value |
keyboardStep
|
グラブ中の矢印 1 打あたりの合成 delta(px)。既定 10。 | data-stimeo--pointer-drag-keyboard-step-value |
disabled
|
全操作を無視。セッション中に有効化するとキャンセルを発火。既定 false。 |
data-stimeo--pointer-drag-disabled-value |
follow
|
有効にすると要素がドラッグに自動追従する。move を CSS translate に適用し、ドロップで確定、キャンセルで元の位置へ復元(transform とは独立で、作者の transform を壊さない)。既定 false。 |
data-stimeo--pointer-drag-follow-value |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
start
|
しきい値通過(ポインタ)またはグラブ(キーボード)で { x, y, pointerType } と共に発火。 |
stimeo--pointer-drag:start |
move
|
{ dx, dy, x, y, pointerType } と共に発火。delta は origin からの累積(軸フィルタ済み)。 |
stimeo--pointer-drag:move |
end
|
ドロップ(pointerup / グラブ中の Space・Enter)で { dx, dy, pointerType } と共に発火。 |
stimeo--pointer-drag:end |
cancel
|
Esc・pointercancel(OS 介入)・セッション中の disabled 化で { pointerType } と共に発火。 |
stimeo--pointer-drag:cancel |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
data-dragging |
コントローラ要素 | ポインタドラッグ中(しきい値通過後)に付与。 |
data-grabbed |
コントローラ要素 / ハンドル | キーボードグラブ中に付与。 |
touch-action(インラインスタイル) |
ハンドル | axis から導出(x→pan-y / y→pan-x / both→none)。ドラッグ中のページパンを抑止。作者指定があればそちらを尊重。 |