アンカー配置ユーティリティ
stimeo--anchored
floating 要素を anchor に配置し、画面端では flip/shift で回避する。
stimeo--anchored コントローラは opt-in の stimeo-ui/positioning エンジン(floating-ui ベース)を宣言的に使う層です — floating 要素を anchor に対して配置し、スクロールやリサイズに追従しながら画面端では反対側へ反転(flip)・軸方向へずらして(shift)画面内に収めます(floating-ui の autoUpdate をコントローラ化)。書き込むのは position/left/top の inline style だけ(装飾は一切出さない)で、解決後(flip 後)の配置を floating 要素の data-anchored-placement に反映します(矢印向き等の CSS フック)。 active が追従を駆動し、隠している間は false にして無駄な計測を止められます。他の Value はエンジンにマップされ、追従中は即時に再適用されます。計測ごとに position を発火します。挙動のみ — 開閉・フォーカス管理・DOM 移送は行いません(Dialog/Popover / Focus Scope / Portal と組み合わせる)。opt-in の stimeo-ui/positioning に同梱されるため、コアの import は zero-dependency のまま。登録した利用者だけが floating-ui を読み込みます(本デモの JS がサブパスを import して stimeo--anchored を登録します)。
パネルは anchor に対して配置されます。側を選ぶと、解決された配置がパネルに表示されます(画面端では flip/shift で変わることがあります)。
<%# Anchored positioning demo: the panel is positioned against the anchor button by the
opt-in stimeo--anchored controller (it writes only position/left/top). Choose a side
with the buttons — demo.js sets the placement value — and the panel mirrors the
resolved side via data-anchored-placement (shown through demo.css). The controller is
registered from the opt-in stimeo-ui/positioning subpath in demo.js; demo.css owns the
look. %>
<div class="anchored-demo">
<p class="anchored-demo__hint"><%= t("components.anchored.demo.hint") %></p>
<div class="anchored-demo__controls" role="group"
aria-label="<%= t('components.anchored.demo.placement_label') %>">
<button type="button" class="demo-trigger" data-placement="top" aria-pressed="false">
<%= t("components.anchored.demo.placements.top") %>
</button>
<button type="button" class="demo-trigger" data-placement="right" aria-pressed="false">
<%= t("components.anchored.demo.placements.right") %>
</button>
<button type="button" class="demo-trigger" data-placement="bottom" aria-pressed="true">
<%= t("components.anchored.demo.placements.bottom") %>
</button>
<button type="button" class="demo-trigger" data-placement="left" aria-pressed="false">
<%= t("components.anchored.demo.placements.left") %>
</button>
</div>
<div class="anchored-demo__viewport">
<div class="anchored-demo__scope" data-controller="stimeo--anchored"
data-stimeo--anchored-placement-value="bottom"
data-stimeo--anchored-offset-value="8"
data-stimeo--anchored-padding-value="8">
<button type="button" class="demo-trigger anchored-demo__anchor"
data-stimeo--anchored-target="anchor">
<%= t("components.anchored.demo.anchor") %>
</button>
<div class="anchored-demo__floating" data-stimeo--anchored-target="floating"></div>
</div>
</div>
</div>
/*
* Presentation-only styles for the anchored-positioning demo. The library writes only
* position/left/top on the floating element and mirrors the resolved side onto
* data-anchored-placement; this CSS frames the viewport (a positioned ancestor the
* absolute coordinates resolve against), the anchor, and the floating panel — and shows
* the resolved placement through the data-* hook.
*/
.anchored-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
align-items: flex-start;
width: 100%;
}
.anchored-demo__hint {
margin: 0;
color: var(--muted);
}
.anchored-demo__controls {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
/* Positioned ancestor: the engine's absolute coordinates resolve against this box. */
.anchored-demo__viewport {
position: relative;
display: grid;
place-items: center;
width: 100%;
min-height: 16rem;
border: 1px dashed var(--border);
border-radius: 0.5rem;
}
/* The scope is layout-transparent so the anchor centers in the viewport grid and the
floating element positions against the viewport, not an extra box. */
.anchored-demo__scope {
display: contents;
}
.anchored-demo__floating {
/* The engine sets position/left/top; everything here is look-only and themable.
Start absolute so the panel never disrupts layout before the controller connects. */
position: absolute;
top: 0;
left: 0;
min-width: 7rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--accent);
border-radius: 0.375rem;
background: var(--bg);
color: var(--fg);
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
/* Surface the resolved (post-flip) placement from the controller's data-* hook. */
.anchored-demo__floating::after {
content: "placement: " attr(data-anchored-placement);
color: var(--muted);
}
// anchored opt-in positioning demo (consumer-side JS).
//
// stimeo--anchored ships in the opt-in stimeo-ui/positioning subpath, so it is NOT
// auto-registered by registerStimeo (which keeps the core install zero-dependency).
// Importing the subpath here is what pulls in @floating-ui/dom; we register the
// controller on the playground's Stimulus app, then wire the placement buttons to the
// controller's placement value so picking a side re-positions the panel.
import { AnchoredController } from "stimeo-ui/positioning";
// Register once (Turbo can re-run this inline module on navigation).
if (window.Stimulus && !window.__stimeoAnchoredRegistered) {
window.Stimulus.register("stimeo--anchored", AnchoredController);
window.__stimeoAnchoredRegistered = true;
}
document.querySelectorAll(".anchored-demo").forEach((root) => {
// Idempotent: wire each root once even if this module re-runs.
if (root.dataset.demoWired) return;
root.dataset.demoWired = "1";
const scope = root.querySelector('[data-controller~="stimeo--anchored"]');
if (!scope) return;
const buttons = root.querySelectorAll("[data-placement]");
buttons.forEach((button) => {
button.addEventListener("click", () => {
// Setting the value triggers placementValueChanged → the controller re-positions.
scope.setAttribute("data-stimeo--anchored-placement-value", button.dataset.placement);
buttons.forEach((other) => other.setAttribute("aria-pressed", String(other === button)));
});
});
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--anchored"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
anchor
必須
|
floating 要素を配置する基準となる参照要素。 | data-stimeo--anchored-target="anchor" |
floating
必須
|
配置される要素(書き込まれるのは座標のみ)。 | data-stimeo--anchored-target="floating" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
placement
|
優先配置(floating-ui の Placement。例 top-start)。既定値は bottom。 |
data-stimeo--anchored-placement-value |
offset
|
anchor と floating の間隔(px)。既定値は 0。 | data-stimeo--anchored-offset-value |
flip
|
優先側がはみ出すとき反対側へ反転するか。既定値は true。 | data-stimeo--anchored-flip-value |
shift
|
軸方向にずらして画面内に収めるか。既定値は true。 | data-stimeo--anchored-shift-value |
padding
|
反転/シフト時に画面端と保つ余白(px)。既定値は 0。 | data-stimeo--anchored-padding-value |
strategy
|
CSS positioning strategy。absolute または fixed。既定値は absolute。 |
data-stimeo--anchored-strategy-value |
active
|
追従を有効にするか。隠している間は false で計測停止。既定値は true。 | data-stimeo--anchored-active-value |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
position
|
配置を計算するたびに { placement, x, y } で発火。 | stimeo--anchored:position |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
data-anchored-placement |
floating ターゲット | flip/shift 解決後の配置(例 top-start)。矢印向き等の CSS フック。 |
position / left / top |
floating ターゲット | 更新ごとに書き込む inline style。装飾は一切設定しない。 |