Dropdown with Icons
A menu with a glyph on every row - icons as scanning aids, labels as the names.
6 個のフレームワーク中級
A menu button with the full keyboard model - arrows, Home/End, Escape, focus restore.
<!--
The menu button pattern, minimum viable version.
aria-haspopup="menu" the promise
aria-expanded the current state, kept in sync on every toggle
aria-controls which element the promise refers to
role="menu"/"menuitem" the structure the promise describes
aria-expanded is the one people forget to UPDATE. A hardcoded "false" that
never changes is worse than no attribute at all: it actively tells a screen
reader the menu is shut while it is open on screen.
The chevron is aria-hidden - aria-expanded already says which way it points,
and a glyph announcing "image" adds nothing but noise.
-->
<div class="dd">
<button
class="dd__trigger"
type="button"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="dd-basic-menu"
>
Options
<svg class="dd__chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6" />
</svg>
</button>
<ul class="dd__menu" id="dd-basic-menu" role="menu" aria-label="Options" hidden>
<li role="none"><button class="dd__item" type="button" role="menuitem">Edit</button></li>
<li role="none"><button class="dd__item" type="button" role="menuitem">Duplicate</button></li>
<li role="none"><button class="dd__item" type="button" role="menuitem">Share</button></li>
<li role="none"><button class="dd__item" type="button" role="menuitem">Delete</button></li>
</ul>
</div>
<script>
document.querySelectorAll('.dd').forEach(function (root) {
var trigger = root.querySelector('.dd__trigger');
var menu = root.querySelector('.dd__menu');
var items = Array.prototype.slice.call(root.querySelectorAll('.dd__item'));
function setOpen(open) {
menu.hidden = !open;
trigger.setAttribute('aria-expanded', String(open));
}
function close() {
setOpen(false);
trigger.focus();
}
trigger.addEventListener('click', function () {
var next = trigger.getAttribute('aria-expanded') !== 'true';
setOpen(next);
if (next && items[0]) items[0].focus();
});
trigger.addEventListener('keydown', function (event) {
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
event.preventDefault();
setOpen(true);
(event.key === 'ArrowDown' ? items[0] : items[items.length - 1]).focus();
});
menu.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
close();
return;
}
if (event.key === 'Home') {
event.preventDefault();
items[0].focus();
return;
}
if (event.key === 'End') {
event.preventDefault();
items[items.length - 1].focus();
return;
}
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
var index = items.indexOf(document.activeElement);
if (index === -1) return;
event.preventDefault();
var next = event.key === 'ArrowDown' ? index + 1 : index - 1;
items[(next + items.length) % items.length].focus();
});
items.forEach(function (item) {
item.addEventListener('click', close);
});
document.addEventListener('mousedown', function (event) {
if (!root.contains(event.target)) setOpen(false);
});
});
</script>| Prop | 型 | デフォルト | 説明 |
|---|---|---|---|
label必須 | string | - | 読み込み中に読み上げられるアクセシブルなラベル。 |
items必須 | string[] | - | レンダリングする項目の配列。 |
onSelect | (item: string) => void | - | ユーザーが選択したメニュー項目を引数に呼ばれます。 |
className | string | - | ルート要素にマージされる追加クラス。 |
The WAI-ARIA menu button pattern is a bargain: `aria-haspopup="menu"` PROMISES a screen reader user that arrow keys work, and `role="menu"`/`role="menuitem"` repeat it on every row. Ship the roles without the key handling and you have told someone to press Down and then done nothing - measurably worse than a plain button and a list, which at least announces what it is. `aria-expanded` is the attribute people forget to UPDATE: a hardcoded `"false"` actively lies about a menu that is open on screen, so bind it to state. Down on the closed trigger opens onto the first item and Up onto the last; the ring wraps. These are menus of ACTIONS - for picking a value from a list you want a listbox (`forms-select`), because menuitem and option are not interchangeable.