steps 단계 표시 —— 내비게이션
단계 표시는 “흐름의 진행 표시기”입니다: 장바구니의 “장바구니 → 정보 입력 → 결제 → 완료”, 마법사 폼. tabs와의 차이는 순서 의미입니다——단계에는 선후가 있고, 상태가 위치에서 파생됩니다(완료/진행 중/대기). core/steps가 관리하는 것은 상태 도출과 아주 잘못 작성하기 쉬운 레이아웃 디테일입니다: 가로 단계 표시가 줄 바꿈될 때, 줄 끝의 연결선을 숨겨야 합니다.
사용 시기
섹션 제목: “사용 시기”- 각 단말에서
<LhSteps>/<LhStep>구현: 주문 흐름, 마법사 폼, 작업 진행, 다단계 설정. - 흐름에 명확한 순서가 있고 사용자가 “어디까지 왔는지” 알아야 하는 경우.
- 사용하면 안 되는 경우: 같은 레벨의 콘텐츠 전환(
tabs사용——순서 의미 없음); 단순한 진행 퍼센트(progress사용); 계층 내비게이션(menu나breadcrumb사용).
가져오기
섹션 제목: “가져오기”import { getStepStatus, getStepsClassNames, getStepClassNames, getStepsLineLastIndices, getStepAria,} from '@lh-ui/share/core/steps'import type { LhStepStatus, LhStepsDirection, LhStepStatusState, LhStepClassState,} from '@lh-ui/share/core/steps'설계 의도
섹션 제목: “설계 의도”상태는 위치에서 파생: getStepStatus의 핵심 규칙은 index < current → finish, === → process, > → wait입니다. 대부분의 상황에서 current만 전달하면 상태가 자동으로 계산됩니다——UI 계층이 매 단계마다 상태를 명시적으로 선언할 필요가 없습니다.
forced는 전체 덮어쓰기: Steps 레벨의 forced 상태(예: error)는 모든 단계를 그 상태로 만듭니다. 이것은 “흐름 전체 실패”의 의미입니다: 결제 실패 시 어느 한 단계가 실패한 것이 아니라 흐름 전체가 계속 불가능해집니다. 우선순위가 가장 높습니다(위치 도출보다 먼저 반환).
마지막 항목은 클래스명을 출력하지 않고 :last-child에 위임: 이 모듈에서 가장 미묘한 설계입니다. 주석에 이유가 적혀 있습니다: 자식 항목은 각자의 setup 단계에서 렌더링되며, 이때 이후 형제가 아직 등록되지 않았기 때문에 등록 총수로 계산한 isLast는 모든 항목을 첫 화면(SSR/SSG 포함)에서 마지막 항목으로 잘못 판정합니다. 구조적 :last-child 선택자는 CSS 렌더링 시점에 평가되므로 본래부터 정확합니다——대가는 연결선 스타일이 반드시 구조 의사 클래스로 구현되어야 한다는 것입니다.
getStepsLineLastIndices가 줄 바꿈 문제를 해결: 가로 단계 표시가 고정 너비로 줄 바꿈된 뒤, 각 줄의 마지막 항목의 연결선이 줄 끝의 빈 공간(다음 줄의 줄 머리)을 가리켜 시각이 끊어집니다. 이 함수는 인접 항목의 offsetTop 차이로 줄 바꿈 지점을 찾습니다: offsetTops[i+1] > offsetTops[i]이면 i+1이 다음 줄로 넘어간 것이고, i가 줄 끝입니다. 줄 끝 항목(전역 마지막 항목 포함)은 UI 계층이 이에 따라 line-last 클래스를 추가해 연결선을 숨깁니다.
lineLast와 isLast는 다른 것: isLast(전역 마지막 항목)는 :last-child가 담당하고; lineLast(줄 끝 항목)은 측정해서 얻는 것이며, UI 계층이 렌더링 후 offsetTop을 읽어 되돌려 넣어야 합니다——가로 줄 바꿈 상황에서만 필요합니다.
함수 표
섹션 제목: “함수 표”| 함수 | 시그니처 | 반환 | 역할 |
|---|---|---|---|
getStepStatus |
(state: LhStepStatusState) => LhStepStatus |
상태 | forced 덮어쓰기 / 위치 도출 |
getStepsClassNames |
(direction: LhStepsDirection) => string[] |
클래스명 | 방향 |
getStepClassNames |
(state: LhStepClassState) => string[] |
클래스명 | 상태 + 클릭 가능 + 줄 끝 |
getStepsLineLastIndices |
(offsetTops: number[]) => number[] |
줄 끝 인덱스 | 가로 줄 바꿈 탐지 |
getStepAria |
(status: LhStepStatus) => LhAriaAttrs |
aria 패키지 | aria-current=“step” |
타입 정의
섹션 제목: “타입 정의”export type LhStepStatus = 'wait' | 'process' | 'finish' | 'error'export type LhStepsDirection = 'horizontal' | 'vertical'
export interface LhStepStatusState { index: number // 단계 인덱스(0부터) current: number // 현재 단계 인덱스 forced?: LhStepStatus // Steps 레벨의 전체 덮어쓰기 상태}
export interface LhStepClassState { status: LhStepStatus clickable: boolean lineLast?: boolean // 현재 줄의 마지막 항목인지(가로 줄 바꿈 시 UI 계층이 측정)}접근성
섹션 제목: “접근성”getStepAria('process') // { 'aria-current': 'step' }getStepAria('finish') // {}getStepAria('error') // {}- aria-current=“step”으로 현재 단계 표시: 이것은 WAI-ARIA의 전문 의미로, 스크린 리더가 “현재 단계”로 읽어 줍니다——“진행 중”이라는 텍스트 힌트보다 표준적입니다. process 상태에서만 출력합니다.
- 순서 있는 목록 의미: 단계 표시는
<ol>로 렌더링하는 것을 권장합니다——순서 의미가 내장되어 있어 스크린 리더가 “2번째 항목, 총 4개”로 읽어 줍니다.<div>로 구현한다면role="list"를 추가하고 각 항목에role="listitem"을 부여해야 합니다. - 상태는 색상이 아니라 텍스트로도: finish의 체크, process의 숫자, wait의 회색——색맹 사용자는 구분할 수 없습니다. 각 단계의 제목 텍스트(“정보 입력”)와 아이콘의 조합이 비로소 완전합니다.
- error 단계의 음성: 오류 상태는 UI 계층에서
aria-invalid를 사용하거나 단계 제목 뒤에 “(실패)” 텍스트를 추가하는 것이 좋습니다; 빨간 아이콘만으로는 충분히 전달되지 않습니다. - 클릭 가능한 단계의 버튼 의미:
clickable: true일 때 단계 제목은<button>으로 렌더링해야(완료된 단계로 되돌아가기 가능), 키보드로 닿을 수 있어야 합니다; 클릭 불가능할 때는 순수 텍스트입니다.
사용 예시
섹션 제목: “사용 예시”import { getStepStatus, getStepsLineLastIndices } from '@lh-ui/share/core/steps'
getStepStatus({ index: 0, current: 2 }) // 'finish'getStepStatus({ index: 2, current: 2 }) // 'process'getStepStatus({ index: 3, current: 2 }) // 'wait'getStepStatus({ index: 0, current: 2, forced: 'error' }) // 'error'(forced가 전체 덮어쓰기)getStepStatus({ index: 2, current: 2, forced: 'wait' }) // 'wait'
// 가로 줄 바꿈(줄당 2개, 4개가 2줄로):getStepsLineLastIndices([10, 10, 60, 60]) // [1, 3](인덱스 2의 offsetTop이 더 큼 → 1이 줄 끝; 3은 전역 마지막)getStepsLineLastIndices([10, 10, 10]) // [2](줄 바꿈 없음, 전역 마지막만)getStepsLineLastIndices([]) // []// UI 계층: 렌더링 후 offsetTop 측정, lineLast 클래스 되돌려 넣기(가로만 필요)const items = ref<HTMLElement[]>([])const lineLastSet = ref<Set<number>>(new Set())
onMounted(() => { const offsetTops = items.value.map((el) => el.offsetTop) lineLastSet.value = new Set(getStepsLineLastIndices(offsetTops))})// 창 resize 감시: 줄 바꿈 지점은 너비에 따라 변함<!-- Vue3: 주문 흐름 --><template> <LhSteps :current="1"> <LhStep title="장바구니" description="선택한 상품 3개" /> <LhStep title="정보 입력" description="배송지 및 영수증" /> <LhStep title="결제" /> <LhStep title="완료" /> </LhSteps></template><!-- 전체 실패(결제 실패 시 흐름 전체를 빨간색으로) --><template> <LhSteps :current="2" status="error"> <LhStep title="장바구니" /><LhStep title="정보 입력" /> <LhStep title="결제" description="결제 시간 초과, 다시 시도해 주세요" /> <LhStep title="완료" /> </LhSteps></template>// React: useLayoutEffect로 offsetTop 측정(첫 프레임 깜빡임 방지), resize 리스너로 재계산// uniapp: <view> + flex; 가로 줄 바꿈은 미니프로그램 환경에서 createSelectorQuery로 top을 취득스타일과 토큰
섹션 제목: “스타일과 토큰”| 클래스명 | 의미 |
|---|---|
.lh-steps |
루트 컨테이너(flex) |
.lh-steps--{horizontal|vertical} |
방향 |
.lh-steps__item |
단계 항목 |
.lh-steps__item--{wait|process|finish|error} |
상태(원 색상 + 텍스트 색상 + 연결선 색상) |
.lh-steps__item--clickable |
클릭 가능(hover 강조 + 포인터) |
.lh-steps__item--line-last |
줄 끝 항목(줄 끝 빈 공간을 가리키는 연결선 숨김) |
.lh-steps__circle / __title / __description |
원 / 제목 / 설명 |
.lh-steps__line |
연결선(:last-child로 전역 마지막 항목의 선을 숨김) |
상태 색상: wait --lh-text-placeholder, process --lh-primary-color, finish --lh-success-color, error --lh-danger-color; 연결선은 보통 --lh-border-color, 완료 구간은 finish 색상.
주의 사항
섹션 제목: “주의 사항”lineLast는 가로 줄 바꿈 상황에서만 필요: 세로 단계 표시는 매 항목이 한 줄을 독차지하므로 연결선이 자연스럽게 아래로 향하고 줄 끝 문제가 없습니다. 고정 너비의 가로 단계 표시(줄 바꿈 없음)도 필요 없습니다——고정 너비 + 줄 바꿈 허용일 때만 측정합니다.- 측정 시점:
offsetTop은 DOM 렌더링 이후에 읽어야 합니다(onMounted / useLayoutEffect), 그리고 창 resize 시 재계산해야 합니다. 첫 프레임에 깜빡임이 있을 수 있습니다(연결선이 보였다가 숨겨짐)——감수할 수 있습니다; 엄격한 상황에서는visibility로 표시를 지연시키세요. forced는 개별 단계가 아니라 전체 덮어쓰기:statusprop에 error를 전달하면 모든 단계가 error가 됩니다, 완료된 단계도 포함——이것은 “흐름 전체 실패”의 의미입니다. 특정 단계 하나만 실패로 표시하려면 해당 LhStep에 별도로 status를 전달하세요(UI 계층이 단계별 status를 forced의 개별 버전으로 합치거나, 해당 항목의 클래스명을 직접 덮어씀).current는 범위를 벗어날 수 있습니다:current: 5인데 단계가 4개뿐이면 전부 finish가 됩니다(index가 모두 < 5)——“흐름 완료” 종료 상태 표시에 사용하며, 특별한 분기가 필요 없습니다.- SSR/SSG의 마지막 항목 함정: 이것이
getStepClassNames가 isLast 매개변수를 받지 않는 이유입니다. setup 시점에 “등록 총수”로 마지막 항목을 계산하는 구현은 첫 화면에서 모든 항목을 잘못 판정합니다——:last-child를 사용하세요. - 클릭으로 되돌아가기의 비즈니스 제약:
clickable은 보통 이미 완료된 단계에만 활성화합니다(정보를 다시 보기 위해), 진행 중이나 대기 단계는 점프 불가입니다. 임의의 단계로의 점프를 허용할지는 비즈니스가 결정하고, core은 클래스명만 내놓습니다. - description의 공간: 가로 단계 표시에 설명이 있으면 높이가 증가하며, 같은 줄에서 줄 바꿈된 줄 높이는 가장 높은 항목이 결정합니다——offsetTop을 측정하기 전에 모든 항목의 렌더링이 끝났는지 확인하세요.
@lh-ui/share/core/steps → getStepStatus( )(填好参数后点运行)Copyright © 2024-2026 FengMo WenXue