DatePicker | CoUI
본문으로 건너뛰기
LogoCoUI

DatePicker

날짜 선택 컴포넌트

목차

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

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

기본 사용법 (Basic Usage)#

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

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

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

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

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

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

빠른 오버라이드 (Chain)#

이미 만든 DatePicker 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.

class DatePickerChainExample extends StatefulWidget {
  const DatePickerChainExample({super.key});

  @override
  State<DatePickerChainExample> createState() => _DatePickerChainExampleState();
}

class _DatePickerChainExampleState extends State<DatePickerChainExample> {
  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: CoreSpace.space256,
      child:
          ControlledDatePicker(
            placeholder: 'Select date',
            onChanged: (_) {},
          ).withStyle(
            const CoreDatePickerStyle(
              contentColor: CoreColor.token(CoreColors.primary),
              placeholderColor: CoreColor.token(CoreColors.onSurfaceVariant),
              valueTypography: CoreTextStyle.token(CoreTextStyles.bodyLarge),
              trailingIconStyle: CoreIconStyle(
                size: CoreSize.size20,
                color: CoreColor.token(CoreColors.primary),
              ),
            ),
          ),
    );
  }
}
class DatePickerChainExample extends StatefulComponent {
  const DatePickerChainExample({super.key});

  @override
  State<DatePickerChainExample> createState() => _DatePickerChainExampleState();
}

class _DatePickerChainExampleState extends State<DatePickerChainExample> {
  String? _value;

  @override
  Component build(BuildContext context) {
    return div(
      [
        DatePicker(
          onChanged: (v) => setState(() => _value = v),
          placeholder: 'Select date',
          value: _value,
        ).withStyle(
          const CoreDatePickerStyle(
            contentColor: CoreColor.token(CoreColors.primary),
            placeholderColor: CoreColor.token(CoreColors.onSurfaceVariant),
            valueTypography: CoreTextStyle.token(CoreTextStyles.bodyLarge),
            trailingIconStyle: CoreIconStyle(
              size: CoreSize.size20,
              color: CoreColor.token(CoreColors.primary),
            ),
          ),
        ),
      ],
      classes: 'w-${CoreSpace.scale.space256}',
    );
  }
}

Live Preview#

기본 (single + popover)#

단일 + 다이얼로그 모드#

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(
      [
        DatePicker(
          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: DatePicker(
        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(
      [
        DateRangePicker(
          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: DateRangePicker(
        onChanged: (v) => setState(() => _value = v),
        value: _value,
        mode: CorePromptMode.popover,
        placeholder: const Text('Select range'),
      ),
    );
  }
}

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

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(
      [
        DateRangePicker(
          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: DateRangePicker(
        onChanged: (v) => setState(() => _value = v),
        value: _value,
        placeholder: const Text('Select range'),
        dialogTitle: const Text('Pick a range'),
      ),
    );
  }
}

사용 시기 (When to Use)#

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

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

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

  • TimePicker: 날짜 값 대신 시간 값을 선택할 때
  • Calendar: 캘린더를 항상 인라인으로 표시할 때
  • TextField: 날짜 그리드 없이 문자열만 받는 경우. 날짜 파싱·검증은 호출부가 담당
  • Select: 미리 정의된 날짜 옵션 중 값 하나를 선택할 때

Props / Parameters#

공통 Contract 필드 (CoreDatePickerContract)를 한 표로 정리합니다. W 는 Flutter Widget / Web Jaspr Component, D 는 날짜 표현인 Flutter DateTime / Web ISO String 입니다. 이 기호는 설명용이며 새로운 공개 타입이 아닙니다.

속성타입기본값설명
value D? null single의 현재 선택 날짜
onChanged void Function(D?)? null single 날짜 선택·해제 커밋
placeholder String? null 값이 없을 때 표시 텍스트
placeholderWidget W? null DatePicker의 커스텀 placeholder. 문자열보다 우선
enabledbooltrue상호작용 허용 여부
minD?null최소 선택 가능 날짜
maxD?null최대 선택 가능 날짜
labelString?null트리거 상단 라벨
labelMaxLines int? null 라벨 줄 수 제한. null이면 제한 없음
labelOverflow CoreTextOverflow? null 라벨 넘침 처리
description String? null 트리거 하단 보조 텍스트
errorText String? null 오류 메시지. 존재 시 description 숨김
mode CorePromptMode? single: popover / range: dialog 선택을 표시할 표면
popoverPlacement CorePopoverPlacement bottomStart popover의 트리거 기준 위치
stateBuilder CoreCalendarDateStateBuilder? null 내부 Calendar의 날짜 상태 판정
initialView CoreCalendarView? null DatePicker의 초기 캘린더 뷰. null이면 value 기준
firstDayOfWeek int DateTime.sunday 내부 Calendar 의 한 주 시작 요일( DateTime.monday … DateTime.sunday ). 로케일에서 유도하지 않고 Calendar 로 그대로 전달
dialogTitle W? null 다이얼로그 제목 슬롯
datePickerStyle CoreDatePickerStyle? null single 스타일 슬롯

DateRangePicker#

범위 선택의 value / onChanged 는 Flutter DateTimeRange?, Web ({String start, String end})? 값을 받습니다. 스타일 슬롯 이름은 양쪽 모두 dateRangePickerStyle 이며 firstDayOfWeek 를 포함한 나머지 공통 필드의 의미는 위와 같습니다. initialView 의 범위 선택 생성자 표면은 현재 양쪽이 일치하지 않는 API 결손이 있습니다.

Flutter 의 builder 가 돌려주는 위젯은 필드의 글자(value) 가 될 수 없어 읽기 전용 TextField 의 placeholder 자리에 놓이고, 값 색(contentColor)을 입습니다(value 는 비어 있습니다).

스타일 시스템 (Style System)#

DatePicker 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreDatePickerStyle 단일 슬롯으로 흐릅니다. 평면 chrome 필드(borderRadius / padding / height / gap / iconSize / popoverPadding / popoverGap)는 모두 제거되었습니다.

시맨틱 vs 스타일#

  • 시맨틱 enum / behaviour: 위젯 파라미터로 직접 (mode, placeholder, enabled, onChanged, min, max, label, description, errorText, dialogTitle)
  • chrome / dimensional / 슬롯 스타일: CoreDatePickerStyle 한곳으로 (triggerStyle / popoverStyle / dialogStyle / calendarStyle / labelStyle / descriptionStyle / errorStyle / contentColor / descriptionColor / errorColor / labelBottomGapStyle)
  • 변형(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 슬롯 스타일 (triggerStyle / popoverStyle / dialogStyle / calendarStyle) 은 자기 컴포넌트의 자체 resolve chain 으로 다시 한 번 머지됩니다. 예: 트리거는 읽기 전용 TextField 라서 triggerStyle 의 chrome 은 → CoreTextFieldTheme → 크기 행(md) → 부모 슬롯 → widget.datePickerStyle.triggerStyle 순. 이전 버튼 트리거의 popoverStyle.triggerStyle (CoreButtonStyle) 은 더 이상 읽지 않습니다.

CoreDatePickerStyle 필드#

필드타입설명
triggerStyle CoreTextFieldStyle? Trigger field chrome — raw-forwarded to the composed read-only TextField , over [defaultTriggerStyle]. The trigger's value colour ([contentColor]), placeholder tone ([placeholderColor]), value role ([valueTypography]), calendar glyph ([trailingIconStyle]) and error edge ([errorTriggerStyle]'s border colour) are folded in under it. An explicit popoverStyle.triggerStyle is the chrome of the earlier button trigger and is not read.
popoverStyle CorePopoverStyle? Popover style for the popover-mode picker (trigger + dropdown chrome).
dialogStyle CoreDialogStyle? Dialog style for the modal-mode picker (panel + title + action button chrome).
errorTriggerStyle CoreButtonStyle? Deprecated (since 0.173) — Use triggerStyle: CoreTextFieldStyle(errorBorderColor: …). Trigger button chrome override while the picker holds an error. Merged on top of [defaultErrorTriggerStyle]; only its borderColor is read now — it becomes the trigger field's errorBorderColor . null takes the default alone.
calendarStyle CoreCalendarStyle? Calendar style applied to the inline / popover / dialog calendar grid. Drives nav buttons / date cells / arrow icons / state tokens.
labelStyle CoreTextStyle? Label text style (text shown above the trigger).
descriptionStyle CoreTextStyle? Description / helper text style (text shown below the trigger when no error).
errorStyleCoreTextStyle?Error text style.
valueTypography CoreTextStyle? Trigger value text style override (the role applied to both the selected-value text and the placeholder text — colour still comes from [contentColor] / [defaultPlaceholderColor]). null leaves the role to the trigger field's own row.
contentColor CoreColor? Trigger / label content text colour override. null defers to [defaultContentColor]. The Web label row and the trigger field's valueStyle both read this slot — for the trigger value while enabled (in error or not: an error changes the trigger's edge, see [defaultErrorTriggerStyle], not its label); a disabled value takes the field's own disabled foreground, which this style does not restate.
descriptionColor CoreColor? Description / helper text colour override. null defers to [defaultDescriptionColor].
errorColor CoreColor? Error text colour override. null defers to [defaultErrorColor].
disabledErrorColor CoreColor? Error text colour override while the picker is disabled. null defers to [defaultDisabledErrorColor].
disabledDescriptionColor CoreColor? Description text colour override while the picker is disabled. null defers to [defaultDisabledDescriptionColor]. [descriptionColor] colours the enabled picker only.
placeholderColor CoreColor? Trigger placeholder (no value selected) text colour override. null defers to [defaultPlaceholderColor].
trailingIconStyle CoreIconStyle? Trigger trailing (calendar) icon style override. null defers to [defaultTrailingIconStyle].
labelBottomGapStyle CoreGapStyle? Nested [CoreGapStyle] slot for the vertical gap between the label row and the trigger row — forwarded straight to the Gap widget that separates the rows. null defers to [defaultLabelBottomGapStyle].
descriptionTopGapStyle CoreGapStyle? Nested [CoreGapStyle] slot for the vertical gap between the trigger row and the description / error text row — forwarded straight to the Gap widget that separates the rows. null defers to [defaultDescriptionTopGapStyle].

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

옛 chrome 필드새 위치
heighttriggerStyle.height
padding triggerStyle.padding 또는 popoverStyle.panelPadding (의도에 따라)
borderRadiustriggerStyle.borderRadius
gap (트리거 내부)필드가 정합니다 (오른쪽 아이콘 자리)
iconSizetrailingIconStyle.size
popoverPaddingpopoverStyle.panelPadding
popoverGappopoverStyle.placementOffset

사용 예 (Flutter)#

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

변형 (Variants)#

Controlled (Flutter)#

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

날짜 범위 선택 (Flutter)#

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

비활성 날짜 지정 (Flutter)#

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

동작 스펙 (Behavior)#

  • 트리거는 입력창과 같은 모양입니다 — 읽기 전용 TextField 의 md 행(높이 40 · 모서리 10 · outlineVariant 테두리 · 값 Regular 14 · 오른쪽 달력 아이콘)이고, 폭은 부모가 준 폭을 채우며, 포커스 테두리는 primary 입니다. 오류일 때는 error 로 바뀝니다. 박스는 합성한 TextField 가 정하므로 DatePicker 는 값 색(contentColor) · 달력(trailingIconStyle) · 오류 테두리색만 말하고, triggerStyle 로 필드 모양을 덮을 수 있습니다. 값이 없을 때(value == null)는 placeholder 를, 있을 때는 날짜를 같은 트리거에 보입니다 — Figma 의 Show value · Value 는 이 두 모양을 고르는 캔버스 표현이고 코드 파라미터가 아닙니다(value 는 이미 DateTime?).
  • single의 popover는 선택·해제를 즉시 onChanged 로 알리고 짧은 강조 표시 후 닫습니다. range는 첫 시작일 클릭에는 커밋하지 않고 완료된 범위 또는 해제에만 콜백을 내며 닫습니다. 지연 단위는 CoreDatePickerStyle.defaultPopoverDismissDelayMs 의 ms입니다.
  • dialog는 pending 선택을 별도로 두고 Save에서만 커밋합니다. Cancel·외부 닫힘은 변경을 버립니다. Web의 Save는 현재 _pending ?? _value 를 사용해 명시적인 null 해제를 이전 값으로 되돌리는 결손이 있습니다.
  • firstDayOfWeek 는 popover · dialog 어느 표면이든 내부 Calendar 로 전달됩니다. 주 시작 요일은 지역의 속성이라 로케일에서 유도하지 않고 호출하는 쪽이 정합니다. 생략하면 일요일입니다.
  • 날짜 제한·stateBuilder는 내부 Calendar로 전달하며 stateBuilder가 min/max보다 우선합니다. 오류 메시지는 값을 검증하지 않고 표시를 정합니다. errorText 가 있으면 트리거는 테두리와 포커스 링만 error 색이 되고 글자색과 배경은 그대로입니다 — 텍스트 필드의 오류 모양과 같습니다. 오류 문구는 DatePicker 가 트리거 아래에 직접 그립니다(필드에는 문구가 아니라 오류 상태만 전달합니다). 오류 테두리색은 triggerStyle.errorBorderColor 로 바꿉니다(errorTriggerStyle 의 borderColor 도 아직 읽지만 폐기 예정입니다). enabled: false 는 트리거를 비활성화하고, 오류가 있어도 오류 표시 없이 일반 비활성 모양이 이깁니다.

표시 모드 (Flutter)#

// Popover 모드 (기본) — 트리거 아래에 캘린더 드롭다운
DatePicker(
  mode: CorePromptMode.popover,
  popoverPlacement: CorePopoverPlacement.bottomStart,
  value: selectedDate,
  onChanged: handleDateChanged,
)

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

DatePickerController (Flutter)#

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

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

범위 선택 (Flutter)#

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

Web 구현#

  • 트리거: 읽기 전용 TextField 에 달력 아이콘과 placeholder/선택 텍스트를 합성
  • Popover mode: Popover 사용 — OverlayHost.popover 레이어로 portal, ancestor overflow: hidden / transform 영향 없음
  • Dialog mode: Dialog 사용 — open boolean 으로 제어, Cancel/Save 액션으로 값 commit
  • 인터랙션: 트리거·닫힘은 합성된 Popover / Dialog가 처리합니다. non-modal popover의 Escape 경로는 Web에서 제공되며 Flutter는 같은 경로가 없습니다
  • Single 값은 ISO 8601 (YYYY-MM-DD) 문자열, range 값은 ({String start, String end}) 레코드
  • min/max는 ISO 문자열로 전달 (내부에서 DateTime으로 파싱)

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

Do#

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

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

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


Don't#

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

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

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

Do#

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

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

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


Don't#

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

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

접근성 (Accessibility)#

역할#

요소FlutterWeb
루트 없음 — date_picker.dart 전체에 Semantics 가 0건입니다 역할 없는 <div> . errorText 가 있으면 aria-invalid="true" , enabled: false 면 aria-disabled="true"
트리거 읽기 전용 TextField — 포커스할 수 있고 누를 수 있지만 글자를 칠 수는 없습니다. 팝오버 모드에서는 Popover 가 Semantics(expanded: …) 를 추가합니다 읽기 전용 <input> . 팝오버 모드에서는 Popover wrapper <div> 가 aria-haspopup="dialog" / aria-expanded 를 붙입니다
팝업 패널역할 없음Popover bubble 에 role="dialog"
다이얼로그 패널 showDialog 모달 라우트 Dialog 패널에 role="dialog" + aria-modal="true"
캘린더 Semantics(container: true, label: "Calendar") + 그리드에 role: .grid / .row / .gridCell / .columnHeader aria-label="Calendar" (역할 없음) + 그리드에 role="grid" / "row" / "gridcell" / "columnheader"
날짜 셀 Button / Clickable Button , 또는 직접 그린 셀은 role="button" + tabindex="0" (비활성은 aria-disabled="true" )
label / description / errorText 형제 Text — 트리거와 시맨틱으로 연결되지 않습니다 형제 <div> — <label for> 도 aria-describedby 도 없습니다

키보드#

키동작
Enter / Space트리거에서 캘린더 팝업/다이얼로그 열기, 날짜 셀에서 날짜 선택
Tab활성 날짜마다 정지점이 하나씩 생깁니다 (roving tabindex 미적용)
Escape Web 만 팝업을 닫습니다. Flutter 는 팝오버 모드에서 동작하지 않고, mode: CorePromptMode.dialog 일 때만 showDialog 라우트가 처리합니다
← → ↑ ↓ 캘린더가 이해하는 날짜 이동은 없습니다 . Flutter 에서는 프레임워크 기본 단축키( DirectionalFocusIntent )로 포커스가 방향 이동하지만 달 경계를 넘지 못하고, Web 에는 화살표 핸들러가 아예 없어 아무 일도 일어나지 않습니다
Page Up / Page Down없음 — 월 이동은 헤더의 이전/다음 버튼으로 합니다

스크린 리더#

  • 트리거에 현재 선택 날짜가 읽힙니다 (선택 전에는 placeholder).
  • 팝업 캘린더는 aria-label="Calendar" 를 가진, 역할 없는 컨테이너 — 그 안의 날짜 그리드가 role="grid" (Web) / role: .grid (Flutter) 로 읽힘. role="application" 은 없음 — 자세한 표는 Calendar 접근성 참조.
  • errorText 존재 시 Web 루트에만 aria-invalid="true" 가 붙습니다. Flutter 에는 대응하는 시맨틱이 없어, 오류 문구는 트리거와 무관한 별개의 텍스트로만 읽힙니다.
  • 비활성 상태는 양쪽 다 전달됩니다. Web 은 루트의 aria-disabled="true" 로, Flutter 는 트리거 TextField 가 붙이는 Semantics(enabled: false) 로 전달합니다.
  • label / description 은 트리거의 접근 가능한 이름·설명에 들어가지 않습니다(양 플랫폼). 둘 다 형제 텍스트 노드로만 그려집니다 — 리더는 읽기 순서상 옆에서 만나지만 트리거에 포커스했을 때는 함께 읽히지 않습니다.
  • 날짜 선택 결과를 알리는 라이브 리전은 없습니다.

포커스 관리#

  • 진입 — 팝오버 모드에서는 포커스가 트리거에 그대로 남습니다. 캘린더에 autofocus 가 없고 Popover 는 기본값(modal: false)으로 합성되기 때문입니다. mode: CorePromptMode.dialog 에서는 Flutter showDialog 라우트가 자기 포커스 스코프를 가져가고, Web 은 배경이 inert 가 되어 첫 Tab 이 패널 안으로 들어갑니다.
  • 이탈 — 팝오버가 닫힐 때 트리거로 포커스를 되돌리지 않습니다(양 플랫폼). 다이얼로그 모드의 Flutter showDialog 는 라우트가 pop 되며 이전 포커스를 복원합니다.
  • 트랩 — 팝오버 모드에는 없습니다. Web 패널은 문서 끝의 오버레이 레이어로 포탈되므로 트리거에서 Tab 을 눌러도 패널로 바로 가지 않고 문서 순서를 계속 걸어갑니다. 다이얼로그 모드에서만 트랩이 생깁니다 — Web 은 OverlayHost 가 앱 서브트리에 inert 를 걸어 Tab 이 패널 안에서만 돌고, Flutter 는 모달 라우트가 포커스를 가둡니다.

알려진 제약#

  • 캘린더 안에서 화살표 키나 PageUp/PageDown 으로 날짜를 이동할 수 없습니다. 날짜 셀이 처리하는 키는 선택(Space / Enter)뿐이고 방향키 처리는 양 플랫폼 어디에도 없습니다. 대신 활성 날짜마다 tab 정지점이 하나씩 생기므로(roving tabindex 미적용) 한 달을 지나가려면 Tab 을 서른 번 넘게 눌러야 합니다. 월 이동은 헤더의 이전/다음 버튼으로 하십시오.
  • 이전/다음 달 이동 버튼에 이름이 없습니다(양 플랫폼). 아이콘만 든 Button 이라 리더는 "버튼" 으로만 읽습니다. 이 버튼들은 Calendar 내부에서 생성되어 호출부가 라벨을 실을 진입점이 없습니다.
  • Flutter 에는 aria-invalid 에 대응하는 시맨틱이 없습니다. date_picker.dart 전체에 Semantics 가 없어 errorText 가 있는 상태가 Flutter 리더에 전달되지 않습니다(오류 문구 자체는 형제 Text 로 그려집니다). 비활성 상태만 트리거 TextField 를 통해 전달됩니다.
  • 팝업이 열려도 포커스가 캘린더로 이동하지 않고, 닫힐 때 트리거로 되돌아오지도 않습니다. Popover 를 기본값(modal: false)으로 합성하고 캘린더에 autofocus 가 없기 때문입니다. Web 은 패널이 문서 끝의 오버레이 레이어로 포탈되므로 트리거에서 Tab 을 눌러도 패널에 바로 닿지 않습니다. mode: CorePromptMode.dialog 로 두면 Flutter 는 showDialog 라우트가, Web 은 OverlayHost 의 inert 배경이 배경을 포커스 대상에서 제외합니다.

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

DatePicker는 CoreDatePickerContract를 통해 핵심 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)
범위 선택 DateRangePicker (Flutter) DateRangePicker (Web) — 동일 contract
컨트롤러DatePickerController (외부 상태)내부 상태 관리
캘린더 UICalendarCalendar (동일)
Popover 인프라 Popover + _popoverController Popover + _popoverController (동일)
Dialog 인프라 Dialog + showDialog Dialog (open boolean 제어)
값 소유value를 호출부에서 갱신하는 DatePicker내부 값과 외부 value 변경을 동기화
DOM 인프라 key · 호출부 시맨틱 래퍼 id / classes / css / attributes / 이벤트 표면
  • Calendar: 인라인 캘린더. DatePicker는 내부적으로 Calendar를 팝업으로 감쌉니다
  • TextField: 텍스트 입력. DatePicker는 트리거 + Calendar 조합
  • Form: 폼 컨테이너. DatePicker를 폼 필드로 사용 가능