TimePicker#
시간(시·분·초)을 선택할 수 있는 시간 선택기 컴포넌트입니다. 12시간/24시간 형식과 선택적인 초 표시를 지원합니다.
Live Preview#
기본 (popover)#
class TimePickerDefaultExample extends StatefulComponent {
const TimePickerDefaultExample({super.key});
@override
State<TimePickerDefaultExample> createState() =>
_TimePickerDefaultExampleState();
}
class _TimePickerDefaultExampleState extends State<TimePickerDefaultExample> {
String? _value;
@override
Component build(BuildContext context) {
return div(
[
TimePicker(
value: _value,
placeholder: 'Select time',
onChanged: (value) => setState(() => _value = value),
),
],
classes: 'w-${CoreSpace.scale.space256}',
);
}
}
class TimePickerDefaultExample extends StatefulWidget {
const TimePickerDefaultExample({super.key});
@override
State<TimePickerDefaultExample> createState() =>
_TimePickerDefaultExampleState();
}
class _TimePickerDefaultExampleState extends State<TimePickerDefaultExample> {
String? _value;
@override
Widget build(BuildContext context) {
return SizedBox(
width: CoreSpace.space256,
child: TimePicker(
value: _value,
placeholder: 'Select time',
onChanged: (value) => setState(() => _value = value),
),
);
}
}
class TimePickerChainExample extends StatefulComponent {
const TimePickerChainExample({super.key});
@override
State<TimePickerChainExample> createState() => _TimePickerChainExampleState();
}
class _TimePickerChainExampleState extends State<TimePickerChainExample> {
String? _value;
@override
Component build(BuildContext context) {
return div(
[
TimePicker(
value: _value,
placeholder: 'Select time',
onChanged: (value) => setState(() => _value = value),
).withStyle(
const CoreTimePickerStyle(
segmentBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
segmentBorderColor: CoreColor.token(CoreColors.primary),
contentColor: CoreColor.token(CoreColors.primary),
triggerHeight: CoreSpace.space48,
triggerBorderWidth: CoreStrokeWidth.stroke2,
triggerContentGapStyle: CoreGapStyle(size: CoreSpace.space12),
trailingIconStyle: CoreIconStyle(
size: CoreIconSize.size20,
color: CoreColor.token(CoreColors.primary),
),
),
),
],
classes: 'w-${CoreSpace.scale.space256}',
);
}
}
class TimePickerChainExample extends StatefulWidget {
const TimePickerChainExample({super.key});
@override
State<TimePickerChainExample> createState() => _TimePickerChainExampleState();
}
class _TimePickerChainExampleState extends State<TimePickerChainExample> {
String? _value;
@override
Widget build(BuildContext context) {
return SizedBox(
width: CoreSpace.space256,
child:
TimePicker(
value: _value,
placeholder: 'Select time',
onChanged: (value) => setState(() => _value = value),
).withStyle(
const CoreTimePickerStyle(
segmentBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
segmentBorderColor: CoreColor.token(CoreColors.primary),
contentColor: CoreColor.token(CoreColors.primary),
triggerHeight: CoreSpace.space48,
triggerBorderWidth: CoreStrokeWidth.stroke2,
triggerContentGapStyle: CoreGapStyle(size: CoreSpace.space12),
trailingIconStyle: CoreIconStyle(
size: CoreIconSize.size20,
color: CoreColor.token(CoreColors.primary),
),
),
),
);
}
}
다이얼로그 모드#
class TimePickerDialogExample extends StatefulComponent {
const TimePickerDialogExample({super.key});
@override
State<TimePickerDialogExample> createState() =>
_TimePickerDialogExampleState();
}
class _TimePickerDialogExampleState extends State<TimePickerDialogExample> {
String? _value;
@override
Component build(BuildContext context) {
return div(
[
TimePicker(
value: _value,
placeholder: 'Select time',
mode: CorePromptMode.dialog,
dialogTitle: Text('Pick a time'),
onChanged: (value) => setState(() => _value = value),
),
],
classes: 'w-${CoreSpace.scale.space256}',
);
}
}
class TimePickerDialogExample extends StatefulWidget {
const TimePickerDialogExample({super.key});
@override
State<TimePickerDialogExample> createState() =>
_TimePickerDialogExampleState();
}
class _TimePickerDialogExampleState extends State<TimePickerDialogExample> {
String? _value;
@override
Widget build(BuildContext context) {
return SizedBox(
width: CoreSpace.space256,
child: TimePicker(
value: _value,
placeholder: 'Select time',
mode: CorePromptMode.dialog,
dialogTitle: const Text('Pick a time'),
onChanged: (value) => setState(() => _value = value),
),
);
}
}
초 단위 (HH:MM:SS)#
class TimePickerSecondsExample extends StatefulComponent {
const TimePickerSecondsExample({super.key});
@override
State<TimePickerSecondsExample> createState() =>
_TimePickerSecondsExampleState();
}
class _TimePickerSecondsExampleState extends State<TimePickerSecondsExample> {
String? _value;
@override
Component build(BuildContext context) {
return div(
[
TimePicker(
value: _value,
placeholder: 'Select time',
showSeconds: true,
onChanged: (value) => setState(() => _value = value),
),
],
classes: 'w-${CoreSpace.scale.space256}',
);
}
}
class TimePickerSecondsExample extends StatefulWidget {
const TimePickerSecondsExample({super.key});
@override
State<TimePickerSecondsExample> createState() =>
_TimePickerSecondsExampleState();
}
class _TimePickerSecondsExampleState extends State<TimePickerSecondsExample> {
String? _value;
@override
Widget build(BuildContext context) {
return SizedBox(
width: CoreSpace.space256,
child: TimePicker(
value: _value,
placeholder: 'Select time',
showSeconds: true,
onChanged: (value) => setState(() => _value = value),
),
);
}
}
에러 상태#
class TimePickerErrorExample extends StatefulComponent {
const TimePickerErrorExample({super.key});
@override
State<TimePickerErrorExample> createState() =>
_TimePickerErrorExampleState();
}
class _TimePickerErrorExampleState extends State<TimePickerErrorExample> {
String? _value;
@override
Component build(BuildContext context) {
return div(
[
TimePicker(
value: _value,
placeholder: 'Select time',
errorText: 'Time is required',
onChanged: (value) => setState(() => _value = value),
),
],
classes: 'w-${CoreSpace.scale.space256}',
);
}
}
class TimePickerErrorExample extends StatefulWidget {
const TimePickerErrorExample({super.key});
@override
State<TimePickerErrorExample> createState() =>
_TimePickerErrorExampleState();
}
class _TimePickerErrorExampleState extends State<TimePickerErrorExample> {
String? _value;
@override
Widget build(BuildContext context) {
return SizedBox(
width: CoreSpace.space256,
child: TimePicker(
value: _value,
placeholder: 'Select time',
errorText: 'Time is required',
onChanged: (value) => setState(() => _value = value),
),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 예약, 일정, 알림 등 시간을 입력받아야 하는 경우
- 12시간(AM/PM) 또는 24시간 형식의 시간 선택이 필요한 경우
- 영업 시간, 배송 시간처럼 선택 가능한 시간 범위를 제한해야 하는 경우
대신 다른 컴포넌트를 사용하세요:
DatePicker: 날짜와 시간을 함께 선택해야 하는 경우TextField: 시간을 자유 형식 텍스트로 직접 입력받는 경우Select: 고정된 시간 슬롯 목록에서 선택해야 하는 경우
기본 사용법 (Basic Usage)#
// 기본 시간 선택기
TimePicker(
value: selectedTime,
placeholder: 'Select time',
onChanged: handleTimeChanged,
)
// 24시간 형식 강제
TimePicker(
value: meetingTime,
use24HourFormat: true,
onChanged: handleMeetingTimeChanged,
)
// 초 표시 포함
TimePicker(
value: preciseTime,
showSeconds: true,
onChanged: handlePreciseTimeChanged,
)
// 선택 가능 범위 제한 (ISO time string)
TimePicker(
value: businessHour,
min: '09:00',
max: '18:00',
onChanged: handleBusinessHourChanged,
)
// 기본 시간 선택기
TimePicker(
value: selectedTime,
placeholder: 'Select time',
onChanged: handleTimeChanged,
)
// 24시간 형식 강제
TimePicker(
value: meetingTime,
use24HourFormat: true,
onChanged: handleMeetingTimeChanged,
)
// 초 표시 포함
TimePicker(
value: preciseTime,
showSeconds: true,
onChanged: handlePreciseTimeChanged,
)
// 선택 가능 범위 제한
TimePicker(
value: businessHour,
min: '09:00',
max: '18:00',
onChanged: handleBusinessHourChanged,
)
빠른 오버라이드 (Chain)#
이미 만든 TimePicker 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class TimePickerChainExample extends StatefulWidget {
const TimePickerChainExample({super.key});
@override
State<TimePickerChainExample> createState() => _TimePickerChainExampleState();
}
class _TimePickerChainExampleState extends State<TimePickerChainExample> {
String? _value;
@override
Widget build(BuildContext context) {
return SizedBox(
width: CoreSpace.space256,
child:
TimePicker(
value: _value,
placeholder: 'Select time',
onChanged: (value) => setState(() => _value = value),
).withStyle(
const CoreTimePickerStyle(
segmentBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
segmentBorderColor: CoreColor.token(CoreColors.primary),
contentColor: CoreColor.token(CoreColors.primary),
triggerHeight: CoreSpace.space48,
triggerBorderWidth: CoreStrokeWidth.stroke2,
triggerContentGapStyle: CoreGapStyle(size: CoreSpace.space12),
trailingIconStyle: CoreIconStyle(
size: CoreIconSize.size20,
color: CoreColor.token(CoreColors.primary),
),
),
),
);
}
}
class TimePickerChainExample extends StatefulComponent {
const TimePickerChainExample({super.key});
@override
State<TimePickerChainExample> createState() => _TimePickerChainExampleState();
}
class _TimePickerChainExampleState extends State<TimePickerChainExample> {
String? _value;
@override
Component build(BuildContext context) {
return div(
[
TimePicker(
value: _value,
placeholder: 'Select time',
onChanged: (value) => setState(() => _value = value),
).withStyle(
const CoreTimePickerStyle(
segmentBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
segmentBorderColor: CoreColor.token(CoreColors.primary),
contentColor: CoreColor.token(CoreColors.primary),
triggerHeight: CoreSpace.space48,
triggerBorderWidth: CoreStrokeWidth.stroke2,
triggerContentGapStyle: CoreGapStyle(size: CoreSpace.space12),
trailingIconStyle: CoreIconStyle(
size: CoreIconSize.size20,
color: CoreColor.token(CoreColors.primary),
),
),
),
],
classes: 'w-${CoreSpace.scale.space256}',
);
}
}
Props / Parameters#
공통 Contract 필드 (CoreTimePickerContract):
| 속성 | Flutter | Web | 기본값 | 설명 |
|---|---|---|---|---|
value |
String? |
String? |
null |
현재 선택된 시간 (ISO 형식: HH:MM 또는 HH:MM:SS) |
placeholder |
String? |
String? |
null (미지정 시 세그먼트 글리프로 조립) |
미선택 시 표시될 텍스트 |
placeholderSegment |
String |
String |
'--' |
미선택 세그먼트(시/분/초) 하나를 나타내는 글리프 |
placeholderSeparator |
String |
String |
':' |
인접한 placeholder 세그먼트 사이 구분자 |
enabled |
bool |
bool |
true |
활성화 여부 |
showSeconds |
bool |
bool |
false |
초 선택 포함 여부 |
use24HourFormat |
bool? |
bool? |
null (플랫폼 기본값) |
24시간 형식 사용 여부 |
min |
String? |
String? |
null |
최소 선택 가능 시간 (ISO 문자열) |
max |
String? |
String? |
null |
최대 선택 가능 시간 (ISO 문자열) |
onChanged |
ValueChanged<String>? |
CoreValueChanged<String>? |
null |
시간 변경 콜백 (ISO 문자열) |
mode |
CorePromptMode? |
CorePromptMode? |
CorePromptMode.popover |
트리거 클릭 시 popover/dialog 중 어떤 surface로 열릴지 |
popoverPlacement |
CorePopoverPlacement |
CorePopoverPlacement |
CorePopoverPlacement.bottomStart |
popover 모드에서 패널이 트리거 기준 어디에 열릴지 |
dialogTitle |
Widget? |
Component? |
null |
mode = dialog일 때 dialog 헤더에 표시되는 타이틀 |
errorText |
String? |
String? |
null |
에러 메시지(있으면 보더가 error 색으로 표시됨, TextField와 패리티) |
timePickerStyle |
CoreTimePickerStyle? |
CoreTimePickerStyle? |
null |
인스턴스 스타일 (Style 시스템 참조) |
스타일 시스템 (Style System)#
TimePicker의 모든 chrome / 치수 / 중첩 슬롯 오버라이드는 CoreTimePickerStyle 한곳으로 흐릅니다.
시맨틱 vs 스타일#
-
시맨틱 enum / 동작: 위젯 파라미터로 직접 (
mode,popoverPlacement,placeholder,placeholderSegment,placeholderSeparator,enabled,showSeconds,use24HourFormat,onChanged,min,max,errorText,dialogTitle) - chrome / 치수 / 슬롯 스타일:
CoreTimePickerStyle한곳 (아래 필드 표) - 변형 교체: asChild —
dialogTitle슬롯 등에 위젯을 직접 넣습니다
Resolve chain#
design system default for time picker
→ CoreTimePickerTheme.style // 프로젝트 공통
→ parent component slot override
→ widget.timePickerStyle // 인스턴스별
각 nested 슬롯 스타일 (popoverStyle / dialogStyle / segmentInputStyle
/ amPmButtonStyle / amPmSelectedButtonStyle / actionButtonStyle) 은 자기 컴포넌트의 자체 resolve chain 으로 다시 한 번 머지됩니다. 예:
popoverStyle.triggerStyle 의 chrome 은 → CoreButtonTheme → variant 룩업 → 부모 슬롯 →
widget.timePickerStyle.popoverStyle.triggerStyle 순. 마찬가지로 popover 패널 박스는 popoverStyle.panelStyle
(CorePopupStyle) 을 통해 Popup 의 resolver 로 흐릅니다.
CoreTimePickerStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
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).
No default, and must not have one.
Both platforms forward this slot to
Dialog(dialogStyle: …)
unchanged, so null is what lets
Dialog
resolve its own panel — the single source for that surface. A constant here would be a second copy of
Dialog
's panel defaults, and because a slot override outranks the child's own baseline it would also win over them for every picker dialog. The asymmetry with [defaultPopoverStyle] is the point: popover mode
diverges
from
Popup
(radius16 / outlineVariant / padding16) and that divergence is what the default states. Dialog mode diverges in nothing, so it states nothing.
|
segmentInputStyle |
CoreTextFieldStyle? |
Style applied to each hour / minute / second segment input (TextField inside the picker panel).
No default, and must not have one.
This slot is the top of two precedence chains that already end at named Core defaults:
text …?.borderColor ?? segmentBorderColor ?? defaultSegmentBorderColor …?.valueStyle ?? valueTypography ?? defaultValueTypography
The baseline exists — under those two names. Giving this slot a non-null default would insert it
above
the flat overrides in both chains, so
segmentBorderColor
and
valueTypography
would stop being reachable: a caller setting either would see no change. That is a dead parameter, not a repaint.
|
amPmButtonStyle |
CoreButtonStyle? |
Style applied to the am/pm toggle button when
use24HourFormat: false
.
No default, and must not have one.
Raw-forwarded to
Button(buttonStyle: …)
on both platforms, where null means the button paints its variant. A constant would outrank
CoreButtonStyle.defaultsByVariant
inside
Button
's resolver and repaint the toggle. [defaultAmPmSelectedButtonStyle] exists for the sibling slot because the selected button genuinely diverges — it needs a transparent border so selection does not resize the toggle. This slot has no such divergence to state.
|
amPmSelectedButtonStyle |
CoreButtonStyle? |
Style applied to the
selected
am/pm toggle button, layered on top of [amPmButtonStyle]. The default carries a transparent border matching the unselected outline button's stroke, so the selected (borderless
primary
) button occupies the same layout footprint and the toggle never resizes when selection moves.
|
actionButtonStyle |
CoreButtonStyle? |
Style merged into the Cancel / Save action buttons of the popover / dialog panel — single shared slot, mirroring
CoreDialogStyle.actionButtonStyle
.
No default, and must not have one
, for the same reason as
CoreDialogStyle.actionButtonStyle
: one slot feeds both buttons, Cancel is
.ghost
and Save is the filled default, and a value named here outranks
CoreButtonStyle.defaultsByVariant
in
Button
's resolver. A single constant would therefore paint the two buttons alike and erase the distinction they exist to draw.
|
labelStyle |
CoreTextStyle? |
Label text style (text shown above the trigger).
Read by no resolver and no widget — wire it or remove it.
Left untouched (deleting a public field is breaking and not mine to decide). Nobody reads it on either platform: both resolvers only author it into
ResolvedTimePicker
(
labelStyle: merged.labelStyle
) and neither widget ever reads
resolved.labelStyle
—
grep -rn 'resolved.labelStyle' packages/coui_{web,flutter}/lib
over time_picker returns nothing. The root cause is that
TimePicker
has no
label
parameter on either platform, so there is no label element to style; the segment captions inside the panel are styled by
helperTypography
+
labelColor
instead. Either the component gains a
label
slot and this styles it, or the field is dropped at the next breaking release — a decision about the component's API, not about a default.
|
descriptionStyle |
CoreTextStyle? |
Description / helper text style.
Read by no resolver and no widget — wire it or remove it.
Left untouched. Same as
labelStyle
: authored into both
ResolvedTimePicker
s, read by neither widget, and
TimePicker
has no
description
parameter on either platform, so no description text is rendered at all. Wire it (add the slot) or drop it in a breaking release.
|
errorStyle |
CoreTextStyle? |
Error text style.
Read by no resolver and no widget — wire it or remove it.
Left untouched. Authored into both
ResolvedTimePicker
s and read by neither widget.
TimePicker
does take
errorText
, but it only drives the trigger's error border colour (
hasError
at time_picker.dart:320-321 Flutter / 622-623 Web) — no error
text
is ever rendered, so there is nothing for a text style to apply to. Either the component renders the message (then this styles it) or the field goes in a breaking release.
|
segmentBorderColor |
CoreColor? |
Custom segment input border colour. Falls back to [defaultSegmentBorderColor] when null. |
segmentSurfaceColor |
CoreColor? |
Custom segment input fill / surface colour. Falls back to [defaultSegmentSurfaceColor] when null. |
segmentContentColor |
CoreColor? |
Custom segment input content text colour. Falls back to [defaultSegmentContentColor] when null. |
segmentFocusOutlineStyle |
CoreFocusOutlineStyle? |
Segment focus ring override.
null
defers to [defaultSegmentFocusOutlineStyle], and a field left null inside it defers to
CoreFocusOutlineStyle
's own default. Geometry is stated as
borderWidth
plus
align
(the ring's total outset), not as a width/offset pair — the gap between the segment and the ring is
align - borderWidth
. Both platforms draw the ring the same way — a composed
FocusOutline
wrapping the segment box — so the slot reaches the same seven fields on each:
borderColor
,
borderWidth
,
align
,
offsetColor
,
ringOpacity
,
borderRadius
,
duration
.
circleBorderRadius
is the eighth and reaches neither. It is read only when the ring's shape is a circle, and a time segment is a rectangle — the two radius fields are mutually exclusive by shape, so no single instance can reach both. A rectangle taking a circle's radius would be the bug; this is the correct outcome, not a gap to close.
|
labelColor |
CoreColor? |
Custom segment label / description text colour. Falls back to [defaultLabelColor] when null. |
contentColor |
CoreColor? |
Custom trigger / separator content text colour. Falls back to [defaultContentColor] when null. |
valueTypography |
CoreTextStyle? |
Custom trigger value / segment numeric typography role. Falls back to [defaultValueTypography] when null. |
helperTypography |
CoreTextStyle? |
Custom helper / segment-label typography role. Falls back to [defaultHelperTypography] when null. |
segmentBorderRadius |
CoreBorderRadius? |
Custom segment input border radius (pre-scaling). |
triggerHeight |
double? |
Custom trigger height (pre-scaling). |
triggerBorderWidth |
double? |
Custom trigger / segment border width (pre-scaling). |
triggerContentGapStyle |
CoreGapStyle? |
Nested [CoreGapStyle] slot for the gap between trigger text and trailing icon. Forwarded as the trigger
Button
's nested
iconLabelGapStyle
. Merged on top of [defaultTriggerContentGapStyle].
|
segmentSize |
double? |
Custom width / height of each numeric segment field (pre-scaling). |
dialogLabelSpacing |
double? |
Uniform spacing between the segment field and its label (Flutter
Column.spacing
) and between the AM/PM stacked toggle buttons (Flutter
Column.spacing
/ Web inline
gap
). Falls back to [defaultDialogLabelSpacing] when null.
|
dialogOuterGapStyle |
CoreGapStyle? |
Nested [CoreGapStyle] slot for the horizontal gap between the segment row and the AM/PM column. Forwarded to a
Gap
widget on Flutter and drives an inline spacer width on Web. Merged on top of [defaultDialogOuterGapStyle].
|
actionRowGapStyle |
CoreGapStyle? |
Nested [CoreGapStyle] slot for the vertical gap between the segment row and the action button row (also used between the optional title and the segment row). Forwarded to a
Gap
widget on Flutter and drives an inline
margin-top
on Web. Merged on top of [defaultActionRowGapStyle].
|
separatorPadding |
CoreEdgeInsets? |
Custom padding around the segment separator glyph (pre-scaling). |
trailingIconStyle |
CoreIconStyle? |
Trigger trailing (clock) icon style override.
null
→ the trigger
popoverStyle.triggerStyle.trailingIconStyle
override, else [defaultTrailingIconStyle].
|
사용 예 (Flutter)#
TimePicker(
value: '14:30',
placeholder: 'Select time',
onChanged: handleTimeChanged,
timePickerStyle: CoreTimePickerStyle(
popoverStyle: CorePopoverStyle(
panelStyle: CorePopupStyle(
borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
padding: CoreEdgeInsets.all(CoreSpace.space16),
),
placementOffset: CoreSpace.space8,
triggerStyle: CoreButtonStyle(
height: CoreSize.size44,
padding: CoreEdgeInsets.symmetric(horizontal: CoreSpace.space16),
),
),
segmentInputStyle: CoreTextFieldStyle(
borderColor: CoreColor.token(CoreColors.primary),
),
amPmButtonStyle: CoreButtonStyle(height: CoreSize.size36),
labelStyle: CoreTextStyle(fontWeight: CoreFontWeight.semiBold),
),
)
동작 스펙 (Behavior)#
인터랙션#
-
트리거 클릭:
mode가popover(기본)면Popover패널이popoverPlacement위치로,dialog면Dialog가 중앙 모달로 열립니다 (양쪽 동일) - 직접 입력: 시/분/초 필드에 숫자를 직접 입력 가능 (양쪽 동일)
- AM/PM 토글: 12시간 형식에서 AM/PM 전환 —
Button기반 (양쪽 동일) - 저장/취소: 패널 하단의
Button(Save/Cancel)
값 형식#
- 값은 ISO 시간 문자열로 주고받습니다:
"14:30"또는"14:30:45"(showSeconds 시) - 12시간 포맷에서도 값은 24시간 ISO로 저장되며, 디스플레이만
hh:mm AM/PM로 포맷팅됩니다
Theme 오버라이드#
CoreComponentTheme.timePicker를 통해 프로젝트 레벨의 기본 스타일(CoreTimePickerStyle)을 지정할 수 있습니다:
CoreComponentTheme(
timePicker: CoreTimePickerTheme(
style: CoreTimePickerStyle(
popoverStyle: CorePopoverStyle(
panelStyle: CorePopupStyle(
borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
padding: CoreEdgeInsets.all(CoreSpace.space16),
),
triggerStyle: CoreButtonStyle(
height: CoreSize.size40,
padding: CoreEdgeInsets.symmetric(horizontal: CoreSpace.space12),
),
),
),
),
)
사용 가이드라인 (Usage Guidelines)#
✅ Do#
min / max 로 선택 가능한 범위를 원천 제한
TimePicker(
value: businessHour,
min: '09:00',
max: '18:00',
onChanged: handleBusinessHourChanged,
)
이유: min/max는 세그먼트 입력 단계에서부터 범위를 제한하므로, 사용자가 영업시간 밖 값을 아예 선택할 수 없습니다 — errorText로 사후에 경고 문구를 보여줄 필요가 없습니다.
❌ Don't#
12시간 포맷 문자열을 value/onChanged에 그대로 넘기지 않기
// ❌ "HH:MM" ISO 형식이 아닌 12시간 문자열
TimePicker(
value: '02:30 PM',
onChanged: (v) => save(v),
)
이유: use24HourFormat이 false여도 value/onChanged는 항상 24시간 ISO 문자열("HH:MM" 또는 "HH:MM:SS")을 주고받습니다. 12시간 포맷은 디스플레이 단계에서만 변환되므로, 12시간 문자열을 직접 넘기면 파싱에 실패하거나 의도하지 않은 시간으로 해석됩니다.
접근성 (Accessibility)#
- 트리거에
role="button"과aria-haspopup="dialog"가 붙습니다. - 패널은
role="dialog"로 열립니다. - 키보드로 시/분/초를 직접 입력할 수 있습니다 (양쪽 동일).
- 최소 터치 타겟(WCAG 2.2 AA)은 24×24 논리 픽셀입니다. 트리거는 필드 기본 높이(40px)라 이미 그보다 큽니다.
크로스 플랫폼 차이점 (Platform Differences)#
양쪽 모두 Popover + Button + Icon을 사용해 동일한 UX를 제공합니다. 값 형식, 토큰, 동작 모두 1:1 통일되어 있습니다.
관련 컴포넌트 (Related Components)#
- DatePicker: 날짜와 시간을 함께 선택해야 하는 경우
- TextField: 시간을 텍스트로 직접 입력받는 경우
- Select: 고정된 시간 슬롯 목록을 드롭다운으로 제공하는 경우