콘텐츠로 이동

collapse —— 폴더 패널

폴더 패널은 ‘콘텐츠가 너무 많아 기본은 접고 필요할 때 펼치는’ 표준 방안입니다: FAQ, 설정 항목 그룹, 긴 폼의 영역 나누기에서 흔히 봅니다. 상태 모델은 극도로 단순합니다. ’어떤 패널이 열려 있는지’의 배열이며, 난점은 아코디언 모드(동시에 하나만 열기)와 제어 값 정규화에만 있습니다.

  • 각 단말이 LhCollapse를 구현할 때: FAQ 질문/답변, 설정 그룹, 복잡한 폼의 영역 폴딩, 사이드바 메뉴 그룹.
  • 콘텐츠 블록을 독립적으로 접기/펼칠 수 있고, 세로 공간을 절약해야 하는 장면.
  • 사용하면 안 되는 곳: 고정 항목이 두세 개뿐인 경우(그냥 보여주는 것이 더 수월); 엄격한 선형 단계가 필요한 경우(steps 사용).
import {
normalizeActiveKeys,
toggleCollapseKey,
getCollapseItemClassNames,
} from '@lh-ui/share/core/collapse'
import type { LhCollapseKey } from '@lh-ui/share/core/collapse'

normalizeActiveKeys의 3상태 정규화: 제어하는 v-model이 단일 값('1'), 배열(['1','2']), undefined(모두 접기)을 넘길 수 있습니다. 배열로 통일 정규화한 후 하위의 includes 판정은 한 가지 형태만 다루면 됩니다. 이것은 ’입력은 관대하게, 내부는 엄격하게’의 또 다른 예입니다.

아코디언의 ‘0개 열림 허용’: toggleCollapseKey(keys, key, true)는 keys.includes(key)일 때 다른 항목을 유지하지 않고 []를 반환합니다. 아코디언에서 열려 있는 패널을 다시 누르면 모두 접힙니다. 이것은 직관에 맞는 동작입니다(다른 것으로 전환되는 것이 아님). 아코디언에 단일 값을 넘길 때 UI 계층의 v-model은 배열을 받게 되므로 직접 타입을 처리해야 합니다(또는 UI 계층이 아코디언 모드에 대해 추가로 단일 값 API를 제공).

불변 업데이트: 두 함수는 모두 새 배열을 반환하여 반응형 상태에 직접 먹이고, 입력 매개변수를 수정하지 않습니다.

함수 시그니처 반환 역할
normalizeActiveKeys (value: LhCollapseKey | LhCollapseKey[] | undefined) => LhCollapseKey[] 키 배열 3상태 정규화
toggleCollapseKey (keys, key, accordion: boolean) => LhCollapseKey[] 새 키 배열 아코디언 / 일반
getCollapseItemClassNames (state: { active, disabled }) => string[] 클래스명 펼침 / 비활성
export type LhCollapseKey = string | number

collapse는 APG accordion / disclosure 패턴의 전형적인 구현이며, 의미 규칙은 UI 계층이 조립합니다(core는 속성 묶음 함수를 제공하지 않습니다. 패턴이 단순하고 각 단말의 DOM 차이가 크기 때문):

<!-- 각 item은 하나의 disclosure -->
<div class="lh-collapse__item">
<h3>
<button type="button" aria-expanded="true" aria-controls="panel-1" id="trigger-1">
제목
</button>
</h3>
<div id="panel-1" role="region" aria-labelledby="trigger-1">콘텐츠</div>
</div>
  • aria-expanded는 트리거 버튼에 붙고 active와 같은 출처입니다. 이것이 접근성 폴더 패널의 핵심 속성입니다.
  • 패널 컨테이너의 role="region" + aria-labelledby가 트리거를 역으로 참조하여 양방향 연결을 만듭니다.
  • 비활성 항목: 트리거 버튼에 disabled를 붙입니다(단순한 클래스명이 아닌 진짜 disabled 속성). 키보드와 스크린 리더 모두 인식합니다.
  • 키보드: Tab으로 트리거 사이를 이동하고, Enter/Space로 펼침을 전환합니다.
  • 아코디언 모드에서 접힌 패널을 aria-hidden="true"로 숨기지 마세요. 콘텐츠가 여전히 DOM에 있습니다(애니메이션 중에 보임), CSS 높이 애니메이션만으로 충분합니다; 정말로 DOM을 제거한다면 aria-expanded의 상태를 잘 관리해야 합니다.
import { normalizeActiveKeys, toggleCollapseKey } from '@lh-ui/share/core/collapse'
normalizeActiveKeys('1') // ['1']
normalizeActiveKeys(['1', '2']) // ['1', '2']
normalizeActiveKeys(undefined) // []
toggleCollapseKey([], '1', false) // ['1']
toggleCollapseKey(['1'], '1', false) // [] (다시 누르면 접힘)
toggleCollapseKey(['1'], '2', false) // ['1', '2'] (일반 모드는 여러 개 열기 가능)
toggleCollapseKey(['1'], '1', true) // [] (아코디언: 다시 누르면 모두 접힘)
toggleCollapseKey(['1'], '2', true) // ['2'] (아코디언: 새로 열린 것만 유지)
<!-- Vue3 -->
<div class="lh-collapse">
<div
v-for="item in items"
:key="item.key"
:class="getCollapseItemClassNames({ active: activeKeys.includes(item.key), disabled: item.disabled })"
>
<h4 class="lh-collapse__header">
<button
type="button"
class="lh-collapse__trigger"
:disabled="item.disabled"
:aria-expanded="activeKeys.includes(item.key)"
:aria-controls="`lh-panel-${item.key}`"
:id="`lh-trigger-${item.key}`"
@click="onToggle(item)"
>
<span class="lh-collapse__arrow">{{ activeKeys.includes(item.key) ? '▾' : '▸' }}</span>
{{ item.title }}
</button>
</h4>
<div
v-show="activeKeys.includes(item.key)"
class="lh-collapse__panel"
:id="`lh-panel-${item.key}`"
role="region"
:aria-labelledby="`lh-trigger-${item.key}`"
>
<slot :name="item.key" :item="item" />
</div>
</div>
</div>
// 제어 + 아코디언
const activeKeys = computed(() => normalizeActiveKeys(props.modelValue))
function onToggle(item) {
if (item.disabled) return
emit('update:modelValue', toggleCollapseKey(activeKeys.value, item.key, props.accordion))
}
// React: 아코디언 모드에서 v-model이 단일 값인지 배열인지는 UI 계층의 API가 결정합니다;
// core는 항상 배열을 다루고, UI 계층이 emit할 때 필요에 따라 변환합니다(아코디언이고 단일 값만 필요할 때 [0]을 취함)
// uniapp: <view> + v-show; 펼침 애니메이션은 CSS max-height transition 사용(<view>가 지원)
클래스명 의미
.lh-collapse 기본 클래스
.lh-collapse__item 단일 패널 항목
.lh-collapse__item--active 펼침 상태
.lh-collapse__item--disabled 비활성
.lh-collapse__header / __trigger 머리글 / 트리거 버튼
.lh-collapse__arrow 펼침 화살표(회전 애니메이션)
.lh-collapse__panel 패널 콘텐츠 영역

머리글 배경색 --lh-fill-color, 화살표 --lh-text-color-secondary, 비활성 항목 --lh-text-disabled, 패널 테두리 --lh-border-color.

  • v-show vs v-if: 펼침 애니메이션에는 DOM이 상주해야 합니다(v-show + 높이/max-height transition). v-if를 쓰면 파괴 시 접힘 애니메이션을 재생할 수 없어 경험이 뻣뻣해집니다.
  • 제어 값 타입과 아코디언: 아코디언은 논리상 펼침 항목이 하나여야 하지만, toggleCollapseKey가 반환하는 것은 배열입니다([] 또는 [key]일 수 있음). UI 계층이 아코디언에 단일 값 API를 노출한다면 emit할 때 [0] ?? undefined를 취하세요; 배열 API를 유지해도 되지만, 그 경우 호출 쪽이 스스로 제약해야 합니다.
  • normalizeActiveKeys의 호출 시점: 매 렌더링마다 한 번 정규화하는 비용은 극히 낮습니다(Array.isArray 판정 한 번); 걱정된다면 computed로 캐시하세요.
  • 중첩 폴더 패널: 내부의 activeKeys는 바깥과 독립적이며, DOM이 중첩될 때 내부 패널의 role="region"이 다른 region 안에 들어갑니다. 이것은 허용되지만, aria-labelledby의 id는 전역적으로 유일해야 합니다(key에 접두어를 붙여 구성).
  • 애니메이션 중의 높이: max-height 방식은 충분히 큰 상한(예: 1000px)을 주어야 합니다. 그렇지 않으면 콘텐츠가 너무 높을 때 애니메이션 속도가 왜곡됩니다; 더 정확한 방식은 JS로 scrollHeight를 측정해 설정하는 것이며, core는 관여하지 않고 UI 계층이 직접 구현합니다.
  • id의 유일성: aria-controls / aria-labelledby는 id에 의존하고, collapse 인스턴스가 여러 개 공존하는 페이지에서는 key에 컴포넌트 수준의 접두어를 붙여야 합니다(UI 계층이 useId나 인스턴스 uid 사용).
@lh-ui/share/core/collapse → toggleCollapseKey( )
返回值
(填好参数后点运行)

Copyright © 2024-2026 FengMo WenXue