PlDateRangePicker
두 날 사이의 구간입니다. 달 두 개를 나란히 두고, 양 끝 사이의 띠는 두 번째 클릭이 닿기 전에 포인터를 따라 그려집니다.
import { PlDateRangePicker } from 'plass-ui';
<PlDateRangePicker label="Stay" startPlaceholder="Check in" endPlaceholder="Check out" />;import 'package:plass_ui/plass_ui.dart';
PlDateRangePicker(
label: const Text('Stay'),
startPlaceholder: const Text('Check in'),
endPlaceholder: const Text('Check out'),
value: stay,
onChanged: (PlDateRange next) => setState(() => stay = next),
);달력은 트리 밖으로 자기를 들어 올리므로 picker 위에 Overlay가 필요합니다.
Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| value | PlDateRange | null | — | 선택된 구간. onValueChange와 함께 controlled로 씁니다 |
| defaultValue | PlDateRange | null | — | uncontrolled일 때 시작하는 구간 |
| onValueChange | (value: PlDateRange) => void | — | 언제나 객체와 함께 호출됩니다. 비워진 구간은 { start: null, end: null }입니다 |
| minDate | Date | null | — | 고를 수 있는 가장 이른 날. 일 단위입니다 — 시각은 무시됩니다 |
| maxDate | Date | null | — | 고를 수 있는 가장 늦은 날 |
| shouldDisableDate | (date: Date) => boolean | — | 범위 안이지만 그래도 쓸 수 없는 날을 막습니다 — 주말, 공휴일, 이미 예약된 방 |
| weekStartsOn | 0 | 1 | 2 | 3 | 4 | 5 | 6 | — | 한 주가 시작하는 요일. 기본은 locale이 말하는 대로이고, 0이 일요일입니다 |
| defaultMonth | Date | — | 값이 없을 때 달력이 열리는 달 |
| monthCount | 1 | 2 | 2 | 한 번에 보여 줄 달의 수. 달을 넘나드는 구간이 예외가 아니라 보통이라 2가 기본입니다 |
| startPlaceholder | ReactNode | — | 아직 정하지 않은 쪽에 보이는 내용. trigger의 각 반쪽마다 하나씩 |
| endPlaceholder | ReactNode | — | startPlaceholder를 보세요 |
| presets | readonly PlDateRangePreset[] | — | 달력 옆에 놓이는 지름길 — "지난 7일", "이번 달" |
| 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' } | trigger가 양 끝을 쓰는 방식. Intl로 그대로 넘어갑니다 |
| placeholder | ReactNode | — | 아무것도 고르지 않았을 때 trigger에 보이는 내용 |
| clearable | boolean | false | 값을 비우는 ×를 보여 줍니다 |
| closeOnSelect | boolean | 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 | — | 폼 제출 시 필드를 식별합니다. 같은 이름의 hidden input 둘이라 양 끝이 FormData.getAll(name)으로 옵니다 |
| classNames | { label?, control?, description?, error?: string } | — | className이 닿지 않는 부분에 붙는 class. control은 실제로 조작하는 부분입니다 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| value * | PlDateRange | — | 선택된 구간. null이 아닙니다 — 비어 있는 것은 PlDateRange.empty입니다 |
| onChanged | ValueChanged<PlDateRange>? | — | 새 구간과 함께 언제나 객체로 호출됩니다. 한 번의 선택에 두 번 울립니다 — 첫 누름에 start만, 둘째에 양 끝 |
| open | bool? | — | 팝업이 열려 있는지. onOpenChange와 함께 controlled로 씁니다 |
| onOpenChanged | ValueChanged<bool>? | — | 달력이 열리거나 닫혀야 할 때 호출됩니다 |
| defaultMonth | DateTime? | — | 값이 없을 때 달력이 열리는 달 |
| minDate | DateTime? | — | 고를 수 있는 가장 이른 날. 일 단위입니다 — 시각은 무시됩니다 |
| maxDate | DateTime? | — | 고를 수 있는 가장 늦은 날 |
| shouldDisableDate | bool Function(DateTime date)? | — | 범위 안이지만 그래도 쓸 수 없는 날을 막습니다 — 주말, 공휴일, 이미 예약된 방 |
| weekStartsOn | PlassWeekday? | — | 한 주가 시작하는 요일. 기본은 locale이 말하는 대로이고, 0이 일요일입니다 |
| names | PlDateNames | PlDateNames.english | 달력이 그리는 월과 요일 이름, 그리고 헤더가 그것들을 쓰는 순서. **React의 locale 문자열에 해당합니다** — 프레임워크에 Intl이 없으므로 단어를 객체로 받습니다 |
| labels | PlPickerLabels | PlPickerLabels.english | picker가 스스로 말하는 문자열들. 전부 영어 기본값이 있습니다. 날짜 이름은 여기 없습니다 — 그건 Intl이 압니다 |
| formatValue | String Function(DateTime value)? | — | trigger가 값을 쓰는 방식. React의 Intl 옵션 대신 콜백입니다. 빼면 names의 medium 형식으로 씁니다 |
| monthCount | int | 2 | 한 번에 보여 줄 달의 수. 달을 넘나드는 구간이 예외가 아니라 보통이라 2가 기본입니다 |
| startPlaceholder | Widget? | — | 아직 정하지 않은 쪽에 보이는 내용. trigger의 각 반쪽마다 하나씩 |
| endPlaceholder | Widget? | — | startPlaceholder를 보세요 |
| presets | List<PlDateRangePreset> | const [] | 달력 옆에 놓이는 지름길 — "지난 7일", "이번 달" |
| clearable | bool | false | 값을 비우는 ×를 보여 줍니다 |
| closeOnSelect | bool | 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를 함께 주고, value는 null이 아닙니다. 비어 있는 구간은 PlDateRange.empty입니다.
PlDatePicker가 locale과 헤더와 경계와 날짜 라이브러리의 부재에 대해 말한 것은 여기서도 그대로 성립합니다. 이건 그 컴포넌트에 끝이 하나 더 붙은 것입니다.
PlDateRange
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| start * | Date | null | — | 구간의 시작 |
| end * | Date | null | — | 끝. 첫 클릭과 둘째 클릭 사이에는 null입니다 — 반쪽짜리 구간은 실제로 존재하는 상태입니다 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| start | DateTime? | — | 구간의 시작 |
| end | DateTime? | — | 끝. 첫 클릭과 둘째 클릭 사이에는 null입니다 — 반쪽짜리 구간은 실제로 존재하는 상태입니다 |
PlDateRangePreset
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| label * | ReactNode | — | 버튼에 적히는 이름 |
| value * | PlDateRange | (() => PlDateRange) | — | 그것이 뜻하는 구간. 오늘에 달려 있다면 함수로 주세요 — 대개 그렇습니다. 모듈 로드 시점에 계산한 "지난 7일"은 탭을 밤새 열어 둔 사람에게 틀린 값입니다 |
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| label * | Widget | — | 버튼에 적히는 이름 |
| build * | PlDateRange Function() | — | 그것이 뜻하는 구간. React가 값도 허용하는 자리에서 여기서는 언제나 콜백입니다 — preset은 거의 언제나 오늘에 달려 있고, 시작할 때 한 번 계산한 "지난 7일"은 앱을 밤새 열어 둔 사람에게 틀린 값입니다 |
값
[Date, Date] 튜플도, prop 두 개도 아닙니다. 구간은 값 하나 입니다. 한 동작으로 고르고, 한 동작으로 비우고, 통째로 검증합니다. 그리고 이름 두 개가 호출자가 끝을 시작에 써 넣는 것을 막습니다.
반쪽짜리 구간은 실제로 존재하는 상태입니다(첫 클릭과 둘째 클릭 사이에 picker가 들고 있는 것이 그것입니다). 그래서 onValueChange는 첫 클릭 뒤에 { start, end: null }을, 둘째 뒤에 완성된 구간을 보고합니다. controlled 호출자가 요청하지 않은 중간 상태를 떠안는 일은 없습니다. 대기 중인 anchor는 폼이 아니라 컴포넌트 안에 삽니다.
미리보기 띠
띠는 두 번째 클릭이 닿기 전에 anchor와 포인터가 지금 올라가 있는 날 사이에 그려집니다. 그것이 없으면 첫 클릭에는 눈에 보이는 결과가 없고, 그 1초 남짓 동안 컨트롤은 고장 난 것처럼 보입니다.
거꾸로 클릭하는 것은 거부해야 할 실수가 아닙니다. 같은 구간을 반대 순서로 말한 것이고, 하나로 확정됩니다.
Examples
monthCount
두 달이 기본인 것은 달을 넘나드는 구간이 예외가 아니라 보통이기 때문입니다. 한 달짜리 picker는 그것을 2단계 탐색 문제로 만듭니다.
두 패널은 반으로 나뉜 하나의 달력 입니다. 왼쪽에는 앞으로 가는 stepper가 없고, 오른쪽에는 뒤로 가는 stepper가 없으며, 어느 헤더의 월/연도 버튼이든 둘 다를 움직입니다. stepper를 그리지 않는 자리에는 그 크기만큼의 빈칸을 남겨서, 두 제목이 같은 중심선에 머뭅니다.
바깥 달의 날도 그리지 않습니다. 취향의 문제가 아닙니다. 두 패널이 모두 여섯 주를 다 그리면 8월 1일이 두 번 나타납니다. 한 번은 7월의 꼬리로, 한 번은 자기 자신으로. 한 팝업 안에 이름이 같은 칸 둘은 포인터에게 모호하고 스크린리더에게는 완전히 고장입니다.
presets
달력 옆에 놓이는 이름 붙은 구간. 사람들이 실제로 고르는 것들입니다. 오늘에 달려 있다면 value를 함수 로 주세요. 대개 그렇습니다. 모듈 로드 시점에 계산한 "지난 7일"은 탭을 밤새 열어 둔 사람에게는 틀린 구간입니다.
minDate · maxDate · shouldDisableDate
PlDatePicker의 그 셋을 양 끝에 적용한 것입니다. 막힌 날은 그리드에 남고 화살표 경로에서 자기 자리를 지키며, 구간의 색을 입지 않습니다. 띠를 두른 막힌 날은 자기가 낄 수 없는 구간의 일부라고 광고하는 셈입니다.
Controlled
value를 onValueChange와 함께 주세요. 콜백은 언제나 객체를 받으므로 null 구간을 방어할 필요가 없습니다. 비워진 picker는 { start: null, end: null }입니다.
Accessibility
- 두 그리드 다 각자 roving tab stop을 하나씩 가지며, 키보드는
PlDatePicker의 것 그대로입니다.
- 두 그리드 다
role="grid"입니다. - 모든 칸의 접근성 이름은 날짜 전체 이고, 한 팝업 안에 같은 날짜가 두 번 나타나지 않습니다. 바깥 달의 날을 끈 대가로 얻는 것이 그것입니다.
- 푸터는 다음 클릭이 어느 쪽을 채우는지 말합니다. trigger의 두 반쪽도 같은 말을 하지만, 팝업이 떠 있는 동안 trigger는 그 뒤에 가려집니다. 읽힐 자리에서 그 말을 할 수 있는 곳은 푸터뿐입니다.
- trigger 두 반쪽 사이의 화살표는
aria-hidden이고 RTL에서 뒤집힙니다. - trigger의 각 반쪽은 자기가 담을 수 있는 모든 날짜에 대해 자기 너비를 붙잡아 둡니다. 그래서 두 번째 끝을 채워도 첫 번째가 크기를 바꾸지 않습니다. 그 샘플들은
aria-hidden이고 generated content로 그려집니다. name이 있으면 같은 이름의 hidden input 둘이 양 끝을 로컬YYYY-MM-DD로 담아,FormData.getAll(name)으로 옵니다.
- trigger는 양 끝을 label에 접어 넣는 대신 semantics value 로 지닙니다.
- 두 반쪽 사이의 화살표는 RTL에서 돌아가므로 언제나 첫 끝에서 둘째 끝을 가리킵니다.
- 너비를 잡아 주는 샘플들은
ExcludeSemantics뒤에 있습니다.
React 빌드와 다른 점
| React | Flutter | 이유 |
|---|---|---|
locale / format | names / formatValue | PlDatePicker가 설명하는 그 거래입니다. 프레임워크에 Intl이 없습니다. |
value: PlDateRange | null | value: PlDateRange, null 없음 | PlDateRange.empty가 그것을 말하고, non-nullable 값은 호출자가 방어할 것이 하나 줄어드는 일입니다. |
preset의 value는 구간이거나 함수 | build는 언제나 함수 | preset은 거의 언제나 오늘에 달려 있고, 언제나 맞는 한 가지 모양이 두 가지보다 쌉니다. |
hidden input, name | — | 참여할 네이티브 form 제출이 없습니다. |
className, style, 네이티브 속성 | — | 통과시킬 class 목록도 style 속성도 없습니다. |