Basic Dropdown
A menu button with the full keyboard model - arrows, Home/End, Escape, focus restore.
6 個のフレームワーク中級
An avatar trigger with an identity header, account items and sign out.
<!--
Two details specific to this shape:
1. The HEADER is not a menuitem. It is your name and email - information, not
a command. Making it focusable adds a stop to the arrow-key ring that does
nothing when you press Enter on it. It sits outside role="menu" entirely,
which is also why the trigger's aria-label names the person: the popup's
name is not the button's name.
2. Sign out is separated but is NOT a destructive action dressed in red. It is
reversible - you log back in. Red here would spend the alarm you need for
"delete account", one row below in most real apps.
The avatar is decorative (alt=""): the name is right beside it in text, and a
screen reader announcing "Avery Chen, image" then "Avery Chen" is noise.
-->
<div class="pdd">
<button
class="pdd__trigger"
type="button"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="pdd-menu"
aria-label="Account menu for Avery Chen"
>
<img class="pdd__avatar" src="/avatars/avery.jpg" alt="" width="32" height="32" />
</button>
<div class="pdd__menu" id="pdd-menu" role="menu" aria-label="Account" hidden>
<div class="pdd__head">
<p class="pdd__name">Avery Chen</p>
<p class="pdd__email">avery@northwind.io</p>
</div>
<hr class="pdd__sep" role="separator" />
<button class="pdd__item" type="button" role="menuitem">Profile</button>
<button class="pdd__item" type="button" role="menuitem">Settings</button>
<button class="pdd__item" type="button" role="menuitem">Keyboard shortcuts</button>
<hr class="pdd__sep" role="separator" />
<button class="pdd__item" type="button" role="menuitem">Sign out</button>
</div>
</div>
<script>
document.querySelectorAll('.pdd').forEach(function (root) {
var trigger = root.querySelector('.pdd__trigger');
var menu = root.querySelector('.pdd__menu');
var items = Array.prototype.slice.call(root.querySelectorAll('.pdd__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();
});
root.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 | 型 | デフォルト | 説明 |
|---|---|---|---|
personName必須 | string | - | 見出しとして表示される人物の名前。 |
email必須 | string | - | ログイン中のアカウントのメールアドレス。 |
items必須 | string[] | - | レンダリングする項目の配列。 |
avatarSrc | string | - | アバターの画像 URL。 |
onSelect | (item: string) => void | - | ユーザーが選択したメニュー項目を引数に呼ばれます。 |
onSignOut | () => void | - | サインアウト項目が選択されたときに呼ばれます。 |
The header is not a menuitem. Your name and email are information, not commands - putting them on the arrow-key ring adds a stop that does nothing when you press Enter, so they sit outside the item list entirely. That is also why the trigger carries its own `aria-label` naming the person: an avatar has no text, and the popup's name is not the button's name. The image is `alt=""` because the name is beside it in text and "Avery Chen, image" then "Avery Chen" is noise. Sign out is separated but deliberately NOT red: it is reversible - you log back in - and spending the alarm here leaves nothing for "delete account" one row below. The menu is anchored `right-0`, since an avatar lives in the top-right corner and a menu growing rightward from it goes off the page. The email truncates rather than letting a 14rem box stretch to fit the longest address a user might have.