ContextMenu 右键菜单

右键或者长按某个区域时在指针处展开的菜单。

在这里点击右键

'use client'

import { useState } from 'react'
import { Copy, FolderInput, Pencil, Trash2 } from 'lucide-react'
import {
  Center,
  ContextMenu,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuSub,
  Stack,
  Text,
} from '@hina-ui/react'

export default function Demo() {
  const [last, setLast] = useState('')

  return (
    <Stack gap="sm" align="stretch" className="w-96">
      <ContextMenu
        label="文件操作"
        content={
          <>
            <ContextMenuItem icon={<Pencil />} onSelect={() => setLast('重命名')}>
              重命名
            </ContextMenuItem>
            <ContextMenuItem icon={<Copy />} onSelect={() => setLast('复制')}>
              复制
            </ContextMenuItem>
            <ContextMenuSub label="移动到" icon={<FolderInput />}>
              <ContextMenuItem onSelect={() => setLast('移动到收藏夹')}>收藏夹</ContextMenuItem>
              <ContextMenuItem onSelect={() => setLast('移动到归档')}>归档</ContextMenuItem>
            </ContextMenuSub>
            <ContextMenuSeparator />
            <ContextMenuItem tone="danger" icon={<Trash2 />} onSelect={() => setLast('删除')}>
              删除
            </ContextMenuItem>
          </>
        }
      >
        <Center className="bg-inset h-32 rounded-lg border border-dashed select-none">
          <Text tone="muted" size="sm">
            在这里点击右键
          </Text>
        </Center>
      </ContextMenu>
      {last && (
        <Text tone="muted" size="sm">
          选择了:{last}
        </Text>
      )}
    </Stack>
  )
}
tsx

用法

import { ContextMenu, ContextMenuItem } from '@hina-ui/react'
ts

children 是响应右键的区域,content 属性是菜单里的条目。在区域内点击右键,或者在触屏上长按,菜单在指针的位置展开,选中条目后自动收起。条目、多选项、单选项、分组、标题、分隔线与子菜单都与 DropdownMenu 同一套,只是名字换成 ContextMenu 开头。

在这里点击右键

import { Center, ContextMenu, ContextMenuItem, Text } from '@hina-ui/react'

export default function Demo() {
  return (
    <ContextMenu
      label="图片操作"
      content={
        <>
          <ContextMenuItem>在新标签页打开</ContextMenuItem>
          <ContextMenuItem>复制图片地址</ContextMenuItem>
          <ContextMenuItem>保存图片</ContextMenuItem>
        </>
      }
    >
      <Center className="bg-inset h-32 w-96 rounded-lg border border-dashed select-none">
        <Text tone="muted" size="sm">
          在这里点击右键
        </Text>
      </Center>
    </ContextMenu>
  )
}
tsx

示例

条目

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

在这里点击右键

import { Copy, Pencil, Trash2 } from 'lucide-react'
import {
  Center,
  ContextMenu,
  ContextMenuItem,
  ContextMenuLabel,
  ContextMenuSeparator,
  Kbd,
  Text,
} from '@hina-ui/react'

export default function Demo() {
  return (
    <ContextMenu
      label="条目操作"
      content={
        <>
          <ContextMenuLabel>这条笔记</ContextMenuLabel>
          <ContextMenuItem icon={<Pencil />} trailing={<Kbd>E</Kbd>}>
            编辑
          </ContextMenuItem>
          <ContextMenuItem icon={<Copy />} trailing={<Kbd>⌘C</Kbd>}>
            复制
          </ContextMenuItem>
          <ContextMenuSeparator />
          <ContextMenuItem tone="danger" icon={<Trash2 />}>
            删除
          </ContextMenuItem>
        </>
      }
    >
      <Center className="bg-inset h-32 w-96 rounded-lg border border-dashed select-none">
        <Text tone="muted" size="sm">
          在这里点击右键
        </Text>
      </Center>
    </ContextMenu>
  )
}
tsx

多选项

ContextMenuCheckboxItem 用于可以同时选中多项的开关,checked 可受控,切换后菜单保持展开。

网格开,标尺关

'use client'

import { useState } from 'react'
import { Center, ContextMenu, ContextMenuCheckboxItem, Text } from '@hina-ui/react'

export default function Demo() {
  const [grid, setGrid] = useState(true)
  const [ruler, setRuler] = useState(false)

  return (
    <ContextMenu
      label="视图"
      content={
        <>
          <ContextMenuCheckboxItem checked={grid} onCheckedChange={setGrid}>
            显示网格
          </ContextMenuCheckboxItem>
          <ContextMenuCheckboxItem checked={ruler} onCheckedChange={setRuler}>
            显示标尺
          </ContextMenuCheckboxItem>
        </>
      }
    >
      <Center className="bg-inset h-32 w-96 rounded-lg border border-dashed select-none">
        <Text tone="muted" size="sm">
          {`网格${grid ? '开' : '关'},标尺${ruler ? '开' : '关'}`}
        </Text>
      </Center>
    </ContextMenu>
  )
}
tsx

单选项

一组互斥的选项用 ContextMenuRadioGroup 包裹,当前项自动带选中标记。

按名称排序

'use client'

import { useState } from 'react'
import {
  Center,
  ContextMenu,
  ContextMenuLabel,
  ContextMenuRadioGroup,
  ContextMenuRadioItem,
  Text,
} from '@hina-ui/react'

const names: Record<string, string> = { name: '名称', date: '修改日期', size: '大小' }

export default function Demo() {
  const [sort, setSort] = useState('name')

  return (
    <ContextMenu
      label="排序"
      content={
        <>
          <ContextMenuLabel>排序方式</ContextMenuLabel>
          <ContextMenuRadioGroup value={sort} onValueChange={setSort}>
            <ContextMenuRadioItem value="name">名称</ContextMenuRadioItem>
            <ContextMenuRadioItem value="date">修改日期</ContextMenuRadioItem>
            <ContextMenuRadioItem value="size">大小</ContextMenuRadioItem>
          </ContextMenuRadioGroup>
        </>
      }
    >
      <Center className="bg-inset h-32 w-96 rounded-lg border border-dashed select-none">
        <Text tone="muted" size="sm">
          {`按${names[sort]}排序`}
        </Text>
      </Center>
    </ContextMenu>
  )
}
tsx

不可用

disabled 让整块区域不再响应右键,浏览器自己的菜单照常出现;条目上的 disabled 只让那一条不可选。

在这里点击右键

'use client'

import { useState } from 'react'
import { Center, ContextMenu, ContextMenuItem, Stack, Switch, Text } from '@hina-ui/react'

export default function Demo() {
  const [locked, setLocked] = useState(false)

  return (
    <Stack gap="md" align="start">
      <Switch checked={locked} onCheckedChange={setLocked}>
        锁定区域
      </Switch>
      <ContextMenu
        label="条目操作"
        disabled={locked}
        content={
          <>
            <ContextMenuItem>编辑</ContextMenuItem>
            <ContextMenuItem disabled>移动</ContextMenuItem>
            <ContextMenuItem tone="danger">删除</ContextMenuItem>
          </>
        }
      >
        <Center className="bg-inset h-32 w-96 rounded-lg border border-dashed select-none">
          <Text tone="muted" size="sm">
            {locked ? '已锁定,右键是浏览器自己的菜单' : '在这里点击右键'}
          </Text>
        </Center>
      </ContextMenu>
    </Stack>
  )
}
tsx

行为

  • 菜单在指针的位置展开,靠近视口边缘时向内翻转。
  • 菜单打开期间页面停止滚动;点击菜单外部、按 Esc 或者选中条目都会收起。
  • 方向键在条目间移动,回车或者空格选中,输入字母跳到匹配的条目。
  • 触屏上长按区域打开菜单。

无障碍

  • 菜单是 role="menu",用 label 给它一个名称;条目分别是 menuitem、menuitemcheckbox 与 menuitemradio。
  • 右键菜单里的操作应当在页面上另有可达的入口,键盘与辅助技术的用户未必能触发它。

API

ContextMenu

属性
类型
默认值
说明
label
string
—
菜单的无障碍名称
disabled
boolean
false
区域是否不再响应右键
open
boolean
—
是否打开,可受控
className
string
—
追加至菜单面板的类名
属性
说明
children
响应右键的区域
content
菜单里的条目

条目与其他子件

ContextMenuItem、ContextMenuCheckboxItem、ContextMenuRadioGroup、ContextMenuRadioItem、ContextMenuGroup、ContextMenuLabel、ContextMenuSeparator 与 ContextMenuSub 的属性与回调与 DropdownMenu 的同名子件完全一致,见 DropdownMenu。