DatePicker | CoUI

DatePicker

날짜 선택 컴포넌트

DatePicker#

캘린더 UI를 통해 날짜를 선택할 수 있는 컴포넌트입니다.

CoDatePicker는 Flutter/Web 양쪽에서 동일한 named-parameter API를 제공하는 통일 컴포넌트입니다. 내부적으로 팝업 캘린더는 CoCalendar를 사용합니다.

Live Preview#

기본 (single + popover)#

Web
Flutter
Loading Flutter...
class DatePickerDefaultExample extends StatefulComponent {
  const DatePickerDefaultExample({super.key});

  @override
  State<DatePickerDefaultExample> createState() =>
      _DatePickerDefaultExampleState();
}

class _DatePickerDefaultExampleState extends State<DatePickerDefaultExample> {
  String? _value;

  @override
  Component build(BuildContext context) {
    return div(
      [
        CoDatePicker(
          onChanged: (v) => setState(() => _value = v),
          placeholder: 'Select date',
          value: _value,
        ),
      ],
      classes: 'w-${CoreSpace.scale.space256}',
    );
  }
}
class DatePickerDefaultExample extends StatefulWidget {
  const DatePickerDefaultExample({super.key});

  @override
  State<DatePickerDefaultExample> createState() =>
      _DatePickerDefaultExampleState();
}

class _DatePickerDefaultExampleState extends State<DatePickerDefaultExample> {
  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: CoreSpace.space256,
      child: CoDatePicker.simple(
        placeholder: 'Select date',
        onChanged: (_) {},
      ),
    );
  }
}

단일 + 다이얼로그 모드#

Web
Flutter
Loading Flutter...
class DatePickerDialogExample extends StatefulComponent {
  const DatePickerDialogExample({super.key});

  @override
  State<DatePickerDialogExample> createState() =>
      _DatePickerDialogExampleState();
}

class _DatePickerDialogExampleState extends State<DatePickerDialogExample> {
  String? _value;

  @override
  Component build(BuildContext context) {
    return div(
      [
        CoDatePicker(
          onChanged: (v) => setState(() => _value = v),
          value: _value,
          mode: CorePromptMode.dialog,
          placeholder: 'Select date',
          dialogTitle: const Text('Pick a date'),
        ),
      ],
      classes: 'w-${CoreSpace.scale.space256}',
    );
  }
}
class DatePickerDialogExample extends StatefulWidget {
  const DatePickerDialogExample({super.key});

  @override
  State<DatePickerDialogExample> createState() =>
      _DatePickerDialogExampleState();
}

class _DatePickerDialogExampleState extends State<DatePickerDialogExample> {
  DateTime? _value;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: CoreSpace.space256,
      child: CoDatePicker(
        onChanged: (v) => setState(() => _value = v),
        value: _value,
        mode: CorePromptMode.dialog,
        placeholder: 'Select date',
        dialogTitle: const Text('Pick a date'),
      ),
    );
  }
}

범위 + 팝오버 모드#

Web
Flutter
Loading Flutter...
class DatePickerRangeExample extends StatefulComponent {
  const DatePickerRangeExample({super.key});

  @override
  State<DatePickerRangeExample> createState() =>
      _DatePickerRangeExampleState();
}

class _DatePickerRangeExampleState extends State<DatePickerRangeExample> {
  ({String start, String end})? _value;

  @override
  Component build(BuildContext context) {
    return div(
      [
        CoDateRangePicker(
          onChanged: (v) => setState(() => _value = v),
          value: _value,
          mode: CorePromptMode.popover,
          placeholder: Text('Select range'),
        ),
      ],
      classes: 'w-${CoreSpace.scale.space256}',
    );
  }
}
class DatePickerRangeExample extends StatefulWidget {
  const DatePickerRangeExample({super.key});

  @override
  State<DatePickerRangeExample> createState() =>
      _DatePickerRangeExampleState();
}

class _DatePickerRangeExampleState extends State<DatePickerRangeExample> {
  DateTimeRange? _value;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: CoreSpace.space256,
      child: CoDateRangePicker(
        onChanged: (v) => setState(() => _value = v),
        value: _value,
        mode: CorePromptMode.popover,
        placeholder: const Text('Select range'),
      ),
    );
  }
}

범위 + 다이얼로그 모드 (CoDateRangePicker 기본)#

Web
Flutter
Loading Flutter...
class DatePickerRangeDialogExample extends StatefulComponent {
  const DatePickerRangeDialogExample({super.key});

  @override
  State<DatePickerRangeDialogExample> createState() =>
      _DatePickerRangeDialogExampleState();
}

class _DatePickerRangeDialogExampleState
    extends State<DatePickerRangeDialogExample> {
  ({String start, String end})? _value;

  @override
  Component build(BuildContext context) {
    return div(
      [
        CoDateRangePicker(
          onChanged: (v) => setState(() => _value = v),
          value: _value,
          placeholder: Text('Select range'),
          dialogTitle: const Text('Pick a range'),
        ),
      ],
      classes: 'w-${CoreSpace.scale.space256}',
    );
  }
}
class DatePickerRangeDialogExample extends StatefulWidget {
  const DatePickerRangeDialogExample({super.key});

  @override
  State<DatePickerRangeDialogExample> createState() =>
      _DatePickerRangeDialogExampleState();
}

class _DatePickerRangeDialogExampleState
    extends State<DatePickerRangeDialogExample> {
  DateTimeRange? _value;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: CoreSpace.space256,
      child: CoDateRangePicker(
        onChanged: (v) => setState(() => _value = v),
        value: _value,
        placeholder: const Text('Select range'),
        dialogTitle: const Text('Pick a range'),
      ),
    );
  }
}

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 폼에서 날짜 입력이 필요할 때 (생년월일, 예약일 등)
  • 트리거 버튼과 캘린더 팝업을 결합한 UI가 필요할 때
  • 체크인/체크아웃 같은 날짜 범위 선택이 필요할 때 (Flutter에서 CoDateRangePicker 사용)

대신 다른 컴포넌트를 사용하세요:

  • CoCalendar: 캘린더를 항상 인라인으로 표시할 때
  • CoTextField: 날짜 형식 텍스트만 입력받을 때
  • CoSelect: 미리 정의된 날짜 옵션 중 선택할 때

기본 사용법 (Basic Usage)#

// 기본 날짜 선택 — 내부 상태 관리 포함
CoDatePicker.simple(
  placeholder: 'Select date',
  onChanged: handleDateChanged,
)

// 외부 상태로 제어
CoDatePicker(
  value: selectedDate,
  placeholder: 'Select date',
  onChanged: handleDateChanged,
)

// 날짜 범위 선택
CoDateRangePicker(
  value: selectedRange,
  onChanged: handleRangeChanged,
)
// 기본 날짜 선택 — ISO 8601 문자열(YYYY-MM-DD)로 값 전달
CoDatePicker(
  onChanged: handleDateChanged,
  placeholder: 'Select date',
  value: selectedDate,
)

// Dialog 모드 — 중앙 모달 + Cancel/Save 액션
CoDatePicker(
  onChanged: handleDateChanged,
  value: selectedDate,
  mode: CorePromptMode.dialog,
  placeholder: 'Select date',
  dialogTitle: const Text('Pick a date'),
)

// 날짜 범위 — Web도 CoDateRangePicker 지원
CoDateRangePicker(
  onChanged: handleRangeChanged,
  value: selectedRange, // ({start: 'YYYY-MM-DD', end: 'YYYY-MM-DD'})
  mode: CorePromptMode.popover,
  placeholder: const Text('Select range'),
)

// 범위 + Dialog 모드 (CoDateRangePicker 기본)
CoDateRangePicker(
  onChanged: handleRangeChanged,
  value: selectedRange,
  placeholder: const Text('Select range'),
  dialogTitle: const Text('Pick a range'),
)

Props / Parameters#

공통 Contract 필드 (CoreDatePickerContract):

속성Flutter 타입Web 타입기본값설명
placeholder String? String? null 값이 없을 때 표시 텍스트
enabled bool bool true 상호작용 허용 여부
onChanged ValueChanged<DateTime?>? CoreValueChanged<String>? null 값 변경 콜백
min DateTime? String? (ISO) null 최소 선택 가능 날짜
max DateTime? String? (ISO) null 최대 선택 가능 날짜
label String? String? null 트리거 상단 라벨
description String? String? null 트리거 하단 보조 텍스트
errorText String? String? null 에러 메시지. 존재 시 description은 숨김
mode CorePromptMode? CorePromptMode? popover (single) / dialog (range) 표시 모드 (popover/dialog)
dialogTitle Widget? Component? null 다이얼로그 모드 제목 슬롯
datePickerStyle (single) / dateRangePickerStyle (range) CoreDatePickerStyle<Color, List<BoxShadow>>? CoreDatePickerStyle<String, String>? null 인스턴스 스타일 (Style 시스템 참조)

Flutter 확장 필드:

속성타입기본값설명
value DateTime? null 현재 선택된 날짜
initialView CoreCalendarView? null 초기 캘린더 뷰 (null 시 value 기준)
stateBuilder CoreCalendarDateStateBuilder? null 각 날짜 셀 상태 빌더
placeholderWidget Widget? null 커스텀 placeholder 위젯
popoverAlignment AlignmentGeometry? Alignment.topLeft 팝오버 정렬점
popoverAnchorAlignment AlignmentGeometry? Alignment.bottomLeft 앵커 정렬점

Web 확장 필드:

속성타입기본값설명
value String? null 현재 값 (ISO 8601)
idString?nullDOM 요소 id
classes String? null 트리거에 적용할 추가 CSS 클래스
css Styles? null 루트에 적용할 인라인 스타일

스타일 시스템 (Style System)#

DatePicker 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreDatePickerStyle<Clr, Shadow> 단일 슬롯으로 흐릅니다 (Epic #1302 원칙 6/7/8). 평면 chrome 필드(borderRadius / padding / height / gap / iconSize / popoverPadding / popoverGap)는 모두 제거되었습니다.

시맨틱 vs 스타일#

  • 시맨틱 enum / behaviour: 위젯 파라미터로 직접 (mode, placeholder, enabled, onChanged, min, max, label, description, errorText, dialogTitle)
  • chrome / dimensional / 슬롯 스타일: CoreDatePickerStyle 한곳으로 (popoverStyle / dialogStyle / calendarStyle / labelStyle / descriptionStyle / errorStyle)
  • 변형(variant) 교체: asChild — dialogTitle / 커스텀 placeholder / 커스텀 builder (range) 등의 슬롯에 직접 위젯 주입

Resolve chain#

design system default for date picker
  → CoreDatePickerTheme.style                  // 프로젝트 공통
  → parent component slot override
  → widget.datePickerStyle                     // 인스턴스별 (range: dateRangePickerStyle)

각 nested 슬롯 스타일 (popoverStyle / dialogStyle / calendarStyle) 은 자기 컴포넌트의 자체 resolve chain 으로 다시 한 번 머지됩니다. 예: popoverStyle.triggerStyle 의 chrome 은 → CoreButtonTheme → variant 룩업 → 부모 슬롯 → widget.datePickerStyle.popoverStyle.triggerStyle 순.

옛 평면 필드 → 새 위치 매핑#

옛 chrome 필드새 위치
heightpopoverStyle.triggerStyle.height
padding popoverStyle.triggerStyle.paddingH 또는 popoverStyle.panelPadding (의도에 따라)
borderRadiuspopoverStyle.triggerStyle.borderRadius
gap (트리거 내부)popoverStyle.triggerStyle.gap
iconSizepopoverStyle.triggerStyle.trailingIconStyle.size
popoverPaddingpopoverStyle.panelPadding
popoverGappopoverStyle.gap

사용 예 (Flutter)#

CoDatePicker(
  placeholder: 'Select date',
  onChanged: handleChanged,
  datePickerStyle: CoreDatePickerStyle(
    popoverStyle: CorePopoverStyle(
      panelBorderRadius: 12,
      gap: 8,
      triggerStyle: CoreButtonStyle(
        height: 44,
        paddingH: 16,
      ),
    ),
    calendarStyle: CoreCalendarStyle(
      selectedColor: Color(0xFF3B82F6),
    ),
    labelStyle: CoreTextStyle(fontWeight: 600),
  ),
)

변형 (Variants)#

Controlled (Flutter)#

// 내부 상태 관리 — simple 팩토리 또는 ControlledCoDatePicker
ControlledCoDatePicker(
  initialValue: DateTime.now(),
  onChanged: handleDateChanged,
)

날짜 범위 선택 (Flutter)#

CoDateRangePicker(
  value: DateTimeRange(checkIn, checkOut),
  onChanged: handleRangeChanged,
)

비활성 날짜 지정 (Flutter)#

CoDatePicker(
  value: selectedDate,
  onChanged: handleDateChanged,
  stateBuilder: (date) =>
      date.weekday <= 5
          ? CoreCalendarDateState.enabled
          : CoreCalendarDateState.disabled,
)

동작 스펙 (Behavior)#

표시 모드 (Flutter)#

// Popover 모드 (기본) — 트리거 아래에 캘린더 드롭다운
CoDatePicker(
  mode: PromptMode.popover,
  popoverAlignment: Alignment.topLeft,
  popoverAnchorAlignment: Alignment.bottomLeft,
  value: selectedDate,
  onChanged: handleDateChanged,
)

// Dialog 모드 — 전체 화면 모달에 캘린더 표시
CoDatePicker(
  mode: PromptMode.dialog,
  dialogTitle: Text('날짜 선택'),
  value: selectedDate,
  onChanged: handleDateChanged,
)

CoDatePickerController (Flutter)#

final controller = CoDatePickerController(DateTime(2024, 3, 15));

// 프로그래매틱 제어
controller.value = DateTime.now();
  • ValueNotifier<DateTime?> 기반
  • ComponentController mixin으로 폼 통합

범위 선택 (Flutter)#

  • CoDateRangePicker는 기본 PromptMode.dialog 사용
  • CoCalendar의 range 모드를 활용하여 시작/끝 날짜를 동시에 선택

Web 구현#

  • 트리거: <div> 기반 field-style 렌더링 (CoIcon(CoLucideIcons.calendar | calendarRange) + placeholder/선택 값을 Text chain 으로 표시)
  • Popover mode: CoPopover 사용 — CoOverlayHost.popover 레이어로 portal, ancestor overflow: hidden / transform 영향 없음
  • Dialog mode: CoDialog 사용 — open boolean 으로 제어, Cancel/Save 액션으로 값 commit
  • 인터랙션: 클릭 / outside-click / Escape — CoPopover / CoDialog 가 모두 처리 (직접 document listener 불필요)
  • Single 값은 ISO 8601 (YYYY-MM-DD) 문자열, range 값은 ({String start, String end}) 레코드
  • min/max는 ISO 문자열로 전달 (내부에서 DateTime으로 파싱)

사용 가이드라인 (Usage Guidelines)#

Do#

적절한 placeholder로 기대 형식을 안내하세요.

CoDatePicker(
  placeholder: 'YYYY-MM-DD',
  value: selectedDate,
  onChanged: handleDateChanged,
)

빈 상태에서 사용자가 무엇을 입력해야 하는지 알 수 있습니다.


Don't#

미래/과거 날짜를 선택 가능하게 두지 마세요 (해당되는 경우).

// Bad — 과거 날짜에 예약 가능
CoDatePicker(value: selectedDate, onChanged: handleDateChanged)

// Good — 미래 날짜만 선택 가능
CoDatePicker(
  min: DateTime.now(),
  value: selectedDate,
  onChanged: handleDateChanged,
)

Do#

범위 선택에는 CoDateRangePicker를 사용하세요 (Flutter).

CoDateRangePicker(
  value: DateTimeRange(checkIn, checkOut),
  onChanged: (range) {
    setState(() { checkIn = range?.start; checkOut = range?.end; });
  },
)

시작/끝 날짜가 시각적으로 연결되어 사용자가 범위를 쉽게 이해합니다.


Don't#

두 개의 독립 CoDatePicker로 범위를 구현하지 마세요.

시작일이 끝일보다 뒤일 수 있는 오류가 발생합니다. CoDateRangePicker가 자동으로 순서를 보장합니다.

접근성 (Accessibility)#

키보드 인터랙션#

동작
Enter / Space캘린더 팝업 열기/닫기
/ 이전/다음 날짜 이동 (캘린더 내)
/ 이전/다음 주 이동 (캘린더 내)
Page Up/Down이전/다음 월 이동 (캘린더 내)
Escape팝업 닫기

스크린 리더#

  • 트리거에 현재 선택 날짜 읽힘
  • 팝업 캘린더에 role="application", aria-label="Calendar" 설정
  • errorText 존재 시 aria-invalid="true" 설정
  • 비활성 상태에서 aria-disabled="true" 설정

포커스 관리#

  • 팝업 열림 시 캘린더로 포커스 이동
  • 날짜 선택 또는 Escape 시 트리거로 포커스 복귀

크로스 플랫폼 차이점 (Platform Differences)#

CoDatePickerCoreDatePickerContract를 통해 핵심 API(placeholder, enabled, onChanged, min, max, label, description, errorText)를 통일합니다. 아래는 플랫폼 고유 차이점만 나열합니다.

항목FlutterWeb
값 타입 DateTime? (single) / DateTimeRange? (range) String? ISO 8601 (single) / ({String start, String end})? (range)
표시 모드 CorePromptMode (popover/dialog) CorePromptMode (popover/dialog)
범위 선택 CoDateRangePicker (Flutter) CoDateRangePicker (Web) — 동일 contract
컨트롤러CoDatePickerController (외부 상태)내부 상태 관리
캘린더 UICoCalendarCoCalendar (동일)
Popover 인프라 CoPopover + _popoverController CoPopover + _popoverController (동일)
Dialog 인프라 CoDialog + showDialog CoDialog (open boolean 제어)
  • Calendar: 인라인 캘린더. CoDatePicker는 내부적으로 CoCalendar를 팝업으로 감쌉니다
  • TextField: 텍스트 입력. DatePicker는 트리거 + Calendar 조합
  • Form: 폼 컨테이너. CoDatePicker를 폼 필드로 사용 가능