オーバーレイ · 準拠済み
Menu
MenuMenuItemMenuProps
import { useRef, useState } from 'react'
import { Button, Icon, Menu, Surface, Text } from '@language-lit/material3-expressive'
export function MenuExample() {
const anchorRef = useRef<HTMLButtonElement>(null)
const [open, setOpen] = useState(false)
const [wordWrap, setWordWrap] = useState(true)
const [autoSave, setAutoSave] = useState(true)
const [lastSelected, setLastSelected] = useState<string | null>(null)
return (
<Surface
as="section"
aria-labelledby="menu-example-title"
color="surface-container-low"
shape="extra-large"
className="menu-example"
>
<Text as="h2" id="menu-example-title" variant="titleLarge" emphasis="emphasized">
Menu
</Text>
<Text as="p" variant="bodyMedium">
A portaled, keyboard-navigable action menu anchored to a trigger the
consumer owns, with a checkable item that keeps the menu open.
</Text>
<div className="menu-example__row">
<Button
ref={anchorRef}
variant="outlined"
aria-haspopup="menu"
aria-expanded={open}
onClick={() => setOpen(true)}
>
Open menu
</Button>
{lastSelected != null && (
<Text as="p" variant="bodySmall">
Last selected: {lastSelected}
</Text>
)}
</div>
<Menu
anchorRef={anchorRef}
open={open}
onOpenChange={setOpen}
items={[
{
value: 'copy',
label: 'Copy',
leadingIcon: <Icon source="content_copy" />,
onSelect: () => setLastSelected('Copy'),
},
{
value: 'paste',
label: 'Paste',
leadingIcon: <Icon source="content_paste" />,
onSelect: () => setLastSelected('Paste'),
},
{
value: 'share',
label: 'Share',
leadingIcon: <Icon source="share" />,
trailingIcon: <Icon source="chevron_right" />,
onSelect: () => setLastSelected('Share'),
},
{
value: 'delete',
label: 'Delete',
leadingIcon: <Icon source="delete" />,
disabled: true,
onSelect: () => setLastSelected('Delete'),
},
{
value: 'word-wrap',
label: 'Word wrap',
checked: wordWrap,
onCheckedChange: setWordWrap,
},
{
value: 'auto-save',
label: 'Auto save',
checked: autoSave,
disabled: true,
onCheckedChange: setAutoSave,
},
]}
/>
</Surface>
)
}Menuは、利用側で用意したトリガーに固定された、データ駆動型のアクションメニューをポータル表示します。ネイティブのトップレイヤープリミティブを利用できない最初のコンポーネントです。位置決め、外側クリック/Escapeによる閉じる動作、フォーカス復帰をライブラリが担います。
import { useRef, useState } from 'react'
import { Button, Icon, Menu } from '@language-lit/material3-expressive'
import '@language-lit/material3-expressive/styles.css'
const anchorRef = useRef<HTMLButtonElement>(null)
const [open, setOpen] = useState(false)
<Button ref={anchorRef} aria-haspopup="menu" aria-expanded={open} onClick={() => setOpen(true)}>
Actions
</Button>
<Menu
anchorRef={anchorRef}
open={open}
onOpenChange={setOpen}
items={[
{ value: 'copy', label: 'Copy', leadingIcon: <Icon source="content_copy" />, onSelect: handleCopy },
{ value: 'paste', label: 'Paste', onSelect: handlePaste },
]}
/>仕様
open/defaultOpen/onOpenChangeは、ほかの状態を持つコンポーネントと同じ制御/非制御の形式です。anchorRefは利用側で用意し、完全に管理するトリガーを参照します。Menuはトリガーをレンダーしません。トリガー側でaria-haspopup="menu"/aria-expandedとクリック時の処理を設定します。これは、開閉状態を利用側が管理するDialogと同様、トリガーに依存しない仕様です。items: readonly MenuItem[]でメニューの項目を定義します。形式は{ value, label, onSelect?, leadingIcon?, trailingIcon?, disabled?, checked?, onCheckedChange? }です。checkedを省略すると通常のrole="menuitem"になり、選択時にメニューを閉じます。checkedを定義すると(falseでも)、aria-checked付きのrole="menuitemcheckbox"となり、選択しても開いたままです。1回の表示中に複数の設定を切り替えられます。
キーボード操作
| キー | 動作 |
|---|---|
| 開く | 有効な最初の項目に実際のフォーカスを移動 |
| ArrowDown / ArrowUp | 有効な項目間でフォーカスを移動し、末尾と先頭を循環 |
| Home / End | 有効な最初/最後の項目に移動 |
| 文字入力 | 入力した文字で始まるラベルを持つ次の項目に移動 |
| Enter / Space | フォーカス中の項目を選択 |
| Escape | 閉じてアンカーにフォーカスを戻す |
| Tab | フォーカスを閉じ込めずに閉じ、ブラウザー標準の順序でTab移動を継続 |
無効な項目はキーボード移動で飛ばされ、クリックもできません。フォーカストラップや背景のinert化は行いません。これはDialogのモーダルパターンではなく、APGのメニューボタンパターンに合わせています。
位置決め
ポップアップはまずアンカーと開始辺をそろえ、次に終了辺を試し、それでも収まらなければビューポート内に留めます。縦方向も同様で、アンカーの下、上の順に試してから位置を調整します。アンカーの幅にかかわらず幅は112〜280pxに収まります。表示中はスクロールとウィンドウのサイズ変更に応じて位置を更新します。
トークンと境界
色、形状、モーションの値は1つの--m3e-comp-menu-*登録にまとめられ、Selectのポップアップリストボックスでもそのまま再利用されます。テーマの上書きはMaterial3Providerのスコープ内で適用されます。Menuは実行時スタイルを注入せず、Next.js、Vite、ルーター、アニメーションライブラリ、非公開のアプリケーションコードをインポートしません。