あふれ項目の退避メニュー
stimeo--overflow-menu
収まらないツールバーの項目を「その他」へ移します。幅が戻れば元の場所へ戻します。
ツールバーの項目を、幅に収まるところまで並べます。入りきらない分は「その他」のメニューへ移し、幅が戻れば元の場所へ戻します。どれを先に移すかは、項目ごとに決めた優先度に従います。低いものから先に移るので、大事なボタンは最後まで残ります。移した項目もメニューの中でそのまま操作でき、項目ごとの設定は要りません。もともと隠してあった項目を勝手に出すことはありません。メニューとしての作法はメニューの部品に任せているので、キーボードでの扱いは同じです。移動の判定は幅が変わるたびに行い、細かな揺れは間引きます。
実行中
キーボード操作
| キー | 動作 |
|---|---|
| Enter / Space / ↓ | その他メニューを開き、最初の項目にフォーカスする。 |
| ↑ | その他メニューを開き、最後の項目にフォーカスする。 |
| ↓ / ↑ | 項目間でフォーカスを移動する(末尾で循環)。 |
| Home / End | 最初 / 最後の項目にフォーカスする。 |
| Esc | メニューを閉じ、フォーカスをその他ボタンへ戻す。 |
<%# Overflow-menu demo: drag the width slider to shrink the toolbar — the controller
measures the items and banks the lowest-priority ones into the More menu (delegated
to stimeo--menu), moving them back as space returns. The items need no data-action of
their own: Menu handles clicks and keys from its own element, so an item becomes
operable the moment it lands in the menu. Only the More trigger is bound. The library
moves items and sets data-overflowing / data-overflow-count; demo.css owns the look. %>
<div class="overflow-demo">
<label class="demo-width-control">
<span><%= t("components.overflow_menu.demo.width") %></span>
<input type="range" min="220" max="640" value="640" data-overflow-demo-width>
</label>
<div
class="overflow-demo__bar"
data-controller="stimeo--overflow-menu"
<%# A group, not a toolbar: these buttons stay in the normal Tab order. The
single-tab-stop roving an APG Toolbar implies belongs to stimeo--toolbar,
which this controller does not provide. %>
role="group"
aria-label="<%= t("components.overflow_menu.name") %>">
<div class="overflow-demo__items" data-stimeo--overflow-menu-target="items">
<% %w[save edit share archive delete].each_with_index do |id, i| %>
<button
type="button"
class="demo-menuitem overflow-demo__item"
<%= "data-priority=\"#{i + 1}\"".html_safe if i < 3 %>>
<%= t("components.overflow_menu.demo.#{id}") %>
</button>
<% end %>
</div>
<div
class="overflow-demo__more"
data-controller="stimeo--menu"
data-stimeo--overflow-menu-target="more"
hidden>
<button
id="overflow-menu-trigger"
type="button"
class="demo-trigger overflow-demo__trigger"
aria-haspopup="menu"
aria-expanded="false"
data-stimeo--menu-target="trigger"
data-action="click->stimeo--menu#toggle keydown->stimeo--menu#onTriggerKeydown">
<%= t("components.overflow_menu.demo.more") %>
</button>
<%# A div, not a ul: the controller appends the items themselves, and a ul may only
hold li children. One container shape keeps the markup contract unambiguous. %>
<div class="overflow-demo__menu" role="menu" aria-labelledby="overflow-menu-trigger"
data-stimeo--menu-target="menu" hidden></div>
</div>
</div>
</div>
/*
* Presentation-only styles for the overflow-menu demo. The library moves items between
* the bar and the More menu and sets data-overflowing / data-overflow-count; this CSS
* lays out the (clipped) toolbar, the items, the More button, and the dropdown.
*/
.overflow-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.overflow-demo__bar {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
}
.overflow-demo__items {
display: flex;
gap: 0.5rem;
min-width: 0;
/* Clip the items row — not the whole bar — so overflow is real (items that do not
fit are what the controller banks), while the More dropdown (a sibling) can still
escape and is not cut off by the bar's bounds. */
overflow: hidden;
}
/*
* In the bar the items are chips rather than menu rows. The rule is scoped to the
* bar container because the controller moves the very same buttons into the More
* menu — once banked, the chip look stops matching and the shared .demo-menuitem
* takes over, so a banked row is identical to every other menu row in the catalog.
*/
.overflow-demo__items .overflow-demo__item,
.overflow-demo__trigger {
flex: none;
width: auto;
white-space: nowrap;
padding: 0.375rem 0.75rem;
border-color: var(--border);
border-radius: 0.375rem;
background: var(--surface-subtle);
}
.overflow-demo__items .overflow-demo__item:hover,
.overflow-demo__trigger:hover {
background: var(--surface-card);
}
/* The items row is clipped so that overflow is real, which would slice an outset
ring off the last chip that fits. Inset it, as the shared bar item does. */
.overflow-demo__items .overflow-demo__item:focus-visible {
outline-offset: -2px;
}
.overflow-demo__more {
position: relative;
margin-left: auto;
}
.overflow-demo__menu {
position: absolute;
right: 0;
top: calc(100% + 0.25rem);
z-index: 20;
display: flex;
flex-direction: column;
gap: 0.125rem;
min-width: 10rem;
padding: 0.25rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
background: var(--surface-card);
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.15);
}
// Overflow-menu demo (consumer-side JS).
//
// No layout knobs are needed for the controller itself — it watches the bar with a
// ResizeObserver. This slider just changes the bar's width so the overflow can be seen
// happening; the controller reacts to the resize on its own.
document.querySelectorAll(".overflow-demo").forEach((root) => {
const bar = root.querySelector('[data-controller~="stimeo--overflow-menu"]');
const range = root.querySelector("[data-overflow-demo-width]");
if (!bar || !range) return;
const apply = () => {
bar.style.maxWidth = `${range.value}px`;
};
apply();
range.addEventListener("input", apply);
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--overflow-menu"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
items
必須
|
計測対象の並ぶ項目のコンテナ。 | data-stimeo--overflow-menu-target="items" |
more
必須
|
あふれ項目の退避先メニュー(Menu に委譲)。 | data-stimeo--overflow-menu-target="more" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
moreLabel
|
More トリガーのラベル。テキストも子要素も aria-label / aria-labelledby も持たない素のトリガーにだけ書き込む(既定 More)。 |
data-stimeo--overflow-menu-more-label-value |
debounce
|
幅変化の再計算デバウンス(ミリ秒、既定 100)。 | data-stimeo--overflow-menu-debounce-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
update
|
再計測してバーとメニューを組み直す。項目を動的に増減した後に呼ぶ。 | stimeo--overflow-menu#update |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
change
|
connect 時と以降のあふれ遷移ごとに発火。detail.visible / detail.hidden の件数を伴う。 |
stimeo--overflow-menu:change |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
data-overflowing |
コントローラ要素 | 1 件以上がメニューへ退避中に付与(true)。 |
data-overflow-count |
コントローラ要素 | 現在メニューへ退避している項目数。 |
hidden |
more ターゲット(その他ボタンとメニューを含むラッパ) | あふれが無いとき付与。ラッパごと隠れるためメニューも同時に隠れる。 |