TabButton (탭버튼)
Va.TabButton — 탭 UI를 위한 선택형 버튼
브라우저 탭이나 IDE 탭처럼 선택 상태 + 닫기 버튼 + 하단 인디케이터 바(bar) 를 갖춘 버튼입니다. 일반 Button/ToggleButton과 달리 부모 탭 컨테이너와 협력하는 라디오형 동작이 내장되어, 자신이 선택되면 형제 TabButton들을 자동으로 해제합니다.
- 클래스: Va.TabButton — va_component.js:16599
- short name: tabButton
- 상속: Va.Component
- isContainer: false
- 기본 appearance: 'transparent' (탭 UI 관행에 맞춤)
- 베이스 CSS: va-tab-button

1. 기본 사용
{
tagName: 'tab', // 부모 탭 컨테이너 (.va-tab 클래스 필요)
tags: [{
tagName: 'tabButton',
text: '홈',
icon: 'ico_home',
selected: true // 초기 선택
}, {
tagName: 'tabButton',
text: '설정',
icon: 'ico_settings',
closable: true, // X 버튼 표시
onClose: 'onCloseTab'
}, {
tagName: 'tabButton',
text: '도움말'
}]
}
핵심 자동 동작: 사용자가 하나의 TabButton을 클릭하면, 클릭 이벤트 핸들러가 parentComponent('.va-tab')을 찾아 형제 컴포넌트 모두에 unselect()를 호출한 뒤 자신을 select()합니다 (va_component.js:16652-16669).
2. 다른 버튼과의 차이
항목Va.ButtonVa.ToggleButtonVa.TabButton
| 상태 유지 | ✕ | ✓ (pressed) | ✓ (selected) |
| 그룹 라디오 동작 | ✕ | ✕ (수동 그룹핑) | ✓ (부모 .va-tab이 있으면 자동) |
| 닫기 버튼 | ✕ | ✕ | ✓ (closable) |
| 하단 인디케이터 바 | ✕ | ✕ | ✓ (bar element) |
| 위치 스타일 (top/right/bottom/left) | ✕ | ✕ | ✓ (headerPosition) |
| ARIA 역할 | 없음 | 없음 | role="tab", aria-selected |
| 기본 appearance | default | default | transparent |
한 줄 요약: TabButton은 "탭 컨테이너 안에서 서로 배타적으로 선택되고, 닫힐 수 있는 버튼".
3. 주요 속성
탭 상태
속성기본값설명
| selected | false | 현재 선택된 탭 여부. role="tab", aria-selected 자동 반영 |
| closable | true | 닫기 X 버튼 표시 여부 |
| noBar | false | 하단 인디케이터 바 숨김 (opacity 0) |
| headerPosition | 'top' | 탭 헤더 위치 (top/right/bottom/left) — 바 위치와 스타일에 영향 |
텍스트/아이콘 (Button 공통)
속성설명
| text / innerHTML | 탭 라벨 |
| icon, iconPosition, iconOnly | 아이콘 |
| textAlign | 텍스트 정렬 |
| textellipsis | 폭 초과 시 말줄임(...) 처리 |
| appearance, shape, size | 시각 스타일 |
| disabled | 비활성화 |
| badge | 배지 부착 |
| stopPropagation | 이벤트 버블링 차단 (기본 true) |
textellipsis: true는 탭 목록에서 폭이 좁아질 때 라벨을 잘라주는 흔한 옵션입니다.
4. 이벤트
이벤트발생 시점
| click | 탭 클릭 (선택 상태 변경 이후 dispatch) |
| close | X 버튼 클릭 — 탭 전용 이벤트. 부모가 실제 제거 처리 |
| contextmenu | 우클릭 (preventDefault 자동) |
| focus / blur | 포커스 진입/이탈 |
close 이벤트 처리 패턴
onCloseTab(component, element, evt) {
// TabButton 자체는 사라지지 않음 — 부모가 삭제해야 함
const tab = this.getRef('mainTab');
tab.removeChild(component);
}
TabButton은 X 클릭 시 이벤트만 dispatch하고 실제 제거는 부모 컨테이너(Tab)가 담당합니다 — Va.Tab.removeChild()는 헤더와 콘텐츠를 함께 정리하도록 오버라이드되어 있음 (CLAUDE.md 3장 컨벤션 참조).
5. 메서드
메서드설명
| select() | 선택 상태로 전환 |
| unselect() | 선택 해제 |
| setText(text) / setInnerHTML(html) | 라벨 변경 |
| setIcon(icon) | 아이콘 변경 |
| setAppearance(v) / setShape(v) / setSize(v) | 시각 속성 변경 |
| setDisabled(bool) | 비활성화 |
| focus() | selected=true로 설정 + 포커스 |
| blur() | selected=false로 설정 + 블러 |
| setPressed(pressed) | (레거시 인터페이스, 실제로 쓸 일 거의 없음 — 주석에 "확인필요" 표기) |
⚠️ focus()/blur()의 부작용 주의: 일반 Button과 달리 TabButton의 focus()는 selected=true를 강제하고, blur()는 selected=false를 강제합니다. 단순히 포커스만 이동시키려는 의도로 호출하면 예기치 않게 탭 선택 상태가 바뀔 수 있습니다.
6. 내부 구조
<div elname="element" class="va-tab-button [top|right|bottom|left] [selected] [disabled]"
role="tab" aria-selected="true|false">
<span elname="inner" class="tab-button-inner">
<button elname="button" class="va-button transparent" tabindex="0">
<span elname="buttonInner" class="button-inner">
<span elname="icon" class="icon ico_xxx [left|right|top|bottom]">…</span> ← icon 지정 시
<span elname="text">라벨</span>
<span elname="close" class="icon close xsmall ico_dismiss"
aria-label="탭 닫기">✕</span> ← closable 시
</span>
</button>
<span elname="bar" class="bar" style="opacity: 1|0"></span> ← 선택 인디케이터
</span>
</div>
시각적 정체성은 bar element입니다 — 선택된 탭 아래(또는 headerPosition에 따라 옆)에 강조 선이 나타나는 흔한 탭 UI 패턴을 이 element로 구현합니다. noBar:true로 완전히 숨길 수 있습니다.
7. 라디오형 자동 선택 로직
가장 중요한 동작은 클릭 시 실행되는 형제 해제 로직입니다 (va_component.js:16656-16667):
let parentComponent = this.getParentComponent('.va-tab');
if (parentComponent != null) {
let parentNode = this.element.parentNode;
let childComponents = parentComponent.getChildComponentsInElement(parentNode);
for (let i = 0; i < childComponents.length; i++) {
if (typeof childComponents[i].select === 'function') {
childComponents[i].unselect();
}
}
}
this.select();
의미:
- 부모 트리에 .va-tab 클래스를 가진 컴포넌트가 있으면, 같은 부모 노드의 자식들 중 select 메서드를 가진 모든 컴포넌트를 unselect 한 뒤 자신을 select.
- 부모가 .va-tab이 아니면(예: 그냥 div 안에 넣으면) 자동 배타 선택이 동작하지 않습니다 — 그때는 하나의 ToggleButton처럼만 동작.
- select 메서드가 있는 형제라면 TabButton이 아니어도 unselect가 호출되므로, 커스텀 컴포넌트를 섞을 때 주의.
8. headerPosition이 하는 일
top/right/bottom/left 중 하나를 element 클래스로 추가합니다 (va_component.js:16710). CSS 측에서 이 클래스로 bar의 위치를 결정합니다 — top이면 하단 바, left면 우측 바 같은 식. 탭 그룹의 위치와 시각적으로 일치시키기 위한 속성.
부모 Tab 컨테이너가 이 값을 각 TabButton에 전달하는 게 일반적입니다.
9. 언제 쓰나
TabButton이 맞을 때
- 브라우저 탭·IDE 탭 같은 닫을 수 있는 탭 UI
- 콘텐츠 전환용 탭 헤더 (Tab 컨테이너와 함께)
- 하단(또는 사이드) 인디케이터 바가 있는 선택 UI
- 상호 배타적 선택이 필요한 목록
다른 걸 쓸 때
- 닫기 없는 심플한 탭 → Va.SimpleTabButton
- 사다리꼴 스타일 탭 (브라우저 탭 모양) → Va.TrapezoidButton (바로 다음에 정의된 형제)
- 상태만 유지하는 단독 버튼 → Va.ToggleButton
- 아코디언(펼침) → Va.AccordionButton
- 팝업 메뉴 → Va.MenuButton
10. 알아두면 좋을 주의사항
- 부모가 .va-tab이어야 자동 배타 선택 — 그냥 div 안에 여러 TabButton을 넣으면 여러 개가 동시 선택될 수 있음. Tab 컨테이너와 세트로 쓰는 것이 정석.
- close 이벤트는 알림만 — TabButton은 자기 자신을 지우지 않으므로 부모가 removeChild로 실제 제거해야 함.
- closable이 기본 true — 닫기 아이콘이 원치 않는 위치에도 뜰 수 있으니 명시적으로 closable:false 지정 권장.
- focus()/blur()가 선택 상태를 건드림 — 단순 포커스 제어 목적으로 호출하지 말 것.
- Va.Tab.remove(childComponent) 잔재 — CLAUDE.md에 언급된 대로 옛 컨벤션 위반 코드가 va_tab.js:733에 남아 있음. 새 코드는 removeChild(child)로 수렴.