Va.MenuButton — 드롭다운 팝업 메뉴 버튼
버튼을 클릭하면 아래로 팝업 메뉴가 펼쳐지고, 항목을 선택하면 접히는 컴포넌트입니다. 일반 Button과 달리 자식으로 menuItem을 담는 컨테이너이며, 팝업 위치·오토하이드·키보드 네비게이션까지 내장되어 있습니다.
- 클래스: Va.MenuButton — va_component.js:13203
- short name: menuButton
- 상속: Va.Component (Button 상속 아님, 별도 구현)
- isContainer: true — 자식 컴포넌트(menuItem)를 담을 수 있음
- 베이스 CSS: va-menu-button, 팝업은 va-menu-pop

1. 기본 사용
자식 태그로 메뉴 정의
{
tagName: 'menuButton',
text: '메뉴 열기',
tags: [
{ tagName: 'menuItem', key: '1', innerHTML: 'menu1' },
{ tagName: 'menuItem', key: '2', innerHTML: 'menu2' },
{ tagName: 'menuItem', seperator: true, size: 'xsmall' }, // 구분선
{ tagName: 'menuItem', key: '3', innerHTML: 'menu3' }
]
}
데이터 배열로 메뉴 정의 (template 방식)
{
tagName: 'menuButton',
text: '선택',
template: {
tagName: 'menuItem',
layout: 'ds-flex fd-row ai-center',
key: '{key}',
innerHTML: '{display}'
},
data: [
{ key: '1', display: 'menu1', list: [ // ← 중첩 서브메뉴
{ key: '1-1', display: 'menu1-1' },
{ key: '1-2', display: 'menu1-2' }
]},
{ key: '2', display: 'menu2' },
{ key: '3', display: 'menu3' }
]
}
data 배열의 각 항목에 list를 넣으면 자동으로 계층형 서브메뉴가 생성됩니다.
2. Va.Button / CompoundButton과의 차이
항목Va.ButtonVa.CompoundButtonVa.MenuButton
| 역할 | 액션 트리거 | 정보성 액션 | 선택 팝업 |
| 컨테이너 여부 | ✕ | ✕ | ✓ (자식 menuItem) |
| 팝업 상태 | — | — | expanded (true/false) |
| 서브메뉴 지원 | — | — | ✓ (data.list 재귀) |
| 오토하이드 | — | — | ✓ (Va.addAutoHide) |
| 키보드 네비 | 기본 focus만 | 기본 focus만 | Esc/Tab/Enter/↓↑ 처리 |
| 전용 이벤트 | iconClick | — | itemClick / select / expand / collapse / beforePop / afterPop / hidePop |
MenuButton은 사실상 "버튼 + 팝업 컨테이너 + DataManager"를 통합한 미니 위젯입니다.
3. 주요 속성
버튼 표현 (Button과 공통 규약)
속성설명
| text / innerHTML | 버튼 라벨 |
| icon, iconPosition, iconOnly, iconSize, iconRotate | 좌측(또는 지정 위치) 아이콘 |
| appearance, shape, size | 시각 스타일 (Button 규약과 동일) |
| textAlign | 텍스트 정렬 |
| disabled | 비활성화 |
| badge | 배지 부착 |
| stopPropagation | 이벤트 버블링 차단 (기본 true) |
| tabIndex | 탭 순서 (기본 0) |
메뉴 아이콘 (드롭다운 화살표)
속성기본값설명
| menuIcon | 'ico_chevron_down' | 우측 화살표 아이콘 |
| menuIconExpand | 'ico_chevron_down' | 확장 상태 아이콘 |
| menuIconVisible | true | 화살표 표시 여부 |
| menuIconSize | — | 화살표 크기 |
iconOnly:true일 땐 자동으로 화살표가 숨겨집니다.
팝업 제어
속성기본값설명
| expanded | false | 현재 펼침 상태 |
| popWidth | (버튼 폭) | 팝업 너비. 미지정 시 버튼 폭과 동일 |
| popMaxHeight | — | 팝업 최대 높이. 넘치면 스크롤 |
| autoHide | true | 외부 클릭 시 자동 닫힘 |
| expandChildCheck | true | 자식이 없으면 팝업 안 열림 |
| ignorePopPosition | — | afterPop 후 재배치 무시 (외부에서 위치 제어할 때) |
데이터
속성설명
| data | 배열. 자동으로 menuItem 자식 생성 |
| template | data 각 원소를 어떤 menuItem 구조로 렌더할지 |
| fakeData | 에디터(runtime='edit') 환경에서만 사용되는 미리보기 데이터 |
4. 이벤트
이벤트발생 시점
| click | 버튼 자체 클릭 |
| contextmenu | 우클릭 |
| focus / blur | 포커스 진입/이탈 |
| itemClick | 메뉴 항목 클릭 |
| select | 항목 선택 (itemClick과 함께 발생) |
| expand | 팝업 열림 |
| collapse | 팝업 닫힘 |
| beforePop | 팝업 표시 직전 |
| afterPop | 팝업 표시 직후 (위치 재조정 훅) |
| hidePop | 팝업 숨김 요청 시 |
itemClick / select 콜백 시그니처
onSelect(component, element, menuItem, data, isFolder, evt) {
// menuItem: 클릭된 Va.MenuItem 인스턴스
// data: 해당 항목의 데이터 객체
// isFolder: 하위 list가 있는 폴더성 항목인지
}
5. 메서드
메서드설명
| setText(text) / setInnerHTML(html) | 라벨 변경 |
| setIcon(icon) | 아이콘 변경 |
| setData(data) | 메뉴 데이터 재바인딩 (기존 항목 replaceChildren) |
| expand() | 팝업 열기 (200ms 지연) |
| collapse() | 팝업 닫기 (200ms 지연) |
| showPop() / hidePop() | 팝업 즉시 표시/숨김 |
| focus() / blur() | 포커스 제어 |
| append(component) | 자식 추가 (자동으로 component.menuButton = this 연결) |
6. 내부 구조
<div elname="element" class="va-menu-button">
<div elname="inner" class="menu-button-inner">
<button elname="button" class="va-button" tabindex="0">
<div elname="buttonInner" class="button-inner">
<span elname="content" class="button-content">
<span elname="icon" class="icon">…</span> ← icon 지정 시
<span elname="text" class="text">라벨</span>
</span>
<span elname="menuIcon" class="icon menu ico_chevron_down">…</span>
<span cpname="badge">…</span> ← badge 지정 시
</div>
</button>
<div elname="popDiv" class="menu-button-pop-div"> ← 원위치 컨테이너
<div elname="pop" class="va-menu-pop"> ← 열릴 때 hiddenArea로 이동
<div elname="popInner" class="menu-pop-inner">
<!-- menuItem들이 여기 렌더됨 (containerElement) -->
</div>
</div>
</div>
</div>
</div>
핵심 트릭: 팝업이 열릴 때 popElement를 getHiddenAreaElement()로 옮겨서 부모 컨테이너의 overflow:hidden, z-index 스택에 영향받지 않게 합니다 (va_component.js:13349). 닫히면 다시 원위치.
7. 팝업 위치 로직
showPop()이 매번 호출될 때 (va_component.js:13721-13748):
- 기본은 버튼 아래 — top = 버튼.bottom + 2px
- 너비: popWidth 지정 없으면 버튼 폭과 동일 (min = max로 고정)
- 뷰포트 하단을 넘치면 위로 뒤집기 — top = 버튼.top - 팝업.height - 3px
- IntersectionObserver로 팝업이 화면 밖으로 밀렸을 때 자동 재표시 (va_component.js:13305-13315)
8. 오토하이드 / 키보드
오토하이드 (프레임워크 팝업 상태머신 연동)
if(this.autoHide == null || this.autoHide == true){
Va.addAutoHide(this.popElement);
}
전역 mousedown / resize / mousewheel / touchmove 이벤트가 발생하면 팝업이 닫힙니다. 프레임워크 공용 팝업 상태머신을 사용하므로, DatePicker/Combobox/Drawer 등 다른 팝업과 자연스럽게 상호 배타적으로 동작합니다.
⚠️ 이 상태머신은 CLAUDE.md 6장에서 "민감 영역"으로 지정되어 있습니다 — 여러 컴포넌트가 공유하므로 커스터마이징 시 주의.
키보드 조작 (va_component.js:13396-13426)
키동작
| Enter | 팝업 열기 + 첫 항목 포커스 |
| ↓ (ArrowDown) | (Enter와 동일 분기로 흡수) 첫 항목 포커스 |
| ↑ (ArrowUp) | 기본 스크롤 방지 |
| Esc / Tab | 팝업 닫고 버튼에 포커스 복귀 |
9. 서브메뉴 (재귀 데이터)
data의 각 원소가 list 필드를 가지면 자동으로 하위 Va.MenuItem이 만들어져 서브메뉴가 됩니다. Va.setMenuTemplate()이 재귀적으로 트리를 만들어 주고, expandChildCheck로 빈 폴더 방지, itemClickChild 이벤트로 하위 클릭을 상위로 버블링합니다.
data: [{
key: 'file',
display: 'File',
list: [
{ key: 'open', display: 'Open' },
{ key: 'save', display: 'Save', list: [
{ key: 'save-as', display: 'Save As...' }
]}
]
}]
10. 언제 쓰나
MenuButton이 맞을 때
- 액션 여러 개를 하나의 버튼으로 묶고 싶을 때 (More · 더보기 · Actions)
- 계층형 선택지 (파일 메뉴, 카테고리 선택)
- 커맨드 팔레트, 툴바 드롭다운
다른 걸 쓸 때
- 값 선택이 목적 (검색·필터 포함) → Va.Combobox / Va.Filterbox
- 단일 액션 → Va.Button
- 우클릭 컨텍스트 메뉴 → Va.ContextMenu
- 액션 실행 + 옵션 드롭다운을 분리하고 싶을 때 → Va.SplitButton
'컴포넌트 > 버튼' 카테고리의 다른 글
| SplitButton (분할버튼) (0) | 2026.09.09 |
|---|---|
| ResponsiveMenuButton (응답형메뉴버튼) (0) | 2026.09.09 |
| ExpandButton (확장버튼) (0) | 2026.09.09 |
| CompoundButton (복합버튼) (0) | 2026.09.09 |
| Button (버튼) (0) | 2026.09.09 |