PlTimePicker
열에서 시각을 고릅니다. 열인 이유는, 시간 picker가 실제로 받는 질문에 답하는 모양이 열이기 때문입니다.
import { PlTimePicker } from 'plass-ui';
<PlTimePicker label="Doors" placeholder="Pick a time" minuteStep={15} />;import 'package:plass_ui/plass_ui.dart';
PlTimePicker(
label: const Text('Doors'),
minuteStep: 15,
value: doors,
onChanged: (DateTime? next) => setState(() => doors = next),
);열들은 트리 밖으로 자기를 들어 올리므로 picker 위에 Overlay가 필요합니다.
Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| value | Date | null | — | 선택된 시각. Date이므로 날짜도 함께 지닙니다 — referenceDate를 보세요 |
| defaultValue | Date | null | — | uncontrolled일 때 시작하는 시각 |
| onValueChange | (value: Date | null) => void | — | 값이 바뀔 때 호출됩니다 |
| referenceDate | Date | today | 아직 값이 없을 때 고른 시각이 얹히는 날. picker가 마운트되어 있는 동안 고정입니다 — 자정을 넘겨 열어 둔 팝업이 값을 다른 날로 옮기면 안 되니까요 |
| minTime | Date | null | — | 고를 수 있는 가장 이른 시각. 시계만 읽습니다 |
| maxTime | Date | null | — | 고를 수 있는 가장 늦은 시각 |
| 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 | { hour: 'numeric', minute: '2-digit' } | trigger가 시각을 쓰는 방식. Intl로 그대로 넘어갑니다. showSeconds면 초가 붙습니다 |
| placeholder | ReactNode | — | 아무것도 고르지 않았을 때 trigger에 보이는 내용 |
| clearable | boolean | false | 값을 비우는 ×를 보여 줍니다 |
| closeOnSelect | boolean | false | 어느 열이든 건드리는 즉시 팝업을 닫습니다. PlDatePicker와 달리 기본이 false인 건 시각이 답 두 개이고, 첫 답에 닫으면 9:30을 고르는 데 팝업을 두 번 열어야 하기 때문입니다 |
| 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 | — | 폼 제출 시 필드를 식별합니다. HH:MM으로, showSeconds면 HH:MM:SS로 보냅니다 |
| classNames | { label?, control?, description?, error?: string } | — | className이 닿지 않는 부분에 붙는 class. control은 실제로 조작하는 부분입니다 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| value * | DateTime? | — | 선택된 시각. DateTime이므로 날짜도 함께 지닙니다 |
| onChanged | ValueChanged<DateTime?>? | — | 고른 시각과 함께 호출됩니다. 비우면 null입니다 |
| open | bool? | — | 팝업이 열려 있는지. onOpenChange와 함께 controlled로 씁니다 |
| onOpenChanged | ValueChanged<bool>? | — | 열들이 열리거나 닫혀야 할 때 호출됩니다 |
| referenceDate | DateTime? | now | 아직 값이 없을 때 고른 시각이 얹히는 날. picker가 마운트되어 있는 동안 고정입니다 — 자정을 넘겨 열어 둔 팝업이 값을 다른 날로 옮기면 안 되니까요 |
| minTime | DateTime? | — | 고를 수 있는 가장 이른 시각. 시계만 읽습니다 |
| maxTime | DateTime? | — | 고를 수 있는 가장 늦은 시각 |
| 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 | AM과 PM이 나오는 곳 |
| labels | PlPickerLabels | PlPickerLabels.english | picker가 스스로 말하는 문자열들. 전부 영어 기본값이 있습니다. 날짜 이름은 여기 없습니다 — 그건 Intl이 압니다 |
| formatValue | String Function(DateTime value)? | — | trigger가 시각을 쓰는 방식. 빼면 H:MM이고, 초와 오전/오후가 켜져 있으면 함께 붙습니다 |
| placeholder | Widget? | — | 아무것도 고르지 않았을 때 trigger에 보이는 내용 |
| clearable | bool | false | 값을 비우는 ×를 보여 줍니다 |
| showNowButton | bool | true | 푸터에 지금으로 가는 지름길을 둡니다 |
| closeOnSelect | bool | false | 어느 열이든 건드리는 즉시 팝업을 닫습니다. PlDatePicker와 달리 기본이 false인 건 시각이 답 두 개이고, 첫 답에 닫으면 9:30을 고르는 데 팝업을 두 번 열어야 하기 때문입니다 |
| 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가 locale과 날짜 라이브러리의 부재에 대해 말한 것은 여기서도 성립합니다. 시계가 12시간 다이얼인지, 오전/오후를 뭐라 부르는지는 Intl이 정합니다.
PlDatePicker가 날짜 라이브러리의 부재에 대해 말한 것은 여기서도 성립합니다. 다른 것은 다이얼을 대신 정해 주는 것이 없다는 점입니다. hour12는 기본이 그냥 false이고, 켰을 때 쓰는 말은 PlDateNames.am과 .pm입니다.
다이얼이 아니라 열입니다
"9시 반"은 두 열에 대한 두 번의 눈길입니다. "정각이면 아무 때나"는 아예 건드리지 않는 열입니다. 시계 문자판은 더 예쁘고, 읽으려면 transform이 필요하며, 어느 질문에도 더 빨리 답하지 못합니다. 그리고 이 라이브러리는 컨트롤에 transform을 걸지 않습니다.
각 열에서 고른 행은 열릴 때 한 번 화면 안으로 스크롤됩니다. 장식이 아닙니다. 값이 45인데 00에서 열리는 60분짜리 열은 자기 답을 숨긴 것입니다.
경계
작동하는 시간 picker와 짜증나는 시간 picker를 가르는 지점입니다. 경계는 한 행이 대표하는 구간 에 대고 검사하지, 그 안의 한 순간에 대고 검사하지 않습니다.
minTime이 09:30이면 시각 9는 09:00:00–09:59:59를 덮고 그것은 허용 범위와 겹치므로 그대로 남습니다. 그리고 00부터 25가 흐려지는 곳은 분 열입니다. 후보 전체를 비교하면 9가 통째로 사라지고 9시 반은 닿을 수 없게 됩니다.
값
문자열도, 분의 개수도 아닙니다. 이 라이브러리에서 순간을 지니는 다른 모든 것이 Date이고, 맨 시각에는 서머타임 경계를 넘었다는 사실을 기록할 자리가 없습니다. referenceDate는 맨 시각이 얹히는 날이고, picker가 마운트되어 있는 동안 고정입니다. 자정을 넘겨 열어 둔 팝업이 값을 조용히 다른 날로 옮기면 안 됩니다.
Examples
hour12
말하지 않으면 locale에서 가져옵니다.
말하지 않으면 false입니다. 이 지역이 무엇을 쓰는지 물어볼 Intl이 없습니다.
12시간 다이얼은 0, 1, 2가 아니라 12, 1, 2 … 11로 읽히고 AM/PM 열이 붙습니다. 24시간 다이얼은 00부터 23까지이고 그 열이 없습니다.
step 간격
hourStep, minuteStep, secondStep이 행 간격을 정합니다. 15분 단위로만 받는 예약이라면 09:07을 나중에 거절하는 대신 minuteStep={15}로 미리 말해야 합니다.
minTime · maxTime · shouldDisableTime
minTime과 maxTime은 시계만 읽습니다. 거기 붙은 날짜는 무시됩니다. shouldDisableTime은 열마다 행마다, 그 행이 만들어 낼 시각과 그 행이 속한 열을 받아 한 번씩 호출됩니다. 규칙은 "오후는 안 됨"만큼 성길 수도, 1분만큼 촘촘할 수도 있습니다.
closeOnSelect
여기서는 false이고 PlDatePicker에서는 true입니다. 날은 답이 하나이고 시각은 둘입니다. 첫 답에 닫아 버리면 9:30을 고르는 데 팝업을 두 번 열어야 합니다.
열들을 읽는 동안 팝업이 떠 있으므로, 그게 그거다 라는 뜻으로 누를 것이 있어야 합니다. 그래서 푸터에 Done 이 있습니다. closeOnSelect를 켜면 할 일이 없어지므로 사라집니다.
readOnly · disabled · error
Accessibility
- 각 열은 자기가 담은 단위의 이름을 달고, 각 행은 자기가 골라진 행인지를 말합니다.
- 막힌 행은 사라지지 않고 자기 열에 남아 사용할 수 없다고 읽힙니다.
- 각 열은
role="listbox"이고 각 행은aria-selected를 지닌option입니다. 막힌 행은 속성이 아니라aria-disabled를 답니다. - 이름 없는 숫자 목록 셋은 보지 않는 독자에게 아무 말도 하지 않습니다. 그래서 열들 옆의 polite live region이 값이 바뀔 때마다 전체 시각을 한 문장으로 읽어 줍니다.
- 각 열에서 고른 행은 자기 열 안에서만 화면 안으로 들어옵니다.
scrollIntoView가 아니라scrollTop을 씁니다. 전자는 문서까지 올라가며 스크롤 가능한 모든 조상을 훑고, 팝업이 열리는 그 프레임에는 곧 움직일 행을 보여 주겠다고 페이지를 맨 위로 끌어올립니다. - trigger는 담을 수 있는 가장 긴 시각의 너비로 붙잡혀 있습니다. 그 샘플들은
aria-hidden이고 generated content로 그려집니다. name이 있으면 hidden input이 값을 로컬HH:MM으로 담습니다.<input type="time">이 제출하는 모양이라, 그것을 이미 파싱하는 서버는 새 코드가 필요 없습니다.
- 각 열은 자기 단위의 이름을 단 semantics container이고, 각 행은 자기가 뜻하는 것 전체로 읽힙니다:
14가 아니라14 Hour. - trigger는 시각을 label에 접어 넣는 대신 semantics value 로 지닙니다.
- 열들 옆의 live region이 값이 바뀔 때마다 전체 시각을 읽어 줍니다.
React 빌드와 다른 점
| React | Flutter | 이유 |
|---|---|---|
hour12가 locale의 다이얼을 따름 | 기본이 false | 물어볼 Intl이 없습니다. 말은 PlDateNames.am / .pm이 댑니다. |
format: Intl.DateTimeFormatOptions | formatValue: String Function(DateTime) | PlDatePicker가 설명하는 그 거래입니다. |
role="listbox"와 option | 이름 붙은 semantics container와 뜻을 말하는 행 | Flutter는 상태를 노드 자체에 적습니다. |
hidden input, name | — | 참여할 네이티브 form 제출이 없습니다. |
className, style, 네이티브 속성 | — | 통과시킬 class 목록도 style 속성도 없습니다. |