オーバーレイ/アクション
オーバーフローアクションメニュー
項目数が増えた際に主動作を圧縮して表示する、アクションの省略メニューを確認するレシピです。
動きを試す
プレビュー
必要なファイルを選ぶ
コード
index.html
利用前に変更する箇所
- アクション名と権限制御表示を運用仕様へ合わせる
<!doctype html>
<html lang="ja" data-theme="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>オーバーフローアクションメニュー</title>
<style>
:root { --bg:#f7f8fa; --surface:#ffffff; --text:#18212f; --muted:#5d6b7a; --border:#d7dee8; --focus:#0077cc; --danger:#b42318; }
:root[data-theme="dark"] { --bg:#0f141a; --surface:#171d26; --text:#f2f5f8; --muted:#aab4c0; --border:#354152; --focus:#65c7ff; --danger:#ff8a80; }
* { box-sizing: border-box; }
body { margin: 0; padding: 16px; background: var(--bg); color: var(--text); font-family: Arial, sans-serif; }
.card { max-width: 640px; margin: 0 auto; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 12px; }
.primary { display: flex; align-items: center; gap: 8px; }
.primary button { min-height: 44px; border: 1px solid var(--border); background: var(--surface); color: var(--text); border-radius: 8px; padding: 8px 12px; }
.overflow {
position: relative;
display: inline-block;
}
.menu {
position: absolute;
top: 48px;
left: 0;
min-width: 190px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
box-shadow: 0 8px 16px rgba(0,0,0,.12);
}
.menu[hidden] { display: none; }
.menu button { width: 100%; border: none; border-bottom: 1px solid var(--border); text-align: left; background: transparent; border-radius: 0; }
.menu button:last-child { border-bottom: none; }
[data-state-panel][hidden] { display: none; }
.status { min-height: 1.3em; }
button:focus-visible { outline: 3px solid var(--focus); }
.danger { color: var(--danger); }
@media (max-width: 360px) { body { padding: 10px; } }
@media (max-width: 240px) {
body, .card { padding: 8px; }
.card { overflow-wrap: anywhere; }
.primary { align-items: stretch; flex-direction: column; }
.overflow, .primary button { width: 100%; }
.menu { left: 0; min-width: 0; right: 0; width: 100%; }
}
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
</style>
</head>
<body>
<main class="card">
<h1>アクション省略メニュー</h1>
<div class="primary">
<button type="button">主操作</button>
<div class="overflow">
<button type="button" data-overflow-toggle aria-controls="overflowMenu" aria-expanded="false">その他</button>
<div class="menu" id="overflowMenu" hidden>
<button type="button" data-menu-action="edit">編集</button>
<button type="button" data-menu-action="copy">コピー</button>
<button type="button" data-menu-action="remove" class="danger">削除(確認あり)</button>
</div>
</div>
</div>
<div data-state-panel="closed">
<p>閉じた状態。補助アクションは隠しています。</p>
</div>
<div data-state-panel="open" hidden>
<p>開いた状態。補助アクションを表示しています。</p>
</div>
<div data-state-panel="disabled" hidden>
<p>一部アクションは無効化された状態です。</p>
</div>
<p class="status" role="status" data-status></p>
</main>
<script>
(() => {
const params = new URLSearchParams(window.location.search);
const instance = params.get('instance') ?? 'missing-instance';
const menu = document.getElementById('overflowMenu');
const toggle = document.querySelector('[data-overflow-toggle]');
const status = document.querySelector('[data-status]');
const statePanels = Array.from(document.querySelectorAll('[data-state-panel]'));
const actionButtons = Array.from(document.querySelectorAll('[data-menu-action]'));
const destructiveAction = document.querySelector('[data-menu-action="remove"]');
const validStates = new Set(['closed', 'open', 'disabled']);
const setTheme = (theme) => {
if (theme === 'dark' || theme === 'light') document.documentElement.dataset.theme = theme;
};
const updateStateDisplay = (nextState, message) => {
document.documentElement.dataset.state = nextState;
for (const panel of statePanels) panel.hidden = panel.dataset.statePanel !== nextState;
if (status) status.textContent = message;
};
const openMenu = ({ disabled = false } = {}) => {
menu.hidden = false;
toggle.setAttribute('aria-expanded', 'true');
for (const button of actionButtons) button.disabled = false;
destructiveAction.disabled = disabled;
updateStateDisplay(disabled ? 'disabled' : 'open', disabled ? '削除操作は現在利用できません。' : '補助操作を表示しました。');
actionButtons[0]?.focus();
};
const closeMenu = ({ restoreFocus = true, message = 'メニューを閉じました。' } = {}) => {
menu.hidden = true;
toggle.setAttribute('aria-expanded', 'false');
for (const button of actionButtons) button.disabled = false;
updateStateDisplay('closed', message);
if (restoreFocus) toggle.focus();
};
const setState = (nextState, { restoreFocus = false } = {}) => {
if (!validStates.has(nextState)) return;
if (nextState === 'open') openMenu();
else if (nextState === 'disabled') openMenu({ disabled: true });
else closeMenu({ restoreFocus });
};
toggle.addEventListener('click', () => {
if (menu.hidden) openMenu();
else closeMenu();
});
for (const action of actionButtons) {
action.addEventListener('click', () => {
if (!action.disabled) {
closeMenu({ message: `${action.textContent?.trim()}を選択しました。` });
}
});
}
document.addEventListener('pointerdown', (event) => {
if (!menu.hidden && !event.target.closest('.overflow')) closeMenu();
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !menu.hidden) {
event.preventDefault();
closeMenu();
}
});
window.addEventListener('message', (event) => {
if (event.source !== window.parent || typeof event.data !== 'object' || event.data === null) return;
if (event.data.source !== 'wpl-host' || event.data.instance !== instance) return;
if (event.data.type === 'wpl-preview-state') setState(event.data.value);
if (event.data.type === 'wpl-preview-theme') setTheme(event.data.value);
});
setTheme(params.get('theme') === 'dark' ? 'dark' : 'light');
setState(validStates.has(params.get('state')) ? (params.get('state') ?? 'closed') : 'closed', { restoreFocus: false });
window.parent.postMessage({ source: 'wpl-preview', type: 'wpl-preview-ready', instance }, '*');
})();
</script>
</body>
</html>
プレビューで試せる動き
- 閉じる: オーバーフローは閉じたまま主要操作のみ表示
- 開く: 追加アクションをリスト化して表示
- 無効: 一部アクションを利用不可で示す
使い分けを確認する
このレシピが合う場面
適する場面
- 複数アクションを整理して重要操作を優先したい
- テーブルやカードで補助アクションを隠して保持したい
- 主アクションと補助アクションの優先順位を示す
適さない場面
- アクション数が常に1〜2個しかない場合
- 複雑な権限条件で遷移先が大きく変わる場合
