Basic Popover
A click-triggered panel anchored to its button, with Escape and outside-click dismiss.
6 個のフレームワーク中級
A popover holding menu items, with the full arrow-key model and Escape.
<!--
The moment a popover contains a list of ACTIONS it stops being a dialog and
becomes a menu, and the contract changes with it:
aria-haspopup="menu" promises the arrow-key model...
role="menu"/"menuitem" ...and these are the promise being kept
Arrow/Home/End the model itself
Escape out, focus back to the trigger
role="menu" without the key handling is the worse of the two failures: it
tells a screen reader user to press Down and then does nothing when they do.
The <li role="none"> is not noise - role="menu" only permits menuitem
children, and a stray listitem in between breaks the structure.
-->
<div class="pmenu">
<button
class="pmenu__trigger"
type="button"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="pmenu-list"
>
Actions
</button>
<ul class="pmenu__list" id="pmenu-list" role="menu" aria-label="Actions" hidden>
<li role="none"><button class="pmenu__item" type="button" role="menuitem">Rename</button></li>
<li role="none"><button class="pmenu__item" type="button" role="menuitem">Duplicate</button></li>
<li role="none"><button class="pmenu__item" type="button" role="menuitem">Move to…</button></li>
<li role="none"><button class="pmenu__item" type="button" role="menuitem">Archive</button></li>
</ul>
</div>
<script>
document.querySelectorAll('.pmenu').forEach(function (root) {
var trigger = root.querySelector('.pmenu__trigger');
var list = root.querySelector('.pmenu__list');
var items = Array.prototype.slice.call(root.querySelectorAll('.pmenu__item'));
function setOpen(open) {
list.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();
});
// Down from the closed trigger opens onto the first item, Up onto the last
// - the platform convention, and the reason Up exists at all.
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();
});
list.addEventListener('keydown', function (event) {
var index = items.indexOf(document.activeElement);
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;
if (index === -1) return;
event.preventDefault();
var next = event.key === 'ArrowDown' ? index + 1 : index - 1;
// Wraps, because a menu is a ring: Down on the last item is the fastest
// way back to the first.
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 | - | ユーザーが選択したメニュー項目を引数に呼ばれます。 |
The moment a popover holds a list of ACTIONS it stops being a dialog and becomes a menu, and the contract changes with it: `aria-haspopup="menu"` promises the arrow-key model, `role="menu"`/`role="menuitem"` describe it, and the key handling is the promise being kept. Shipping the roles without the keys is the worse failure - it tells a screen reader user to press Down and then does nothing when they do. Down on the closed trigger opens onto the first item, Up onto the last; the ring wraps both ways because a menu is a ring, not a list with two dead ends. `<li role="none">` is not noise: `role="menu"` only permits menuitem children and a stray listitem breaks the structure. Style `:focus-visible` alongside `:hover` - arrow keys move real focus, and that highlight is the cursor.