ContextMenu#
우클릭(데스크탑) 또는 길게 누르기(모바일)로 표시되는 컨텍스트 메뉴 컴포넌트입니다. 선택 가능한 액션 목록을 제공합니다.
Live Preview#
ContextMenu(
triggerChild: text('Right-click me'),
items: [
ContextMenuItem(label: 'Cut'),
ContextMenuItem(label: 'Copy'),
ContextMenuItem(label: 'Paste'),
],
)
ContextMenu(
triggerChild: Text('Right-click me'),
items: [
ContextMenuItem(label: 'Cut'),
ContextMenuItem(label: 'Copy'),
ContextMenuItem(label: 'Paste'),
],
)
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 파일, 항목, 이미지에 우클릭 시 편집/공유/삭제 등 상황별 액션을 제공할 때
- 데스크탑 앱 스타일의 오른쪽 클릭 메뉴가 필요할 때
- 모바일에서 길게 누르기로 항목 액션을 표시할 때
대신 다른 컴포넌트를 사용하세요:
DropdownMenu: 특정 버튼 클릭으로 열리는 메뉴에Menu: 사이드바나 네비게이션 메뉴 구성에
기본 사용법 (Basic Usage)#
// 기본 컨텍스트 메뉴
ContextMenu(
onSelect: handleContextMenuSelect,
items: [
ContextMenuItem(value: 'copy', label: '복사', icon: Icon(Icons.copy)),
ContextMenuItem(value: 'cut', label: '잘라내기', icon: Icon(Icons.cut)),
ContextMenuDivider(),
ContextMenuItem(value: 'paste', label: '붙여넣기', icon: Icon(Icons.paste)),
ContextMenuDivider(),
ContextMenuItem(
value: 'delete',
label: '삭제',
icon: Icon(Icons.delete),
isDestructive: true,
),
],
child: Container(
padding: EdgeInsets.all(kDefaultPadding),
color: Colors.grey[100],
child: Text('우클릭하거나 길게 누르세요'),
),
)
ContextMenu(
onSelect: handleContextMenuSelect,
items: [
ContextMenuItem(value: 'copy', label: '복사', icon: Icon(Icons.copy)),
ContextMenuItem(value: 'cut', label: '잘라내기', icon: Icon(Icons.cut)),
ContextMenuDivider(),
ContextMenuItem(value: 'paste', label: '붙여넣기', icon: Icon(Icons.paste)),
ContextMenuDivider(),
ContextMenuItem(
value: 'delete',
label: '삭제',
isDestructive: true,
),
],
child: Container(
child: Text('우클릭하세요'),
),
)
Props / Parameters#
ContextMenu#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
child | Widget | 필수 | 컨텍스트 메뉴를 붙일 대상 위젯 |
items |
List<ContextMenuEntry> |
필수 | 메뉴 아이템 목록 |
onSelect |
void Function(String value)? |
null |
아이템 선택 핸들러 |
dropdownMenuStyle |
CoreDropdownMenuStyle<Color, List<BoxShadow>>?
(Flutter) /
CoreDropdownMenuStyle<String, String>?
(Web)
|
null |
인스턴스 스타일 (Style 시스템 참조) |
스타일 시스템 (Style System)#
DropdownMenu / Menu / Menubar / ContextMenu 의 모든 chrome / dimensional / nested-slot 오버라이드는 공통
CoreDropdownMenuStyle<Clr, Shadow> 단일 슬롯으로 흐릅니다 (Epic #1302 원칙 6/7/8). 4 종류 위젯이지만 chrome 은 모두 동일 Style 클래스로 받습니다.
시맨틱 vs 스타일#
-
시맨틱 / behaviour: 위젯 파라미터로 직접 (
enabled,direction,behavior…) -
chrome / dimensional / 슬롯 스타일:
CoreDropdownMenuStyle한 곳으로 (popoverStyle/triggerStyle/itemStyle/headerTextStyle+ flatseparatorColor/separatorThickness/separatorIndent) - 변형 교체: asChild — 트리거 자식 (
child) 을 위젯으로 직접 주입
Resolve chain#
design system default for context menu
→ CoreDropdownMenuTheme.style // 프로젝트 공통
→ parent component slot override
→ widget.dropdownMenuStyle // 인스턴스별
슬롯 매핑 (CoreDropdownMenuStyle 7 필드)#
| 필드 | 타입 (Flutter) | 적용 영역 |
|---|---|---|
popoverStyle |
CorePopoverStyle<Color, List<BoxShadow>>? |
우클릭 / long-press 로 열린 컨텍스트 팝업 패널 chrome |
triggerStyle |
CoreButtonStyle<Color>? |
(사용 안 함 — child 직접 주입) |
itemStyle |
CoreButtonStyle<Color>? |
각 컨텍스트 메뉴 항목 chrome |
headerTextStyle |
CoreTextStyle<Color>? |
그룹 헤더 라벨 |
separatorColor | Color? | divider 색 |
separatorThickness | double? | divider 두께 |
separatorIndent | double? | divider 좌우 indent |
Migration — 옛 평면 필드 → 새 위치 매핑#
| 옛 chrome 필드 | 새 위치 |
|---|---|
ContextMenuTheme.surfaceBlur (Flutter) |
위젯 파라미터로 유지 (현재 backward-compat) |
ContextMenuTheme.surfaceOpacity (Flutter) |
위젯 파라미터로 유지 (현재 backward-compat) |
ContextMenuTheme.barrierColor | 별도 시스템 (overlay barrier — Style 슬롯 영역 밖) |
MenuPopupTheme.borderColor |
dropdownMenuStyle.popoverStyle.panelBorderColor |
MenuPopupTheme.borderRadius |
dropdownMenuStyle.popoverStyle.panelBorderRadius |
MenuPopupTheme.fillColor |
dropdownMenuStyle.popoverStyle.panelBackgroundColor |
MenuPopupTheme.padding |
dropdownMenuStyle.popoverStyle.panelPadding |
사용 예 (Flutter)#
ContextMenu(
dropdownMenuStyle: CoreDropdownMenuStyle(
popoverStyle: CorePopoverStyle(
panelBorderRadius: 8,
panelPadding: 4,
),
),
items: [
MenuButton(child: Text('Copy')),
MenuButton(child: Text('Paste')),
MenuDivider(),
MenuButton(child: Text('Delete')),
],
child: Container(),
)
사용 예 (Web)#
ContextMenu(
triggerChild: div([Component.text('Right-click me')]),
dropdownMenuStyle: CoreDropdownMenuStyle<String, String>(
popoverStyle: CorePopoverStyle<String, String>(
panelBorderRadius: 8,
),
),
items: [
ContextMenuItem(label: 'Copy', onSelect: () {}),
ContextMenuItem(label: 'Paste', onSelect: () {}),
],
)
ContextMenuItem#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
value | String | 필수 | 선택 시 onSelect에 전달되는 값 |
label | String | 필수 | 메뉴 아이템 텍스트 |
icon | Widget? | null | 아이템 아이콘 |
isDestructive |
bool |
false |
파괴적 액션 스타일 적용 (빨간색) |
enabled | bool | true | 활성화 여부 |
ContextMenuDivider#
메뉴 아이템 사이의 구분선입니다. 별도의 속성이 없습니다.
변형 (Variants)#
아이콘 포함#
아이콘과 함께 메뉴 아이템을 표시합니다.
ContextMenu(
onSelect: handleContextMenuSelect,
items: [
ContextMenuItem(
value: 'edit',
label: '편집',
icon: Icon(Icons.edit),
),
ContextMenuItem(
value: 'share',
label: '공유',
icon: Icon(Icons.share),
),
],
child: FileCard(name: '문서.pdf'),
)
파괴적 액션 포함#
삭제 등 위험한 액션은 빨간색으로 강조합니다.
ContextMenu(
onSelect: handleContextMenuSelect,
items: [
ContextMenuItem(value: 'rename', label: '이름 변경'),
ContextMenuDivider(),
ContextMenuItem(
value: 'delete',
label: '삭제',
isDestructive: true,
),
],
child: FileItem(name: '파일명'),
)
동작 스펙 (Behavior)#
인터랙션#
- 우클릭 (데스크탑): 마우스 우클릭 시 커서 위치에 메뉴 표시
- 길게 누르기 (모바일): 500ms 이상 터치 유지 시 메뉴 표시
- 항목 선택: 클릭/탭 시
onSelect(value)호출 후 메뉴 닫힘 - 외부 클릭: 메뉴 외부 클릭 또는 탭 시 메뉴 닫힘
상태 전환#
hidden→visible: 우클릭/길게 누르기 시visible→hidden: 항목 선택 또는 외부 클릭 시
애니메이션#
- 표시: 페이드 인 + 스케일 아웃 150ms
- 닫힘: 페이드 아웃 100ms
사용 가이드라인 (Usage Guidelines)#
✅ Do#
파괴적 액션은 구분선으로 분리하고 isDestructive 적용
CouiContextMenu(
onSelect: handleContextMenuSelect,
items: [
ContextMenuItem(value: 'copy', label: '복사'),
ContextMenuItem(value: 'rename', label: '이름 변경'),
ContextMenuDivider(), // 일반 액션과 구분
ContextMenuItem(
value: 'delete',
label: '삭제',
isDestructive: true, // 빨간색으로 강조
),
],
child: FileCard(name: '파일.pdf'),
)
파괴적 액션을 시각적으로 분리하면 사용자가 실수로 삭제를 선택하는 것을 방지한다.
❌ Don't#
너무 많은 메뉴 항목 사용 (7개 초과)
// ❌ 너무 많은 항목
CouiContextMenu(
items: List.generate(12, (i) => ContextMenuItem(
value: 'action_$i',
label: '액션 ${i + 1}',
)),
onSelect: handleContextMenuSelect,
child: targetWidget,
)
항목이 많으면 메뉴가 너무 길어져 스크롤이 필요하고 사용성이 떨어진다.
✅ Do#
현재 상황에 관련된 액션만 표시
CouiContextMenu(
onSelect: handleContextMenuSelect,
items: [
if (canEdit) ContextMenuItem(value: 'edit', label: '편집'),
if (canShare) ContextMenuItem(value: 'share', label: '공유'),
if (canDelete) ContextMenuItem(
value: 'delete',
label: '삭제',
isDestructive: true,
),
],
child: DocumentCard(document: document),
)
권한에 따라 동적으로 표시되는 항목은 사용자에게 혼란 없이 관련 액션만 제공한다.
❌ Don't#
컨텍스트 메뉴에만 접근 가능한 핵심 기능 배치
// ❌ 중요한 기능이 우클릭에만 있음
CouiContextMenu(
items: [
ContextMenuItem(value: 'save', label: '저장'), // 저장이 여기에만 있으면?
],
onSelect: handleContextMenuSelect,
child: Editor(),
)
컨텍스트 메뉴를 모르는 사용자나 키보드 사용자가 핵심 기능에 접근하지 못한다.
✅ Do#
현재 컨텍스트에 관련된 항목만 표시하세요.
CouiContextMenu(
items: [
ContextMenuItem(label: '복사', onTap: handleCopy),
ContextMenuItem(label: '잘라내기', onTap: handleCut),
ContextMenuItem(label: '붙여넣기', onTap: handlePaste, enabled: canPaste),
ContextMenuDivider(),
ContextMenuItem(label: '삭제', onTap: handleDelete, destructive: true),
],
child: EditableContent(),
)
컨텍스트와 관련 없는 항목을 포함하면 사용자가 원하는 액션을 찾기 어려워집니다. 상황에 맞는 항목만 노출하세요.
❌ Don't#
너무 많은 항목을 Context Menu에 넣지 마세요.
// ❌ 20개 이상의 항목을 Context Menu에 나열
CouiContextMenu(
items: allTwentyPlusActions, // 너무 많은 항목
child: content,
)
항목이 너무 많으면 스크롤이 필요하고 원하는 항목을 찾기 어렵습니다. 7개 이하로 제한하고 그룹화하세요.
접근성 (Accessibility)#
키보드 인터랙션#
| 키 | 동작 |
|---|---|
Shift + F10 | 포커스된 요소의 컨텍스트 메뉴 열기 |
Arrow Up/Down | 메뉴 항목 간 이동 |
Enter | 포커스된 항목 선택 |
Escape | 메뉴 닫기 |
스크린 리더#
- Flutter:
Semantics(customSemanticsActions: ...)으로 메뉴 항목을 시맨틱 액션으로 추가 - Web:
role="menu"+ 각 항목role="menuitem"자동 적용
터치 타겟#
- 각 메뉴 항목 최소 높이: 44dp
크로스 플랫폼 차이점 (Platform Differences)#
v3.0부터 기본 API (
enabled,onChanged등)가 통일되었습니다. 아래는 플랫폼 고유 차이점만 나열합니다.
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | CouiContextMenu | ContextMenu |
| 트리거 | GestureDetector (우클릭 + 길게 누르기) |
contextmenu 이벤트 |
| 위치 계산 | OverlayEntry + 포인터 위치 |
position: fixed + 마우스 좌표 |
| 기본 메뉴 차단 | 해당 없음 | event.preventDefault() 자동 |
관련 컴포넌트 (Related Components)#
- Menu: 네비게이션이나 사이드바에 사용하는 일반 메뉴
- DropdownMenu: 버튼 클릭으로 열리는 드롭다운 메뉴
조합 예제#
// 파일 탐색기에서 파일 우클릭 메뉴
CouiContextMenu(
onSelect: (value) {
switch (value) {
case 'open':
handleFileOpen(file);
case 'copy':
handleFileCopy(file);
case 'rename':
handleFileRename(file);
case 'delete':
handleFileDelete(file);
}
},
items: [
ContextMenuItem(value: 'open', label: '열기', icon: const Icon(Icons.open_in_new)),
ContextMenuItem(value: 'copy', label: '복사', icon: const Icon(Icons.copy)),
ContextMenuItem(value: 'rename', label: '이름 변경', icon: const Icon(Icons.edit)),
const ContextMenuDivider(),
ContextMenuItem(
value: 'delete',
label: '삭제',
icon: const Icon(Icons.delete),
isDestructive: true,
),
],
child: FileTile(file: file),
)