Drop page / section descriptions, state notes and repeated captions from the representative Stories of 44 components, the Patterns, Center Graphic and OrgChart pages. Value readouts, spec lists and captions hide only on the representative page; hidden verification Stories keep them. Tabs Default API and the Filter Bar draft / applied counts move to the hidden DefaultApi and DraftApplied Stories; Center Overview is hidden with its direct URL kept. Story IDs are unchanged. Align the component, pattern and OrgChart docs with the pages, point the quality reference check at the DraftApplied Story, and make the P8 styles check expect no section header on the preview-only Expanded page.
13 KiB
Date Picker
현재 접근성 보완(2026-10-10): 흰 Trigger의 빈 값 안내는 기존
--hm-sem-fg-secondary를 사용한다. 기본 Theme에서 #6e6759/흰색이며 공통 placeholder Token과 Disabled opacity는 보존한다. 아래 과거 검증의 #999 실측은 당시 기록이다.
현재 HM 구현의 기준 문서. 공통 원칙은 docs/guidelines/프로젝트-지침.md(§6, §7), Token 구조 원칙은 테마-토큰-가이드.md를 따른다. 이 문서는 Date Picker 고유의 API·Token·상태·결정만 기록한다.
1. 상태와 성격
| 항목 | 내용 |
|---|---|
| 성격 | Reusable component, Popover + Calendar 조합 (새 날짜 engine 없음) |
| 구현 상태 | 구현 완료 (single-date, 현재 HM 규격) |
| 기준일 | 2026-10-02 |
| 기반 | Popover(public API) + Calendar(public API) 조합. shadcn 공식 Date Picker는 예시 조합이다 |
| 대표 Story | Components/Search & Date/Date Picker → All (이름 "Date Picker") |
| 보류 | 아래 §11 |
2. 적용 근거
- 참고 근거: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Date Picker가 이름뿐이고 규격이 없다. 참고할 수치가 없었다.
- Component로 만든 이유: 서비스의 모든 날짜 선택이 같은 trigger 텍스트 형식, 선택 후 닫힘, 값 처리, focus 복귀를 필요로 해 반복 사용이 확실하다고 판단했다. 값 처리(
value/defaultValue/onValueChange)는 패키지가 소유한다. - 구현 방식: Popover(Portal, collision, Escape, focus 복귀)와 Calendar(날짜 모델, 키보드)를 그대로 조합한다. Popover 내부 구현을 복제하지 않았다.
- Trigger: Button이 아니라 Input/Select의 field language를 따르는 필드형 버튼이다. Button/Input/Select Token은 참조하지 않고
--hm-date-picker-*로 관리한다. - 보류: §11.
3. 기본 규격 (현재 HM 규격)
| 항목 | 값 |
|---|---|
| Size | large(기본, 안쪽 42px + 1px border = 44px, 16 / 24), medium(안쪽 32px = 34px, 14 / 20) — Select와 같은 두 크기 |
| Trigger | 폭 100%(부모 폭), padding-x 12px, 1px --hm-sem-stroke-input-default, radius 4px, 흰 면, 오른쪽 달력 아이콘 18px --hm-sem-fg-secondary |
| 값 / placeholder | 선택값은 --hm-sem-fg-primary, placeholder("날짜 선택")는 --hm-sem-fg-secondary, 길면 말줄임 |
| 날짜 표시 | 로컬 Date의 연·월·일 → YYYY.MM.DD (예: 2026.01.09, 2026.10.06). format prop으로 교체 |
| Popup | Popover 면(흰 면, 1px pale border, 4px radius, 그림자)에 Popover padding 0, 폭 auto로 Calendar에 맞춤 |
4. 상태 규격
| 상태 | 값 |
|---|---|
| Empty / Selected | data-empty가 있으면 placeholder 색 |
| Focus / Open | 테두리 전체가 --hm-sem-stroke-input-focus(ring 없음, Input·Select와 같은 방식). 열림은 Radix data-state="open" |
Invalid (aria-invalid="true") |
Trigger border 1px --hm-sem-action-destructive. 새 invalid / error / status prop 없음 |
Invalid + Keyboard Focus (:focus-visible) |
Destructive border 유지 + Focus Semantic 3px / 50% outer ring |
| Invalid + Open | Trigger Error border 유지. Calendar Selected는 기존 Active, 날짜 Keyboard Focus는 기존 Focus Semantic 유지 |
| Disabled | opacity 0.6(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), cursor not-allowed, 열리지 않음 |
| 선택 후 | 값 갱신, Popover 닫힘, focus가 trigger로 복귀 |
5. API
export: DatePicker, type DatePickerSize.
value/defaultValue(Date),onValueChange(date),placeholder,size"medium" | "large"(기본large),disabled,format(date) => string,calendarProps(disabled,startMonth,endMonth,locale,captionLayout,weekStartsOn,today), 그리고buttonprops(id,aria-label,className…).- 선택은
required라 선택된 날짜를 다시 눌러도 해제되지 않는다(해제/clear UI는 없음). - 접근성: Trigger는
button에 Radix Popover의aria-haspopup="dialog",aria-expanded, 팝업은role="dialog". 접근 가능한 이름은<label htmlFor=id>(Story) 또는aria-label을 소비자가 준다. 선택값은 버튼 텍스트로 읽힌다. - Popover가 열릴 때 Radix 기본은 첫 tabbable(이전 달 버튼)에 focus하므로, DatePicker가
onOpenAutoFocus로 Calendar의 tab stop(선택일, 없으면 오늘)에 focus를 보낸다. - 범위(range) Date Picker는 만들지 않았다(필요가 명확해질 때 추가).
값 소유권과 외부 초기화
controlled?: boolean은 선택적 값 소유권 설정이다.true이면value가undefined여도 Controlled이며, 부모 값만 표시한다.defaultValue는 표시값을 대신하지 않는다. 사용자 선택은 기존(Date | undefined) => void콜백으로 전달하며, 부모가 값을 반영하지 않으면 표시값은 유지된다.controlled={false}이면 내부 state가 소유권을 가진다.value는 사용하지 않고defaultValue를 최초 마운트 시에만 적용한다. 이후defaultValue변경은 초기화가 아니다. 사용자 선택은 내부 state를 갱신하고 콜백을 호출한다.- Legacy 호환:
controlled를 생략하면 기존value !== undefined판정을 유지한다. 날짜가 있으면 Controlled,undefined이면 Uncontrolled이다. 이 방식으로 외부 값을undefined로 바꾸면 과거 내부 값/defaultValue가 다시 표시될 수 있다. 기존 소비 코드의 동작은 변경하지 않았다. - 신규 Controlled 사용과 FilterBar 연동은
controlled={true}를 사용한다.controlled옵션과 값 소유권 모드는 마운트 중 변경하지 않는 사용 계약을 권장한다. Controlled에서defaultValue를 함께 전달해도 부모value가 우선한다.
const [date, setDate] = useState<Date | undefined>()
<DatePicker controlled={true} value={date} onValueChange={setDate} />
// 외부 날짜 지정/갱신: setDate(new Date(2026, 9, 6))
// 외부 초기화: setDate(undefined) — key remount 불필요
외부 값 갱신/초기화는 onValueChange를 호출하지 않는다. FilterBar의 부모는 날짜 state와 조회 조건을 함께 초기화한다. 선택값 clear는 팝업 닫기나 달력의 현재 월/focus 초기화를 의미하지 않으며, 새 clear UI를 추가하지 않는다. controlled를 생략한 Legacy 호출의 remount 우회는 이 신규 계약과 구분한다.
6. Dependency
새 dependency 없음(Calendar의 react-day-picker, Popover의 Radix를 그대로 사용). 날짜 표시는 Intl.DateTimeFormat, 값은 native Date이며 별도 date 라이브러리를 추가하지 않았다.
7. Token
Component Mapping 위치: packages/ui/src/theme/date-picker.css (--hm-date-picker-*). Trigger만 스타일하며 Popover와 Calendar의 스타일은 중복 정의하지 않는다. 선택일 색은 Calendar Selected(--hm-sem-state-active)를 그대로 상속하며 Date Picker 전용 selected 색이나 scope override는 없다.
height-md|lg, border-width, padding-x, gap, radius, font-size|line-height-md|lg, background, text, placeholder, border-color, focus-border-color, icon-size|color, disabled-opacity. Semantic/Theme palette를 참조하며 --hm-popover-*와 --hm-calendar-*는 참조하지 않는다. 기본 모양은 @layer components에 두어 소비자의 className(폭, 높이)이 이긴다.
Popover 결합 지점: Popup의 padding과 폭을 PopoverContent의 inline style로 --hm-popover-padding: 0px, --hm-popover-width: auto를 지정해 Popover의 token 값을 인스턴스 단위로 덮는다. Popover 코드를 바꾸지 않는 선택이며, Popover token 이름에 대한 약한 결합이다.
8. 조합한 컴포넌트에서 유지한 항목
Popover: Portal, collision 처리, Escape, 바깥 클릭 닫힘, trigger focus 복귀, role=dialog. Calendar: 키보드(방향키, Enter/Space, PageUp/Down), disabled 날짜, 월 이동, locale(기본 한국어).
9. Storybook
- 대표 Story
All("Date Picker"): Empty, Selected(2026.10.06), 기본 형식(2026.01.09)/custom override, Controlled(대표 화면은 값 표시 숨김, 숨김Controlled는 표시), Medium, Disabled dates(5일 이전·일요일), Disabled를 한 페이지에 둔다. Label은 Story 조합이며today는 Calendar prop으로 2026-10-02에 고정. - verification:
Empty,Selected,Controlled.
10. 사용자 확정 결정
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다. Date Picker는 실제 소비 API가 필요한 Component로 만든다(작업 지시).
- 2026-10-06 Visual QA: Trigger 기본 formatter만
YYYY.MM.DD로 변경. Calendar 날짜 셀·caption·locale, customformat은 유지.
11. 미확정 / 보류
- 기본 표시 형식은
YYYY.MM.DD로 확정했다. 다른 형식은format으로 교체한다. - 선택 해제(clear) 버튼, 직접 입력(typing) 입력, Range Date Picker, 시간 선택은 만들지 않았다.
- 타임존: native
Date의 로컬 시간 기준이다. - Popup의 방향/collision은 Popover 기본(
align="start", 아래쪽 우선). - 좁은 화면 전용 picker(바텀시트 등)는 만들지 않았다.
12. 관련 파일
packages/ui/src/components/ui/date-picker.tsx
packages/ui/src/theme/date-picker.css
packages/ui/src/theme.css (import)
packages/ui/src/index.ts (export)
src/components/date-picker.stories.tsx
13. 검증 상태
-
2026-10-08 Controlled 초기화: 실제 소스를 직접 묶은 격리 Headless Edge 환경에서 수정 전 과거 값 재표시를 재현했다.
controlled={true}의 undefined → A → B → undefined, 최초 Controlled 값 clear, 부모 선택 거부, 외부 값 갱신, 콜백 횟수, Legacy/defaultValue/명시적 Uncontrolled, 열린 팝업 clear, 키보드 날짜 선택·focus 복귀를 확인했다. 소스 기준 영향 범위 타입 검사 통과. 공용 dist / Storybook 빌드 및 전체 시각 회귀는 수행하지 않았다. -
2026-10-06 Visual QA: 1280/1024/768/390px에서
2026.10.06,2026.01.09, custom override, Korean caption, 키보드 날짜 선택과 focus 복귀 통과.typecheck,build,build-storybook통과. 아래 2026-10-02 interaction의 긴 한국어 날짜 표시는 당시 기록이다. -
npm run typecheck,npm run build: 통과(2026-10-02). -
실제 interaction(Browser pane): trigger에서 Enter로 열림(
aria-haspopup=dialog,aria-expandedtrue), 선택된 날짜/오늘에 focus, ArrowRight 2 → 3, Enter로 선택하면 trigger 텍스트가2026년 10월 3일로 바뀌고 Popover가 닫히며 focus가 trigger로 복귀(controlled 값2026. 10. 3.). 다시 열면 선택일(3)에 focus, Escape로 닫혀도 값은 유지되고 trigger로 복귀, 날짜 클릭(20)으로도 선택과 닫힘. Disabled trigger는 클릭해도 열리지 않음. -
computed style: Large 44px / 16·24, Medium 34px / 14, border
rgb(222,219,210), radius 4px, placeholderrgb(153,153,153), Disabled opacity 0.6·cursor not-allowed. -
Responsive(390×844): trigger 폭 320px, popup 264px(왼쪽 23px)이 viewport 안에 들어가고 페이지 가로 overflow 없음.
-
수정한 문제: 처음에
PopoverContent에data-slot="date-picker-content"를 넘겨 Popover의data-slot을 덮어쓰는 바람에 Popover 스타일(윤곽, 그림자)이 빠졌다. 제거했다. -
미확인: Disabled dates가 popup 안에서 선택 불가한 것(Calendar 문서의 검증으로 갈음), 포인터 hover 시각, 좁은 viewport의 collision flip,
calendarProps.locale교체. -
숨김
InvalidFocusStory (tags: ["verification"])를 추가하여 기본 Sidebar는 유지한다.
Invalid + Focus 검증 (2026-10-07)
- Focus ring:
--ring→--hm-sem-stroke-input-focus→--hm-color-border-focus. Invalid border: 기존--hm-sem-action-destructive. 새 Runtime Role / Production Error prop / 메시지 컴포넌트 없음. - Chromium computed style: Invalid border
rgb(211, 47, 47), Invalid + Keyboard Focus box-shadowcolor(srgb 0.14902 0.4 0.317647 / 0.5) 0px 0px 0px 3px,outline-style: none. Invalid 단독은box-shadow: none. - Theme Designer UI에서 Focus
#c75b00, Destructive#d41466, Active#007f73, Accent#ffd36d로 분리. Invalid borderrgb(212, 20, 102)+ Focus shadowcolor(srgb 0.780392 0.356863 0 / 0.5) 0px 0px 0px 3px확인. npm run typecheck,npm run build,npm run build-storybook통과. 기존 문서 예시 CSS warning 및 Storybook chunk 크기 / plugin timing warning 유지.- 검증 범위: Chromium 환경. 다른 브라우저의 렌더링은 별도 확인이 필요하다.
- 숨김
InvalidFocus의 실제 Trigger DOMaria-invalid="true"확인. Trigger Invalid border computed 1px, Focus 전후 shadow none → 3px Focus ring. 대표AllDefault / Selected / Medium / Disabled 유지. - Enter / Space open, ArrowRight 날짜 이동, Enter 선택 후 닫힘 및 Trigger focus return, Escape close / focus return, Tab / Shift+Tab 확인. 2026.10.06 → 2026.10.07 선택값 표시 확인.
- Calendar Selected
rgb(0, 127, 115)(Active), 선택 날짜 Focuscolor(srgb 0.780392 0.356863 0 / 0.5) 0px 0px 0px 3px유지. 일반 Calendar 대표 Story의 Single 및 Range 끝점은 Active, Range 중간 면은 Accent 45% 유지. Calendar CSS/API 수정 없음.