PlDateTimePicker
한 팝업 안의 날짜와 시각입니다. 시계가 자란 date picker도, 달력이 자란 time picker도 아닙니다. 두 패널의 높이가 같은 것은 의도된 것입니다.
import { PlDateTimePicker } from 'plass-ui';
<PlDateTimePicker label="Starts" placeholder="Pick a moment" minDate={new Date()} />;import 'package:plass_ui/plass_ui.dart';
PlDateTimePicker(
label: const Text('Starts'),
minDate: DateTime.now(),
value: starts,
onChanged: (DateTime? next) => setState(() => starts = next),
);패널들은 트리 밖으로 자기를 들어 올리므로 picker 위에 Overlay가 필요합니다.
Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| value | Date | null | — | 선택된 순간. onValueChange와 함께 controlled로 씁니다 |
| defaultValue | Date | null | — | uncontrolled일 때 시작하는 순간 |
| onValueChange | (value: Date | null) => void | — | 값이 바뀔 때 호출됩니다 |
| minDate | Date | null | — | 고를 수 있는 가장 이른 순간. PlDatePicker와 달리 **전체 정밀도로** 읽습니다 — 그 경계가 놓인 날은 달력에서 그대로 고를 수 있고, 그 앞의 시각을 막는 건 시계 열입니다 |
| maxDate | Date | null | — | 고를 수 있는 가장 늦은 순간. 역시 전체 정밀도입니다 |
| shouldDisableDate | (date: Date) => boolean | — | 범위 안이지만 그래도 쓸 수 없는 날을 막습니다 |
| weekStartsOn | 0 | 1 | 2 | 3 | 4 | 5 | 6 | — | 한 주가 시작하는 요일. 기본은 locale이 말하는 대로이고, 0이 일요일입니다 |
| defaultMonth | Date | — | 값이 없을 때 달력이 열리는 달 |
| hour12 | boolean | — | AM/PM 열이 붙은 12시간 다이얼. 기본은 locale이 하는 대로입니다 |
| showSeconds | boolean | false | 초 열을 더합니다 |
| hourStep | number | 1 | 각 열의 행 간격 |
| minuteStep | number | 1 | hourStep를 보세요 |
| secondStep | number | 1 | hourStep를 보세요 |
| shouldDisableTime | (value: Date, unit: TimeUnit) => boolean | — | 개별 행을 막습니다. 열마다 행마다, 그 행이 만들어 낼 시각과 그 행이 속한 열을 받아 한 번씩 호출됩니다 — "오후는 안 됨"만큼 성길 수도, 1분만큼 촘촘할 수도 있습니다 |
| showNowButton | boolean | true | 푸터에 이 순간으로 가는 지름길을 둡니다 |
| variant공통 | 'solid' | 'glass' | 'ghost' | 'glass' | trigger의 재질. PlTextField와 같은 껍데기를 씁니다. solid는 시트에 파인 우물 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 높이와 타입 스케일 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | 'default' | 'compact' | 'default' | 여백만 바꿉니다. 높이와 글자 크기는 그대로 |
| elevation공통 | 0 | 1 | 2 | 3 | 0 | trigger의 그림자 깊이. 팝업은 3으로 고정입니다 — 팝업은 정말로 페이지 위에 떠 있습니다 |
| open | boolean | — | 팝업이 열려 있는지. onOpenChange와 함께 controlled로 씁니다 |
| defaultOpen | boolean | false | 팝업이 열린 채로 시작할지 |
| onOpenChange | (open: boolean) => void | — | 팝업이 열리고 닫힐 때 호출됩니다 |
| locale | string | — | BCP 47 태그. 월과 요일 이름, 헤더 두 버튼의 순서, trigger가 날짜를 쓰는 방식을 정합니다. 기본은 브라우저의 것 |
| format | Intl.DateTimeFormatOptions | { dateStyle: 'medium', timeStyle: 'short' } | trigger가 순간을 쓰는 방식. Intl로 그대로 넘어갑니다 |
| placeholder | ReactNode | — | 아무것도 고르지 않았을 때 trigger에 보이는 내용 |
| clearable | boolean | false | 값을 비우는 ×를 보여 줍니다 |
| closeOnSelect | boolean | false | 날을 고르는 즉시 팝업을 닫습니다. 여기서는 false, PlDatePicker에서는 true인 건 순간이 날 *그리고* 시각이고 둘 중 첫째에서 닫으면 둘째가 답 없이 남기 때문입니다 |
| labels | Partial<PlPickerLabels> | — | picker가 스스로 말하는 문자열들. 전부 영어 기본값이 있습니다. 날짜 이름은 여기 없습니다 — 그건 Intl이 압니다 |
| label | ReactNode | — | trigger 위 라벨 |
| description | ReactNode | — | trigger 아래 보조 설명 |
| error | ReactNode | — | 오류 메시지. 존재 자체가 invalid 상태를 만듭니다 |
| invalid | boolean | — | 메시지 없이 invalid로 만듭니다 |
| startIcon | ReactNode | — | 값 앞의 글리프. 기본은 달력(또는 시계)입니다 |
| fullWidth | boolean | false | 컨테이너 너비만큼 확장 |
| readOnly | boolean | false | 값은 보이지만 바꿀 수 없고, 팝업도 열리지 않습니다 |
| disabled | boolean | false | 사용 불가 |
| required | boolean | false | 폼 제출 전에 값이 있어야 하는지 |
| name | string | — | 폼 제출 시 필드를 식별합니다. 로컬 YYYY-MM-DDTHH:MM으로 보냅니다 |
| classNames | { label?, control?, description?, error?: string } | — | className이 닿지 않는 부분에 붙는 class. control은 실제로 조작하는 부분입니다 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| value * | DateTime? | — | 선택된 순간, 또는 없으면 null |
| onChanged | ValueChanged<DateTime?>? | — | 고른 순간과 함께 호출됩니다. 비우면 null입니다 |
| open | bool? | — | 팝업이 열려 있는지. onOpenChange와 함께 controlled로 씁니다 |
| onOpenChanged | ValueChanged<bool>? | — | 패널들이 열리거나 닫혀야 할 때 호출됩니다 |
| defaultMonth | DateTime? | — | 값이 없을 때 달력이 열리는 달 |
| minDate | DateTime? | — | 고를 수 있는 가장 이른 순간. PlDatePicker와 달리 **전체 정밀도로** 읽습니다 — 그 경계가 놓인 날은 달력에서 그대로 고를 수 있고, 그 앞의 시각을 막는 건 시계 열입니다 |
| maxDate | DateTime? | — | 고를 수 있는 가장 늦은 순간. 역시 전체 정밀도입니다 |
| shouldDisableDate | bool Function(DateTime date)? | — | 범위 안이지만 그래도 쓸 수 없는 날을 막습니다 |
| weekStartsOn | PlassWeekday? | — | 한 주가 시작하는 요일. 기본은 locale이 말하는 대로이고, 0이 일요일입니다 |
| hour12 | bool | false | AM/PM 열이 붙은 12시간 다이얼. React가 locale에서 가져오는 자리에서 여기서는 그냥 false입니다 — 물어볼 Intl이 없고, 켰을 때 쓰는 말은 PlDateNames의 am/pm입니다 |
| showSeconds | bool | false | 초 열을 더합니다 |
| hourStep | int | 1 | 각 열의 행 간격 |
| minuteStep | int | 1 | hourStep를 보세요 |
| secondStep | int | 1 | hourStep를 보세요 |
| shouldDisableTime | bool Function(DateTime value, PlassTimeUnit unit)? | — | 개별 행을 막습니다. 열마다 행마다, 그 행이 만들어 낼 시각과 그 행이 속한 열을 받아 한 번씩 호출됩니다 — "오후는 안 됨"만큼 성길 수도, 1분만큼 촘촘할 수도 있습니다 |
| names | PlDateNames | PlDateNames.english | 달력이 그리는 월과 요일 이름, 그리고 헤더가 그것들을 쓰는 순서. **React의 locale 문자열에 해당합니다** — 프레임워크에 Intl이 없으므로 단어를 객체로 받습니다 |
| labels | PlPickerLabels | PlPickerLabels.english | picker가 스스로 말하는 문자열들. 전부 영어 기본값이 있습니다. 날짜 이름은 여기 없습니다 — 그건 Intl이 압니다 |
| formatValue | String Function(DateTime value)? | — | trigger가 값을 쓰는 방식. React의 Intl 옵션 대신 콜백입니다. 빼면 names의 medium 형식으로 씁니다 |
| placeholder | Widget? | — | 아무것도 고르지 않았을 때 trigger에 보이는 내용 |
| clearable | bool | false | 값을 비우는 ×를 보여 줍니다 |
| showNowButton | bool | true | 푸터에 이 순간으로 가는 지름길을 둡니다 |
| closeOnSelect | bool | false | 날을 고르는 즉시 팝업을 닫습니다. 여기서는 false, PlDatePicker에서는 true인 건 순간이 날 *그리고* 시각이고 둘 중 첫째에서 닫으면 둘째가 답 없이 남기 때문입니다 |
| variant공통 | PlassVariant | PlassVariant.glass | trigger의 재질. PlTextField와 같은 껍데기를 씁니다. solid는 시트에 파인 우물 |
| size공통 | PlassSize | PlassSize.md | 높이와 타입 스케일 |
| color공통 | PlassColor | PlassColor.primary | 의미론적 색 역할. 임의 색상값은 받지 않습니다 |
| density공통 | PlassDensity | PlassDensity.standard | 여백만 바꿉니다. 높이와 글자 크기는 그대로 |
| elevation공통 | int | 0 | trigger의 그림자 깊이. 팝업은 3으로 고정입니다 — 팝업은 정말로 페이지 위에 떠 있습니다 |
| label | Widget? | — | trigger 위 라벨 |
| description | Widget? | — | trigger 아래 보조 설명 |
| error | Widget? | — | 오류 메시지. 존재 자체가 invalid 상태를 만듭니다 |
| invalid | bool? | — | 메시지 없이 invalid로 만듭니다 |
| startIcon | Widget? | — | 값 앞의 글리프. 기본은 달력(또는 시계)입니다 |
| fullWidth | bool | false | 컨테이너 너비만큼 확장 |
| readOnly | bool | false | 값은 보이지만 바꿀 수 없고, 팝업도 열리지 않습니다 |
| disabled | bool | false | 사용 불가 |
| semanticLabel | String? | — | 보이는 label이 없는 trigger를 스크린 리더가 부를 이름 |
| focusNode | FocusNode? | — | 포커스를 밖에서 제어할 때 넘깁니다 |
| autofocus | bool | false | 트리에 들어가면서 포커스를 가져갑니다 |
나머지 <div> 속성은 field 래퍼로 그대로 통과합니다. color는 위 표의 color와 겹쳐서, defaultValue는 DOM 속성이 아니라 값으로 쓰기 때문에, children은 패널들이 곧 컴포넌트이기 때문에 제외했습니다.
className은 label과 control, 그 아래 두 줄을 함께 담는 stack에 붙습니다. 그 안쪽 네 부분에 닿는 것이 classNames입니다: label, control(트리거), description, error.
picker는 controlled입니다. value와 onChanged를 함께 주고, null은 아무것도 고르지 않은 picker입니다.
달력은 PlDatePicker의 것이고 열들은 PlTimePicker의 것이며, 둘 다 그대로입니다. 그 두 페이지가 단어와 헤더와 열과 날짜 라이브러리의 부재에 대해 말한 것이 여기서도 성립합니다.
하나의 팝업
달력의 그리드는 헤더까지 세어 일곱 줄입니다. 시계의 열들은 같은 칸 일곱 개입니다. 정확히 그 이유로 둘은 같은 칸 사다리를 읽고, 그래서 팝업은 크기가 다른 두 덩어리를 붙여 놓은 것이 아니라 사각형 하나입니다. 달력을 월 뷰나 연 뷰로 바꿔도 그대로입니다.
경계
minDate와 maxDate를 전체 정밀도로 읽습니다. PlDatePicker와 갈라지는 유일한 지점입니다. 거기서 경계는 어떤 날이 존재하는가에 대한 것이고 붙은 시각은 무시됩니다. 여기서는 27일 09:30이라는 최솟값이 달력에서 27일을 그대로 고를 수 있게 두고, 시계에서 오전을 흐리게 만듭니다.
"지금 이전은 안 됨" 규칙이 실제로 필요로 하는 동작이 그것이고, 일 단위 검사로는 낼 수 없습니다. 오늘 전체를 막거나 오늘 아침을 허용하거나 둘 중 하나가 됩니다.
Examples
어느 순서로든
날을 고르면 날짜만 바뀌고 시계는 그대로, 시각을 고르면 시계만 바뀌고 날짜는 그대로입니다. 날짜를 고칠 때마다 시각을 자정으로 되돌리는 picker는 순간을 고르는 일을 순서가 정해진 작업으로 만들고, 팝업을 쓰인 순서대로 읽는 사람은 없습니다.
아직 날을 고르지 않았다면 시계는 오늘 위에 쓰이고, 나중에 날을 고르면 설정된 시각이 유지됩니다.
closeOnSelect가 여기서 false인 것도 같은 이유입니다. 순간은 답 둘이라, 푸터에 Done 이 있습니다.
step 간격
hourStep, minuteStep, secondStep은 PlTimePicker의 것 그대로입니다.
이름과 라벨
locale 태그 하나가 월과 요일 이름, 헤더 두 버튼의 순서, 시계가 12시간 다이얼인지, 오전/오후를 뭐라 부르는지, 그리고 trigger가 순간 전체를 어떻게 쓰는지를 정합니다.
names 객체 하나가 월과 요일 이름, 헤더 두 버튼의 순서, 오전/오후의 말을 담습니다. 프레임워크가 대신 정해 줄 수 없는 둘이 hour12와 formatValue입니다. 이유는 PlDatePicker에 있습니다.
readOnly · disabled · error
Accessibility
달력은
PlDatePicker의 것 전부입니다(roving tab stop 하나, 접근성 이름은 날짜 전체). 그리고 열들은PlTimePicker의 것이며, 시각을 한 문장으로 읽어 주는 live region까지 포함합니다.trigger는 둘이 아니라 달력 글리프 하나만 답니다. 컨트롤은 한 번에 두 가지를 말할 수 없고, 독자가 훑는 부분은 날짜입니다.
전체 정밀도 경계에 막힌 날과 같은 경계에 막힌 시각 둘 다 속성이 아니라
aria-disabled를 답니다. 어느 쪽도 키보드가 걷는 경로에서 빠지지 않습니다. ::: fw reactname이 있으면 hidden input이 값을 로컬YYYY-MM-DDTHH:MM으로 담습니다.<input type="datetime-local">이 제출하는 모양입니다.toISOString()은 절대 아닙니다. 서울의 picker라면 다른 날을 제출하게 됩니다.
:::
React 빌드와 다른 점
| React | Flutter | 이유 |
|---|---|---|
locale / format / locale이 정하는 hour12 | names / formatValue / hour12: false | PlDatePicker와 PlTimePicker가 설명하는 그 거래입니다. 프레임워크에 Intl이 없습니다. |
value / defaultValue / onValueChange | value / onChanged | Flutter의 컨트롤은 controlled입니다. |
hidden input, name | — | 참여할 네이티브 form 제출이 없습니다. |
className, style, 네이티브 속성 | — | 통과시킬 class 목록도 style 속성도 없습니다. |