Menubar 菜单栏

横向排列的一组菜单,像桌面应用的菜单栏。

'use client'

import { useState } from 'react'
import {
  Kbd,
  Menubar,
  MenubarCheckboxItem,
  MenubarItem,
  MenubarMenu,
  MenubarRadioGroup,
  MenubarRadioItem,
  MenubarSeparator,
  MenubarSub,
  Stack,
  Text,
} from '@hina-ui/react'

export default function Demo() {
  const [last, setLast] = useState('')
  const [wrap, setWrap] = useState(true)
  const [numbers, setNumbers] = useState(false)
  const [theme, setTheme] = useState('system')

  return (
    <Stack gap="sm" align="start">
      <Menubar label="编辑器菜单">
        <MenubarMenu label="文件">
          <MenubarItem trailing={<Kbd>Ctrl N</Kbd>} onSelect={() => setLast('新建')}>
            新建
          </MenubarItem>
          <MenubarItem trailing={<Kbd>Ctrl O</Kbd>} onSelect={() => setLast('打开')}>
            打开
          </MenubarItem>
          <MenubarSub label="最近打开">
            <MenubarItem onSelect={() => setLast('第一章.md')}>第一章.md</MenubarItem>
            <MenubarItem onSelect={() => setLast('大纲.md')}>大纲.md</MenubarItem>
          </MenubarSub>
          <MenubarSeparator />
          <MenubarItem trailing={<Kbd>Ctrl S</Kbd>} onSelect={() => setLast('保存')}>
            保存
          </MenubarItem>
        </MenubarMenu>
        <MenubarMenu label="编辑">
          <MenubarItem trailing={<Kbd>Ctrl Z</Kbd>} onSelect={() => setLast('撤销')}>
            撤销
          </MenubarItem>
          <MenubarItem trailing={<Kbd>Ctrl Y</Kbd>} onSelect={() => setLast('重做')}>
            重做
          </MenubarItem>
          <MenubarSeparator />
          <MenubarItem trailing={<Kbd>Ctrl F</Kbd>} onSelect={() => setLast('查找')}>
            查找
          </MenubarItem>
        </MenubarMenu>
        <MenubarMenu label="视图">
          <MenubarCheckboxItem checked={wrap} onCheckedChange={setWrap}>
            自动换行
          </MenubarCheckboxItem>
          <MenubarCheckboxItem checked={numbers} onCheckedChange={setNumbers}>
            显示行号
          </MenubarCheckboxItem>
          <MenubarSeparator />
          <MenubarRadioGroup value={theme} onValueChange={setTheme}>
            <MenubarRadioItem value="light">浅色</MenubarRadioItem>
            <MenubarRadioItem value="dark">深色</MenubarRadioItem>
            <MenubarRadioItem value="system">跟随系统</MenubarRadioItem>
          </MenubarRadioGroup>
        </MenubarMenu>
      </Menubar>
      {last && (
        <Text tone="muted" size="sm">
          选择了:{last}
        </Text>
      )}
    </Stack>
  )
}
tsx

用法

import { Menubar, MenubarMenu, MenubarItem } from '@hina-ui/react'
ts

Menubar 是一条横向的栏,里面放若干个 MenubarMenu,每个菜单的 label 是栏上的触发器文字,children 是菜单里的条目。点击触发器展开菜单,展开期间把指针移到另一个触发器上就切换过去;条目、多选项、单选项、分组、标题、分隔线与子菜单都与 DropdownMenu 同一套,只是名字换成 Menubar 开头。

import { Menubar, MenubarItem, MenubarMenu } from '@hina-ui/react'

export default function Demo() {
  return (
    <Menubar label="示例菜单">
      <MenubarMenu label="文件">
        <MenubarItem>新建</MenubarItem>
        <MenubarItem>打开</MenubarItem>
        <MenubarItem>保存</MenubarItem>
      </MenubarMenu>
      <MenubarMenu label="编辑">
        <MenubarItem>撤销</MenubarItem>
        <MenubarItem>重做</MenubarItem>
      </MenubarMenu>
      <MenubarMenu label="帮助">
        <MenubarItem>文档</MenubarItem>
        <MenubarItem>关于</MenubarItem>
      </MenubarMenu>
    </Menubar>
  )
}
tsx

示例

条目

条目的 icon 属性用于前置图标,trailing 属性放快捷键这类尾部内容,删除这类不可撤销的操作设置 tone="danger";MenubarLabel 是不可选中的标题,MenubarSeparator 画一条分隔线。

import { Copy, FilePlus, Trash2 } from 'lucide-react'
import {
  Kbd,
  Menubar,
  MenubarItem,
  MenubarLabel,
  MenubarMenu,
  MenubarSeparator,
} from '@hina-ui/react'

export default function Demo() {
  return (
    <Menubar label="条目示例">
      <MenubarMenu label="文件">
        <MenubarLabel>当前文档</MenubarLabel>
        <MenubarItem icon={<FilePlus />} trailing={<Kbd>Ctrl N</Kbd>}>
          新建
        </MenubarItem>
        <MenubarItem icon={<Copy />}>复制一份</MenubarItem>
        <MenubarSeparator />
        <MenubarItem tone="danger" icon={<Trash2 />}>
          删除
        </MenubarItem>
      </MenubarMenu>
    </Menubar>
  )
}
tsx

多选项与单选项

MenubarCheckboxItem 是可以同时选中多项的开关,切换后菜单保持展开;一组互斥的选项用 MenubarRadioGroup 包裹。

自动换行开,行号关,字号中

'use client'

import { useState } from 'react'
import {
  Menubar,
  MenubarCheckboxItem,
  MenubarLabel,
  MenubarMenu,
  MenubarRadioGroup,
  MenubarRadioItem,
  MenubarSeparator,
  Stack,
  Text,
} from '@hina-ui/react'

const names: Record<string, string> = { sm: '小', md: '中', lg: '大' }

export default function Demo() {
  const [wrap, setWrap] = useState(true)
  const [numbers, setNumbers] = useState(false)
  const [size, setSize] = useState('md')

  return (
    <Stack gap="sm" align="start">
      <Menubar label="视图设置">
        <MenubarMenu label="视图">
          <MenubarCheckboxItem checked={wrap} onCheckedChange={setWrap}>
            自动换行
          </MenubarCheckboxItem>
          <MenubarCheckboxItem checked={numbers} onCheckedChange={setNumbers}>
            显示行号
          </MenubarCheckboxItem>
          <MenubarSeparator />
          <MenubarLabel>字号</MenubarLabel>
          <MenubarRadioGroup value={size} onValueChange={setSize}>
            <MenubarRadioItem value="sm">小</MenubarRadioItem>
            <MenubarRadioItem value="md">中</MenubarRadioItem>
            <MenubarRadioItem value="lg">大</MenubarRadioItem>
          </MenubarRadioGroup>
        </MenubarMenu>
      </Menubar>
      <Text tone="muted" size="sm">
        {`自动换行${wrap ? '开' : '关'},行号${numbers ? '开' : '关'},字号${names[size]}`}
      </Text>
    </Stack>
  )
}
tsx

受控

value / onValueChange 绑定当前展开的菜单的 value,没有展开时是空字符串;可以从外部展开某个菜单。

当前展开:无

'use client'

import { useState } from 'react'
import { Button, Inline, Menubar, MenubarItem, MenubarMenu, Stack, Text } from '@hina-ui/react'

export default function Demo() {
  const [open, setOpen] = useState('')

  return (
    <Stack gap="md" align="start">
      <Inline gap="sm">
        <Button variant="outline" tone="neutral" onClick={() => setOpen('file')}>
          展开「文件」
        </Button>
        <Button variant="outline" tone="neutral" onClick={() => setOpen('help')}>
          展开「帮助」
        </Button>
      </Inline>
      <Menubar value={open} onValueChange={setOpen} label="受控菜单">
        <MenubarMenu label="文件" value="file">
          <MenubarItem>新建</MenubarItem>
          <MenubarItem>打开</MenubarItem>
        </MenubarMenu>
        <MenubarMenu label="帮助" value="help">
          <MenubarItem>文档</MenubarItem>
          <MenubarItem>关于</MenubarItem>
        </MenubarMenu>
      </Menubar>
      <Text tone="muted" size="sm">
        当前展开:{open || '无'}
      </Text>
    </Stack>
  )
}
tsx

不可用的菜单

MenubarMenu 的 disabled 让触发器不可用,条目上的 disabled 只让那一条不可选。

import { Menubar, MenubarItem, MenubarMenu } from '@hina-ui/react'

export default function Demo() {
  return (
    <Menubar label="不可用示例">
      <MenubarMenu label="文件">
        <MenubarItem>新建</MenubarItem>
        <MenubarItem disabled>另存为</MenubarItem>
      </MenubarMenu>
      <MenubarMenu label="调试" disabled>
        <MenubarItem>开始调试</MenubarItem>
      </MenubarMenu>
    </Menubar>
  )
}
tsx

行为

  • 点击触发器展开菜单,再次点击收起;展开期间指针移到另一个触发器即切换到那个菜单。
  • 左右方向键在触发器之间移动,展开期间左右键切换菜单,上下键在条目间移动,回车或者空格选中,Esc 收起并把焦点还给触发器。
  • 菜单打开期间页面照常可以滚动与交互,指针才能在触发器之间移动;选中条目或者点击外部都会收起。

无障碍

  • 根是 role="menubar",用 label 给它一个名称;触发器是带 aria-haspopup 的 menuitem,展开的面板是 role="menu"。
  • 触发器构成一组漫游焦点,Tab 只进出一次,方向键在其中移动。

API

Menubar

属性
类型
默认值
说明
label
string
—
菜单栏的无障碍名称
loop
boolean
true
方向键到达两端时是否绕回
value
string
—
当前展开的菜单的 value,可受控
className
string
—
追加至根元素的类名

条目与其他子件

MenubarItem、MenubarCheckboxItem、MenubarRadioGroup、MenubarRadioItem、MenubarGroup、MenubarLabel、MenubarSeparator 与 MenubarSub 的属性与回调与 DropdownMenu 的同名子件完全一致,见 DropdownMenu。