# ContextMenu

Menu mở tại pointer khi nhấn chuột phải, khi dùng các phím context menu của bàn phím, hoặc khi nhấn giữ trên cảm ứng trên một vùng, với cùng các mục, nhóm, mục checkbox và radio, shortcut và kiểu destructive như DropdownMenu. Mọi hành động trong đó cũng phải tới được bằng cách khác.

**Phụ thuộc:** npm: 1 trực tiếp, 5 tổng; registry: 3 trực tiếp, 4 tổng.

## Cách dùng

### Khi nào dùng

- Đưa cùng các hành động như một menu đang hiện khi người dùng nhấn chuột phải, dùng phím menu hoặc nhấn giữ một mục.

### Khi nào không dùng

- Cách duy nhất để tới một hành động. Giữ cùng các hành động ở nơi khác.
- Menu hành động trên cảm ứng mà không có dấu hiệu nhấn giữ. Dùng DropdownMenu.

## Ví dụ

### Hàng file

```tsx
<div className="flex w-64 max-w-full flex-col gap-md">
  <div role="group" aria-label="Thao tác tệp" className="flex gap-sm">
    <Button size="sm" variant="outline">Đổi tên</Button>
    <Button size="sm" variant="outline">Xóa</Button>
  </div>
  <ContextMenu>
    <ContextMenuTrigger className="rounded-container border border-dashed border-border px-lg py-xl" aria-label="Thêm thao tác cho report.pdf">
      <p className="text-sm font-medium">report.pdf</p>
      <p className="text-xs text-text-secondary">Nhấp chuột phải, nhấn giữ hoặc Shift+F10</p>
    </ContextMenuTrigger>
    <ContextMenuContent aria-label="Thao tác tệp">
      <ContextMenuItem leadingIcon={<IconPencil size={16} />} shortcut="F2">Rename</ContextMenuItem>
      <ContextMenuItem disabled>Nhân bản</ContextMenuItem>
      <ContextMenuSeparator />
      <ContextMenuItem destructive leadingIcon={<IconTrash size={16} />}>Delete</ContextMenuItem>
    </ContextMenuContent>
  </ContextMenu>
</div>
```

### Lựa chọn checkbox

```tsx
<div className="flex w-64 max-w-full flex-col gap-md">
  <div role="group" aria-label="Thao tác tệp" className="flex gap-sm">
    <Button size="sm" variant="outline">Đổi tên</Button>
    <Button size="sm" variant="outline">Xóa</Button>
  </div>
  <ContextMenu>
    <ContextMenuTrigger className="rounded-container border border-dashed border-border px-lg py-xl" aria-label="Thêm thao tác cho report.pdf">
      <p className="text-sm font-medium">report.pdf</p>
      <p className="text-xs text-text-secondary">Nhấp chuột phải, nhấn giữ hoặc Shift+F10</p>
    </ContextMenuTrigger>
    <ContextMenuContent aria-label="Thao tác tệp">
      <ContextMenuLabel>Xem</ContextMenuLabel>
      <ContextMenuCheckboxItem checked={menuPinned} onCheckedChange={setMenuPinned}>Ghim lên đầu</ContextMenuCheckboxItem>
      <ContextMenuSeparator />
      <ContextMenuItem leadingIcon={<IconPencil size={16} />} shortcut="F2">Rename</ContextMenuItem>
      <ContextMenuItem disabled>Nhân bản</ContextMenuItem>
      <ContextMenuSeparator />
      <ContextMenuItem destructive leadingIcon={<IconTrash size={16} />}>Delete</ContextMenuItem>
    </ContextMenuContent>
  </ContextMenu>
</div>
```

### Lựa chọn radio

```tsx
<div className="flex w-64 max-w-full flex-col gap-md">
  <div role="group" aria-label="Thao tác tệp" className="flex gap-sm">
    <Button size="sm" variant="outline">Đổi tên</Button>
    <Button size="sm" variant="outline">Xóa</Button>
  </div>
  <ContextMenu>
    <ContextMenuTrigger className="rounded-container border border-dashed border-border px-lg py-xl" aria-label="Thêm thao tác cho report.pdf">
      <p className="text-sm font-medium">report.pdf</p>
      <p className="text-xs text-text-secondary">Nhấp chuột phải, nhấn giữ hoặc Shift+F10</p>
    </ContextMenuTrigger>
    <ContextMenuContent aria-label="Thao tác tệp">
      <ContextMenuLabel>Sắp xếp theo</ContextMenuLabel>
      <ContextMenuRadioGroup value={menuSort} onValueChange={setMenuSort}>
        <ContextMenuRadioItem value="name">Tên</ContextMenuRadioItem>
        <ContextMenuRadioItem value="date">Ngày</ContextMenuRadioItem>
      </ContextMenuRadioGroup>
      <ContextMenuSeparator />
      <ContextMenuItem leadingIcon={<IconPencil size={16} />} shortcut="F2">Rename</ContextMenuItem>
      <ContextMenuItem disabled>Nhân bản</ContextMenuItem>
      <ContextMenuSeparator />
      <ContextMenuItem destructive leadingIcon={<IconTrash size={16} />}>Delete</ContextMenuItem>
    </ContextMenuContent>
  </ContextMenu>
</div>
```

### Vùng vô hiệu

```tsx
<div className="flex w-64 max-w-full flex-col gap-md">
  <div role="group" aria-label="Thao tác tệp" className="flex gap-sm">
    <Button size="sm" variant="outline">Đổi tên</Button>
    <Button size="sm" variant="outline">Xóa</Button>
  </div>
  <ContextMenu>
    <ContextMenuTrigger disabled className="rounded-container border border-dashed border-border px-lg py-xl" aria-label="Thêm thao tác cho report.pdf">
      <p className="text-sm font-medium">report.pdf</p>
      <p className="text-xs text-text-secondary">Nhấp chuột phải, nhấn giữ hoặc Shift+F10</p>
    </ContextMenuTrigger>
    <ContextMenuContent aria-label="Thao tác tệp">
      <ContextMenuItem leadingIcon={<IconPencil size={16} />} shortcut="F2">Rename</ContextMenuItem>
      <ContextMenuItem disabled>Nhân bản</ContextMenuItem>
      <ContextMenuSeparator />
      <ContextMenuItem destructive leadingIcon={<IconTrash size={16} />}>Delete</ContextMenuItem>
    </ContextMenuContent>
  </ContextMenu>
</div>
```

## Nên và không nên

### Nên

- Cho giống một menu hoặc thanh công cụ đang hiện; context menu là một lối tắt.
- Dùng cùng tên mục như menu đang hiện.
- Gắn tên truy cập cho ContextMenuContent khi chỉ label của trigger không giải thích được các hành động. ContextMenuTrigger thêm tabIndex 0 trừ khi disabled.

### Không nên

- Đừng biến nó thành cách duy nhất để tới một hành động.
- Đừng ghi đè menu của trình duyệt trên input chữ hoặc link.

## Props

| Tên | Kiểu | Giá trị | Mặc định | Mô tả |
| --- | --- | --- | --- | --- |
| `onOpenChange` | `(open: boolean) => void` |  |  | Được gọi khi nó mở hoặc đóng. |
| `modal` | `boolean` |  | `true` | Chặn tương tác bên ngoài khi đang mở. |
| `disabled` | `boolean` |  | `false` | Trên ContextMenuTrigger: bỏ qua menu tùy chỉnh và giữ menu của trình duyệt. Trên các phần mục: ngăn việc chọn. |
| `asChild` | `boolean` |  | `false` | Trên ContextMenuTrigger hoặc ContextMenuItem: dùng phần tử con duy nhất làm phần tử được render. Link tương tác và field chữ giữ context menu native của chúng. |
| `longPressDelay` | `number` |  | `500` | Số mili giây một lần chạm phải được giữ trước khi menu mở. |
| `loop` | `boolean` |  | `false` | Trên ContextMenuContent: quay vòng điều hướng bằng phím mũi tên từ mục enabled cuối về mục đầu. |
| `avoidCollisions` | `boolean` |  | `true` | Trên ContextMenuContent: dịch hoặc lật menu để giữ nó trong viewport. |
| `collisionPadding` | `number \| Partial<Record<"top" \| "right" \| "bottom" \| "left", number>>` |  | `8` | Trên ContextMenuContent: khoảng cách tối thiểu tới mép viewport, tính bằng px. |
| `onSelect` | `(event: Event) => void` |  |  | Trên ContextMenuItem: chạy khi được chọn bằng pointer hoặc bàn phím. Gọi event.preventDefault() để giữ menu mở. |
| `destructive` | `boolean` |  | `false` | Trên ContextMenuItem: chữ và icon danger cho hành động destructive. |
| `inset` | `boolean` |  | `false` | Trên ContextMenuItem và ContextMenuLabel: căn chữ với các hàng có icon hoặc dấu chọn. |
| `leadingIcon` | `node` |  |  | Trên ContextMenuItem: icon trang trí 16px. Bị bỏ qua khi có asChild. |
| `shortcut` | `string` |  |  | Trên ContextMenuItem: gợi ý phím được hiện; ứng dụng xử lý shortcut. |
| `checked` | `boolean \| "indeterminate"` |  |  | Trên ContextMenuCheckboxItem: trạng thái check được kiểm soát. |
| `onCheckedChange` | `(checked: boolean) => void` |  |  | Trên ContextMenuCheckboxItem: nhận trạng thái check kế tiếp. |
| `value` | `string` |  |  | Trên ContextMenuRadioGroup: value đang được chọn. Trên ContextMenuRadioItem: định danh option bắt buộc. |
| `onValueChange` | `(value: string) => void` |  |  | Trên ContextMenuRadioGroup: nhận value của mục radio được chọn. |

### Slot

| Tên | Bắt buộc | Mô tả |
| --- | --- | --- |
| `children` | có | ContextMenuTrigger và ContextMenuContent với các phần mục. |

## Cấu tạo

| Phần | Mô tả |
| --- | --- |
| `root` | ContextMenu: giữ trạng thái mở. |
| `trigger` | ContextMenuTrigger: vùng lắng nghe nhấn chuột phải. |
| `content` | ContextMenuContent: cùng danh sách nổi như DropdownMenuContent, đặt tại pointer hoặc trigger. |
| `item` | ContextMenuItem: giống DropdownMenuItem. |
| `icon` | Icon đầu của một mục, như trong DropdownMenu. |
| `destructive` | Một ContextMenuItem destructive, như trong DropdownMenu. |
| `destructiveIcon` | Icon đầu của mục destructive, bằng màu chữ danger. |
| `indicator` | Dấu check hoặc chấm radio của mục checkbox hoặc radio, như trong DropdownMenu. |
| `groupLabel` | ContextMenuLabel: tiêu đề nhóm, như trong DropdownMenu. |
| `separator` | ContextMenuSeparator: một đường giữa các nhóm, như trong DropdownMenu. |
| `shortcut` | ContextMenuShortcut: gợi ý shortcut, như trong DropdownMenu. |

## Khả năng truy cập

Role: `menu`

| Phím | Hành động |
| --- | --- |
| `Shift+F10 / ContextMenu key` | Mở menu cho vùng trigger đang focus. |
| `ArrowDown / ArrowUp` | Di chuyển giữa các mục. |
| `Enter / Space` | Kích hoạt mục. |
| `Escape` | Đóng menu và trả focus về vùng trigger. |
| `Type a letter` | Chuyển tới mục kế tiếp bắt đầu bằng chữ đó. |

- Các mục là các phần mục của DropdownMenu (dùng chung kiểu và bàn phím), được xuất dưới tên ContextMenu.
- Vùng trigger phải focus được (tabIndex 0 với một tên truy cập) cho người dùng bàn phím.
- Menu được đặt tại pointer, hoặc tại đầu trigger khi mở từ bàn phím.
- Cảm ứng: nhấn giữ (khoảng 500ms, không di chuyển quá vài px) thì mở menu tại điểm chạm. Component tự làm việc này vì iOS Safari không phát contextmenu khi nhấn giữ. Nếu một sự kiện contextmenu native theo sau lần nhấn giữ, sự kiện đó bị bỏ qua để menu không mở hai lần.
- Chuột và bàn phím vẫn dùng được trên thiết bị có gắn chúng: nhấn chuột phải, Shift+F10 và phím Menu.
- Vùng trigger đặt -webkit-touch-callout: none và user-select: none để callout của trình duyệt và việc chọn chữ không tranh với nhấn giữ.
- Các phần submenu không được xuất. Dùng nhóm và separator cho các phần bên trong menu này.

## Cài đặt

```bash
npx werewolf-ui add context-menu
```

Chạy `npx werewolf-ui init` một lần trong app trước khi thêm component. Init cài token, thêm CSS import và luật cho agent. Add cài component này cùng các dependency và giữ nguyên theme của bạn. Xem [Cài đặt](https://www.werewolf-ui.dev/vi/docs/installation.md) để biết yêu cầu và cách cài qua registry.
