メニューバー
stimeo--menubar
アプリのメニューバー。上の項目を横に渡り歩き、それぞれがメニューを開きます。頭文字での移動つき。
アプリケーションのメニューバーです。WAI-ARIA の Menubar パターンを 1 段で実装しています。バー全体で Tab は 1 回止まるだけで、左右キーで項目を移動します。下キー、Enter、Space のどれでもメニューが開き、上キーで開くと末尾の項目から始まります。メニューの中は上下キーで巡回し、Home と End で両端へ、文字を打つとその頭文字の項目へ飛びます。メニューを開いたまま左右キーを押すと隣のメニューへ移ります。Esc で閉じるとフォーカスは元の項目へ戻ります。項目の実行、Tab、外側のクリックでも閉じます。使えない項目と隠れている項目は矢印キーの移動先から外れます。上の項目とメニューの対応はマークアップで指定するので、並び順は自由です。位置はこのデモの CSS で決めています。
キーボード操作
| キー | 動作 |
|---|---|
| → / ← | トップ項目間を移動(ループ)。メニューが開いていれば隣を開く。RTL では左右が入れ替わる。 |
| ↓ / Enter / Space | フォーカス中のトップ項目のメニューを開き先頭項目へ(ArrowUp は末尾)。 |
| ↓ / ↑ | 開いているメニュー内を移動(ループ)。 |
| Home / End | 先頭/末尾の項目へ。メニューが開いていれば先頭/末尾のメニューを開く。 |
| 印字可能文字 | 開いているメニュー内で次の先頭一致項目へ型先読み。同じ文字の連打で候補を巡回する。 |
| Enter / Space(メニュー項目) | 項目を実行してメニューを閉じる。 |
| Esc | メニューを閉じて対応トップ項目へフォーカスを戻す。 |
| Tab | メニューを閉じる。フォーカスは次の要素へ進み、戻さない。 |
<%# Markup for the menubar demo.
Top-level items directly under role="menubar" each open a role="menu". The library
handles roving between top items (left/right keys), opening a menu with
ArrowDown/Enter, up/down movement and typeahead within a menu, moving sideways to
an adjacent menu, returning on Escape, and closing on outside click / Tab. Top
items ↔ menus are linked via aria-controls / id. The look is in demo.css. %>
<div
class="menubar"
data-controller="stimeo--menubar"
role="menubar"
aria-label="<%= t("components.menubar.demo.label") %>"
>
<button
type="button"
class="demo-baritem"
role="menuitem"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="menubar-file"
data-stimeo--menubar-target="top"
data-action="click->stimeo--menubar#toggle keydown->stimeo--menubar#onTopKeydown">
<%= t("components.menubar.demo.menus.file.label") %>
</button>
<ul
id="menubar-file"
class="menubar__menu"
role="menu"
aria-label="<%= t("components.menubar.demo.menus.file.label") %>"
hidden
data-stimeo--menubar-target="menu"
>
<% %w[new open save].each do |action| %>
<li role="none">
<button
type="button"
class="demo-menuitem"
role="menuitem"
tabindex="-1"
data-stimeo--menubar-target="item"
data-action="click->stimeo--menubar#activate keydown->stimeo--menubar#onItemKeydown">
<%= t("components.menubar.demo.menus.file.items.#{action}") %>
</button>
</li>
<% end %>
</ul>
<button
type="button"
class="demo-baritem"
role="menuitem"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="menubar-edit"
data-stimeo--menubar-target="top"
data-action="click->stimeo--menubar#toggle keydown->stimeo--menubar#onTopKeydown">
<%= t("components.menubar.demo.menus.edit.label") %>
</button>
<ul
id="menubar-edit"
class="menubar__menu"
role="menu"
aria-label="<%= t("components.menubar.demo.menus.edit.label") %>"
hidden
data-stimeo--menubar-target="menu"
>
<% %w[cut copy paste].each do |action| %>
<li role="none">
<button
type="button"
class="demo-menuitem"
role="menuitem"
tabindex="-1"
data-stimeo--menubar-target="item"
data-action="click->stimeo--menubar#activate keydown->stimeo--menubar#onItemKeydown">
<%= t("components.menubar.demo.menus.edit.items.#{action}") %>
</button>
</li>
<% end %>
</ul>
</div>
/*
* Presentation-only styles for the menubar demo.
* The library expresses open/close via the menu's hidden, the active top item via
* aria-expanded, and the active in-menu item via focus. Menu placement (directly
* below the top item) is static and the consumer's CSS responsibility; use
* stimeo-ui/positioning for dynamic flip.
*/
.menubar {
display: flex;
gap: 0.25rem;
padding: 0.25rem;
border: 1px solid var(--border-strong);
border-radius: 0.375rem;
background: var(--surface-subtle);
/* Positioning context for the menus' absolute placement. */
position: relative;
width: fit-content;
}
/*
* The open menu is the same floating surface as the navigation-menu demo's panel
* (same width, padding, border, radius and shadow), so the two menu components
* read as one visual language in the catalog. Only the horizontal anchor differs:
* no `left` is set, which leaves each menu at its static position under its own
* top item instead of the bar's left edge.
*/
.menubar__menu {
position: absolute;
top: calc(100% + 0.25rem);
z-index: 10;
min-width: 12rem;
margin: 0;
padding: 0.5rem;
list-style: none;
background: var(--surface-card);
border: 1px solid var(--border-strong);
border-radius: 0.375rem;
box-shadow: 0 8px 24px rgb(15 23 42 / 0.12);
}
.menubar__menu[hidden] {
display: none;
}
このデモに固有の消費側 JS はありません(挙動はコントローラが担います)。
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--menubar"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
top
必須
|
トップレベルの menuitem ボタン。ロービングタブインデックスで一つの Tab ストップを形成する。 | data-stimeo--menubar-target="top" |
menu
必須
|
aria-controls/id でトップ項目に紐付く role=menu のポップアップ。開いている間表示される。 |
data-stimeo--menubar-target="menu" |
item
|
メニュー内の menuitem。矢印キーとタイプアヘッドで項目間のフォーカスを移動する。 | data-stimeo--menubar-target="item" |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
activate
|
項目を実行した後にメニューを閉じ、所有元のトップ項目にフォーカスを戻す。 | stimeo--menubar#activate |
onItemKeydown
|
メニュー項目にフォーカスがある間のキーボード操作を処理する(矢印、Home/End、隣接メニューへの移動、Tab、タイプアヘッド)。 | stimeo--menubar#onItemKeydown |
onTopKeydown
|
トップ項目にフォーカスがある間のキーボード操作を処理する(矢印で移動・開く、Home/End、Tab で閉じる)。 | stimeo--menubar#onTopKeydown |
toggle
|
クリックされたトップ項目のメニューを開閉する。 | stimeo--menubar#toggle |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-expanded |
トップ項目 | そのメニューの開閉状態。 |
tabindex |
トップ項目 | アクティブは 0、他は -1(ロービング)。 |
hidden |
メニュー | 閉じているときは付与。 |