コマンドパレット
stimeo--command-palette
アクセシブルなコマンドランチャー。ダイアログとコンボボックスを融合し、仮想フォーカス・矢印移動・フォーカストラップ・インライン検索に対応。
stimeo--command-palette コントローラは、極めて双方向的なランチャーダッシュボードを実装します。 WAI-ARIA の Dialog および Combobox(入力欄付きリストボックス)のパターンに準拠しています。グローバルショートカット(mod+k または Cmd/Ctrl+K)で表示を切り替え、入力欄にフォーカスをトラップします。検索欄への入力により大文字小文字を区別せず候補を動的フィルタリングし、矢印キーで仮想フォーカスを移動(aria-activedescendant)させてキーボード操作を完結します。
キーボードショートカット Cmd+K または Ctrl+K でも開くことができます。
- 人気のコマンド
- マイプロフィール表示 Go P
- アカウント設定 Go S
- システム管理
- ダークモード切り替え
- ライトモード切り替え
キーボード操作
| キー | 動作 |
|---|---|
| Cmd+K / Ctrl+K | 画面のどこからでもコマンドパレットの表示/非表示を切り替える。 |
| ↓ / ↑ | コマンド候補間で仮想フォーカスを移動させる(aria-activedescendant を同期)。 |
| Enter | 現在仮想フォーカスが当たっているコマンド候補を選択する。 |
| Esc | コマンドパレットを閉じ、開く直前の要素へフォーカスを復帰させる。 |
| Tab / Shift+Tab | 入力欄と閉じるボタンの間でフォーカスを循環させる(フォーカストラップ)。 |
<%# Markup for the command_palette demo.
The single source used for both the live render and the copy-paste code.
stimeo--command-palette provides the global shortcut (Cmd+K) / arrow navigation /
focus trap. The controller registers the global keydown itself in connect(), so no
keydown wiring is needed here. %>
<div
class="command-palette-demo"
data-controller="stimeo--command-palette">
<button
type="button"
class="command-palette-demo__trigger"
data-action="click->stimeo--command-palette#open">
<%= t("components.command_palette.demo.open_button") %>
</button>
<p class="command-palette-demo__help">
<%= t("components.command_palette.demo.help_text_html") %>
</p>
<%# Overlay backdrop. Closes only on a click on the backdrop itself
(closeOnBackdrop ignores inner clicks). %>
<div
class="command-palette"
role="dialog"
aria-modal="true"
aria-label="<%= t("components.command_palette.demo.popular_title") %>"
data-stimeo--command-palette-target="dialog"
data-action="click->stimeo--command-palette#closeOnBackdrop"
hidden>
<%# The palette body container. %>
<div class="command-palette__panel">
<%# Control area (Combobox structure). role/aria-* go on the real input
(the controller syncs them). %>
<div class="command-palette__search-wrapper">
<svg class="command-palette__search-icon" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input
type="text"
placeholder="<%= t("components.command_palette.demo.placeholder") %>"
class="command-palette__input"
data-stimeo--command-palette-target="input"
data-action="input->stimeo--command-palette#filter
keydown->stimeo--command-palette#onKeydown"
role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
aria-controls="playground-command-list"
aria-autocomplete="list" />
<button
type="button"
class="command-palette__close-btn"
data-action="click->stimeo--command-palette#close"
aria-label="<%= t("components.command_palette.demo.close_label") %>">
<kbd class="command-palette-demo__kbd font-normal text-xs">ESC</kbd>
</button>
</div>
<%# Listbox display area. %>
<ul
id="playground-command-list"
role="listbox"
aria-label="<%= t("components.command_palette.demo.popular_title") %>"
class="command-palette__listbox"
data-stimeo--command-palette-target="list">
<%# Section heading (data-disabled="true" makes it non-selectable). %>
<li role="option" data-stimeo--command-palette-target="option"
data-disabled="true" class="command-palette__group-title">
<%= t("components.command_palette.demo.popular_title") %>
</li>
<li
id="cmd-profile"
role="option"
data-stimeo--command-palette-target="option"
data-action="click->stimeo--command-palette#selectByClick"
data-alert="<%= t("components.command_palette.demo.profile_alert") %>"
class="command-palette__option">
<svg class="command-palette__option-icon" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
<span class="command-palette__option-text"><%= t(
"components.command_palette.demo.profile_label"
) %></span>
<span class="command-palette__option-shortcut">Go P</span>
</li>
<li
id="cmd-settings"
role="option"
data-stimeo--command-palette-target="option"
data-action="click->stimeo--command-palette#selectByClick"
data-alert="<%= t("components.command_palette.demo.settings_alert") %>"
class="command-palette__option">
<svg class="command-palette__option-icon" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65
1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0
0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65
1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0
4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0
0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1
1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0
0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0
0-1.51 1z"/>
</svg>
<span class="command-palette__option-text"><%= t(
"components.command_palette.demo.settings_label"
) %></span>
<span class="command-palette__option-shortcut">Go S</span>
</li>
<li role="option" data-stimeo--command-palette-target="option"
data-disabled="true" class="command-palette__group-title">
<%= t("components.command_palette.demo.admin_title") %>
</li>
<li
id="cmd-theme-dark"
role="option"
data-stimeo--command-palette-target="option"
data-action="click->stimeo--command-palette#selectByClick"
data-alert="<%= t("components.command_palette.demo.dark_alert") %>"
class="command-palette__option">
<svg class="command-palette__option-icon" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
<span class="command-palette__option-text"><%= t(
"components.command_palette.demo.dark_label"
) %></span>
</li>
<li
id="cmd-theme-light"
role="option"
data-stimeo--command-palette-target="option"
data-action="click->stimeo--command-palette#selectByClick"
data-alert="<%= t("components.command_palette.demo.light_alert") %>"
class="command-palette__option">
<svg class="command-palette__option-icon" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/>
<line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/>
<line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
</svg>
<span class="command-palette__option-text"><%= t(
"components.command_palette.demo.light_label"
) %></span>
</li>
</ul>
</div>
</div>
</div>
/*
* Presentation-only styles for the command_palette demo.
* Toned-down styling that blends with the wireframe look (plain white background)
* of the other components (Dialog, Combobox, etc.).
*/
.command-palette-demo {
margin: 1rem 0;
}
.command-palette-demo__trigger {
padding: 0.5rem 1rem;
font-size: 1rem;
cursor: pointer;
border: 1px solid var(--border-interactive);
border-radius: 0.375rem;
background: var(--surface-card);
transition: border-color 0.15s ease;
}
.command-palette-demo__trigger:hover {
border-color: var(--accent);
}
.command-palette-demo__help {
margin: 0.5rem 0 0;
font-size: 0.875rem;
color: var(--muted);
}
/* Modal backdrop - identical to dialog__backdrop. */
.command-palette {
position: fixed;
inset: 0;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 4rem 1rem;
background: rgba(0, 0, 0, 0.5);
z-index: 200;
animation: cp-fade-in 0.15s ease-out;
}
.command-palette[hidden] {
display: none !important;
}
/* Palette body - plain white background and shadow, matching dialog__panel. */
.command-palette__panel {
width: min(36rem, 100%);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 0.5rem;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
overflow: hidden;
animation: cp-slide-down 0.15s ease-out;
}
/* Search input area. */
.command-palette__search-wrapper {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
gap: 0.75rem;
}
.command-palette__search-icon {
width: 1.15rem;
height: 1.15rem;
color: var(--muted);
flex-shrink: 0;
}
.command-palette__input {
width: 100%;
border: none;
background: transparent;
color: var(--fg);
font-size: 1rem;
font-family: inherit;
outline: none;
}
.command-palette__input::placeholder {
color: var(--muted);
}
.command-palette__close-btn {
background: transparent;
border: none;
cursor: pointer;
padding: 0.25rem 0.5rem;
display: flex;
align-items: center;
border: 1px solid var(--border);
border-radius: 0.25rem;
font-size: 0.75rem;
color: var(--muted);
background: var(--surface-subtle);
}
.command-palette__close-btn:hover {
border-color: var(--border-interactive);
}
/* List area. */
.command-palette__listbox {
max-height: 20rem;
overflow-y: auto;
margin: 0;
padding: 0.25rem;
list-style: none;
}
/* Section title. */
.command-palette__group-title {
padding: 0.5rem 0.75rem 0.25rem;
font-size: 0.75rem;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.05em;
user-select: none;
}
/* Option items - consistent with the items in dropdown.css and combobox.css. */
.command-palette__option {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
border-radius: 0.25rem;
cursor: pointer;
user-select: none;
font-size: 0.95rem;
color: var(--fg);
}
.command-palette__option-icon {
width: 1rem;
height: 1rem;
color: var(--muted);
flex-shrink: 0;
}
.command-palette__option-text {
flex-grow: 1;
}
.command-palette__option-shortcut {
font-size: 0.75rem;
color: var(--muted);
}
/* Roving virtual focus status: aria-selected="true" - consistent with combobox. */
.command-palette__option[aria-selected="true"] {
background: var(--accent);
color: var(--white);
}
.command-palette__option[aria-selected="true"] .command-palette__option-icon {
color: var(--white);
}
.command-palette__option[aria-selected="true"] .command-palette__option-shortcut {
color: rgba(255, 255, 255, 0.8);
}
/* Animations. */
@keyframes cp-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes cp-slide-down {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
// Consumer-side example: handle the select event and run the chosen command (here, an alert).
// On selection the controller closes the palette and dispatches stimeo--command-palette:select.
document
.querySelector('.command-palette-demo')
.addEventListener('stimeo--command-palette:select', function (e) {
const message = e.detail.option && e.detail.option.dataset.alert;
if (message) window.alert(message);
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--command-palette"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
dialog
必須
|
role="dialog" aria-modal のコンテナ。hidden で表示制御し、自身(背景)クリックで閉じる。 |
data-stimeo--command-palette-target="dialog" |
input
必須
|
絞り込みを駆動する role="combobox" の検索入力。aria-expanded と aria-activedescendant を持つ。 |
data-stimeo--command-palette-target="input" |
list
必須
|
コマンドの選択肢を含む role="listbox" コンテナ。 |
data-stimeo--command-palette-target="list" |
option
|
role="option" のコマンド。絞り込み・仮想フォーカスでの移動・選択の対象(無効なものは除外)。 |
data-stimeo--command-palette-target="option" |
empty
|
選択可能な一致がないときに表示する空状態の要素。 | data-stimeo--command-palette-target="empty" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
hotkey
|
パレットをトグルするグローバルホットキー(既定 mod+k。mod は Cmd または Ctrl)。 |
data-stimeo--command-palette-hotkey-value |
open
|
パレットが開いているかどうか。開いた状態を反映・初期化する(既定 false)。 | data-stimeo--command-palette-open-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
close
|
パレットを閉じ、開いた要素へフォーカスを戻す。 | stimeo--command-palette#close |
closeOnBackdrop
|
クリックがダイアログ背景自身のときのみ閉じ、内側のクリックは無視する。 | stimeo--command-palette#closeOnBackdrop |
filter
|
入力値でメモリ上の選択肢を絞り込み、空状態を切り替え、アクティブな選択肢をリセットする。 | stimeo--command-palette#filter |
onKeydown
|
入力でのコンボボックス操作を処理する。矢印キー、Home/End、Enter で選択。 | stimeo--command-palette#onKeydown |
open
|
パレットを開き、フォーカスを閉じ込め、入力へフォーカスして絞り込みをリセットする。 | stimeo--command-palette#open |
selectByClick
|
クリックされた選択肢を選択(無効なものは無視)し、select を発火して閉じる。 |
stimeo--command-palette#selectByClick |
toggle
|
パレットの開閉をトグルする。 | stimeo--command-palette#toggle |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
select
|
選択肢が選ばれたときに発火。detail に選択肢の value と option 要素を含む。 | stimeo--command-palette:select |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
hidden |
ダイアログ要素 | 閉じている間は付与され、開くと外れる(表示を切り替える)。 |
aria-activedescendant="[選択肢のID]" |
コンボボックス入力欄 | 現在フォーカスされている(アクティブな)選択肢の ID を指し示し、スクリーンリーダーへ伝える。 |
aria-selected="true" |
コマンド選択肢 | アクティブな選択肢に付与され、CSS スタイリングや支援技術と連動する。 |
data-disabled="true" |
コマンド選択肢 | 見出しや区切り線など、選択不可能な項目に付与してフォーカス移動をスキップさせる。 |