Field.TimePicker
Field.TimePicker는 Field 안에서 Popover에 시각 목록을 여는 버튼입니다. value는 Temporal.PlainTime이나 null이고, onValueChange는 목록에서 고른 시각을 받으며, 그러면 목록이 닫힙니다. 목록에는 자정부터 step분마다, 기본값 30분마다, min과 max 사이의 시각이 있습니다. Field.DatePicker와 같은 방식으로 동작하고, 뜻이 같은 props는 같은 이름으로 받습니다.
이 페이지의 내용
const id = useId();
const [time, setTime] = useState<Temporal.PlainTime | null>(null);
const [text, setText] = useState('');
const [invalid, setInvalid] = useState(false);
<Field.Group>
<Label.Root htmlFor={id}>Start time</Label.Root>
<Field.Root id={id} invalid={invalid}>
<Field.TextInput
value={text}
onChange={(event) => {
setText(event.target.value);
setTime(parseTime(event.target.value));
setInvalid(false);
}}
onBlur={() => {
if (time) setText(formatTime(time));
else setInvalid(text.trim() !== '');
}}
/>
<Field.TimePicker
value={time}
onValueChange={(next) => {
setTime(next);
setText(formatTime(next));
setInvalid(false);
}}
step={15}
showValue={false}
/>
</Field.Root>
</Field.Group>
여기서는 같은 필드의 Field.TextInput으로 시각을 입력할 수도 있습니다. 9:07처럼 목록의 시각 사이에 있는 시각도 됩니다. 고른 시각은 formatTime으로 텍스트에 쓰이고, parseTime이 시각으로 읽은 입력은 min과 max 사이에 있으면 목록의 시각들 사이에 나타납니다. Field.TextInput 옆에서는 그 입력이 필드의 컨트롤입니다. 입력이 필드의 id, name, 설명을 받고, 필드의 다른 곳을 누르면 입력에 포커스가 갑니다. showValue={false}이면 버튼은 시계 글리프만 보여 줍니다. 버튼은 자신의 name이 있으면 그 이름으로 폼에 시각을 보냅니다. 없으면 필드의 name으로 보내는데, Field.ColorPicker, Field.DatePicker, Field.DateRangePicker, Field.TimePicker, Field.DateTimePicker가 아닌 Field 파트나 useField로 연결한 컨트롤이 그 name을 가지고 있으면 보내지 않습니다. 자신의 name이 없는 Field.TextInput이 그런 경우입니다. 입력한 텍스트 대신 09:30 형식으로 시각을 보내려면 Root 대신 선택기에 name을 두세요.
입력한 시각
formatTime(time, { locale })은 시각을 로캘의 short 시각 스타일로 씁니다. 예: en-US에서는 9:30 PM, de에서는 21:30, ko에서는 오후 9:30. 초가 있으면 초까지 쓰는 medium 스타일로 씁니다. parseTime(text, { locale })은 입력한 텍스트를 Temporal.PlainTime으로 읽고, 시각이 아니면 null을 돌려줍니다. 9, 9:30, 21.30.15, 0930처럼 시, 시와 분, 또는 시와 분과 초를 읽고, 그 앞이나 뒤에 있는 로캘 자체의 하루 중 때를 나타내는 단어도 읽습니다. 예: en-US에서는 9:30 pm, ko에서는 오후 9:30. 대소문자는 상관없고, 숫자는 어느 문자 체계의 숫자여도 되며, 숫자 사이는 어떤 문장 부호, 기호, 공백으로 나누거나, fr-CA의 21 h 30처럼 로캘이 숫자 사이에 쓰는 단어로 나눠도 됩니다. 하루 중 때를 나타내는 단어가 없으면 시는 0부터 23까지입니다. locale이 없으면 둘 다 런타임의 로캘을 씁니다.
예시에서는 시각이 아닌 텍스트가 시각을 비우므로 폼은 시각을 보내지 않고, 필드는 포커스를 잃을 때 유효하지 않음으로 표시됩니다. parseTime은 선택기의 min과 max를 확인하지 않지만, 이를 벗어난 입력 시각은 폼을 유효하지 않게 합니다.
단독으로
단독으로 쓰면 Field.TimePicker가 필드의 컨트롤입니다. 시각을 formatTime이 locale로 쓰는 대로, 시각이 없으면 placeholder를 보여 주고, 필드의 다른 곳을 누르면 목록이 열립니다. Root에 name이 있으면 폼이 HTML이 쓰는 형식으로 시각을 보냅니다. 예: 09:30, 초가 있으면 09:30:15.
목록은 open과 onOpenChange, 또는 defaultOpen으로 열고 닫습니다. onOpenChange는 닫힌 이유를 받습니다. 시각을 고르면 'select', 그 밖에는 'escape', 'outside', 'trigger', 필드가 비활성화되면 'disabled'입니다. 열리면 목록이 포커스를 받고, 그 시각에서, 없으면 지금 다음 시각에서, 그것도 없으면 첫 시각에서 시작합니다. Escape를 누르면 닫히고, 포커스는 열리기 전에 있던 곳으로 돌아갑니다.
required는 시각이 없는 폼을, min과 max는 그보다 이르거나 늦은 시각이 있는 폼을 유효하지 않게 하며, 목록의 범위도 제한합니다. max보다 늦은 min은 네이티브 시각 입력처럼 밤을 넘기는 범위입니다. 목록은 min부터 자정까지와 자정부터 max까지의 시각을 보여 주고, 폼도 같은 시각을 받아들입니다. max와 min 사이의 빈 구간에 있는 시각은 범위를 벗어나며, 메시지는 "Enter 10:00 PM or later"처럼 min을 부릅니다. 브라우저는 선택기가 버튼 옆에 렌더링하는 네이티브 <input type="time">에서 세 가지를 모두 검사합니다. 이 입력은 화면과 보조 기술에서 숨겨지고 탭 정지점이 아니며, 폼이 필드의 name으로 보내는 값입니다. Form.Root 안에서는 문제가 필드의 ErrorMessage.Root에 페이지의 언어로 나타나고 min이나 max는 목록이 시각을 쓰는 방식으로 표기되며, 포커스는 숨은 입력이 아니라 버튼으로 갑니다. 요약의 해당 필드 행도 버튼으로 연결됩니다. Form.Root 밖에서도 브라우저가 제출을 막고, 포커스는 마찬가지로 버튼에 놓입니다.
Temporal
Todae는 Temporal 폴리필을 포함하지 않습니다. 선택기의 값과 formatTime, parseTime은 Temporal.PlainTime이므로, 브라우저에 Temporal이 없는 곳에서는 앱이 폴리필을 불러옵니다(네이티브 플랫폼 참고). Todae의 타입에는 Temporal을 선언하는 TypeScript 6 이상이 필요합니다.
접근성을 지키는 조합
Todae가 맡는 것: 단독으로 쓰면 버튼은 Field.Root에서 id, aria-label, aria-labelledby, aria-describedby를 받고, Root나 바깥 Fieldset에서 네이티브 disabled와 aria-invalid를 받습니다. 레이블이 버튼의 이름이 되므로, 버튼에 보이는 시각은 aria-describedby에 더해집니다. 텍스트 입력처럼 Field.ColorPicker, Field.DatePicker, Field.DateRangePicker, Field.DateTimePicker, 다른 Field.TimePicker가 아닌 Field 파트나 useField로 연결한 컨트롤이 필드에 함께 있으면 그 컨트롤이 대신 id와 aria-describedby를 받고, 버튼은 disabled와 aria-invalid를 유지하며, 자체 aria-label이 없으면 StringsProvider의 timePicker 문자열로 이름이 붙습니다. 버튼에는 aria-haspopup="listbox"와 aria-expanded가, 목록이 열려 있는 동안에는 aria-controls가 붙습니다. 목록은 timePicker 문자열로 이름이 붙는 OptionList이고, 자체 역할이 없는 팝오버 안에 있습니다. 화살표 키, Home, End로 시각 사이를 옮기고, Enter나 Space로 고릅니다. 글리프는 GlyphsProvider의 clock입니다. 필수 선택기의 버튼은 버튼에 aria-required를 쓸 수 없으므로, StringsProvider의 required 문자열로 설명됩니다.
직접 제공할 것: 이름. htmlFor가 Root id인 Label이나, Root의 aria-label / aria-labelledby입니다. 텍스트 입력과 그 옆의 선택기에는 같은 시각을 주고, formatTime으로 쓰고 parseTime으로 읽으세요.
주의할 점: 서버 렌더는 버튼 옆에 다른 컨트롤이 있는지 알 수 없으므로, 페이지가 하이드레이션될 때까지 버튼과 그 컨트롤 둘 다 필드의 id를 가지고 필드의 name으로 값을 보냅니다. value가 있으면 서버는 그 시각을 서버의 로캘로 쓰므로, 서버에서 렌더링할 때는 locale을 넘기세요.
언제 쓰나요
Field.TimePicker
언제: 영업시간, 매일 알림, 회의 시작처럼 하루 중 시각.
쓰지 않을 때: 하나의 값인 날짜와 시각 → Field.DateTimePicker. 기간 → Field.NumberInput과 단위를 보여 주는 Affix.
formatTime
언제: 피커 옆 텍스트 입력처럼, 시각을 로캘에 맞는 텍스트로 쓸 때.
쓰지 않을 때: 시간대를 붙이는 것처럼 다른 형식의 시각 → Intl.DateTimeFormat.
parseTime
언제: 사람이 입력한 시각을 읽을 때.
쓰지 않을 때: 09:30처럼 형식이 정해진 텍스트 → Temporal.PlainTime.from.
Props
| 이름 | 타입 | 설명 |
|---|---|---|
value 필수 | Temporal.PlainTime | null | 시각, 없으면 null. |
onValueChange | (value: Temporal.PlainTime) => void | 목록에서 고른 시각을 받아 호출됩니다. |
min | Temporal.PlainTime | 목록이 보여 주는 가장 이른 시각. 이보다 이른 시각이 있으면 폼이 유효하지 않습니다. max보다 늦으면 min부터 자정을 지나 max까지의 밤을 넘기는 범위가 됩니다. |
max | Temporal.PlainTime | 목록이 보여 주는 가장 늦은 시각. 이보다 늦은 시각이 있으면 폼이 유효하지 않습니다. |
required | boolean | 시각이 없으면 폼이 유효하지 않습니다. |
step | number | 자정부터 센, 목록의 시각 사이 간격(분). 기본값: 30 |
locale | string | 시각 텍스트의 로캘, 예: "ko". 없으면 브라우저의 로캘이므로, 서버에서 렌더링할 때는 넘기세요. |
placeholder | string | showValue가 true일 때, 시각이 없는 동안 버튼에 보입니다. |
showValue | boolean | 버튼이 글리프 옆에 시각이나 placeholder를 보여 줄지 여부. 시각을 보여 주는 텍스트 입력 옆에서는 false로 두세요. 기본값: true |
open | boolean | 목록의 제어 열림 상태. onOpenChange와 함께 전달하세요. |
defaultOpen | boolean | 비제어일 때 처음 열림 상태. |
onOpenChange | (open: boolean, reason: 'escape' | 'outside' | 'trigger' | 'select' | 'disabled') => void | 목록이 열리거나 닫힐 때 호출되며, 닫힌 이유를 함께 받습니다. |
| 이름 | 타입 | 설명 |
|---|---|---|
time 필수 | Temporal.PlainTime | 쓸 시각. |
options.locale | string | ko 같은 로캘. 지정하지 않으면 런타임의 로캘입니다. |
| 이름 | 타입 | 설명 |
|---|---|---|
text 필수 | string | 입력된 텍스트. |
options.locale | string | ko 같은 로캘. 지정하지 않으면 런타임의 로캘입니다. |