ポインタドラッグ
stimeo--pointer-drag
ドラッグの土台。始まり、移動、終わり、取り消しを揃え、キーボードでも同じことができます。
並べ替えやスワイプ、引き出しのように、つかんで動かす操作すべての土台です。マウス、指、ペンの違いを 1 つの流れに揃えます。少し動かすまではただのクリックとして扱い、必要なら縦か横かに軸を固定します。途中で指が要素から外れても追い続け、ドラッグ中にページごと動いてしまうこともありません。Esc や割り込みでいつでも取り消せます。ドラッグが何を意味するかは決めないので、既定では要素を動かしも描きもしません。つかんだものを指に追わせるだけなら、設定ひとつで済みます。キーボードでも同じことができます。Space でつかみ、矢印キーで動かし、もう一度 Space で置き、Esc で戻せます。
カードをドラッグするか、フォーカスして Space でつかみ、矢印キーで動かして、もう一度 Space で置きます。Esc で元の位置へ戻ります。追従は設定に任せていて、このデモの JavaScript は状態表示だけです。
キーボード操作
| キー | 動作 |
|---|---|
| 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. The card can travel out of the
dashed area, which the area clips; the reset action brings it back, since
follow's committed offset is internal state a consumer cannot clear. The
button sits outside the controller element, so its click reaches the action
as a custom event the element's own data-action maps. %>
<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"
data-action="demo:reset->stimeo--pointer-drag#reset">
<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__actions">
<button type="button" class="demo-trigger" data-pointer-drag-reset>
<%= t("components.pointer_drag.demo.reset_label") %>
</button>
</p>
<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__actions {
margin: 0;
}
.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 and hands the reset button to the action.
// 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 resetButton = document.querySelector("[data-pointer-drag-reset]");
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);
// The button sits outside the controller element, where a data-action cannot
// reach it. Dispatching on the element lets its own data-action run the reset.
resetButton?.addEventListener("click", () => {
card?.dispatchEvent(new CustomEvent("demo:reset"));
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の 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 |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
reset
|
要素を原点へ戻す。確定済みの follow オフセットと、それを載せているインライン translate を捨てる(進行中のドラッグは先にキャンセル)。follow 有効時はこのオフセットが DOM から触れない内部状態のため、位置のリセットや消費側が書いた位置への合わせ直しには本アクションが要る。 |
stimeo--pointer-drag#reset |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
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 化・要素が残る detach での teardown・セッション中のハンドルが要素外へ出たときに { 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)。ドラッグ中のページパンを抑止。作者指定があればそちらを尊重。 |