Field.DateTimePicker

Field.DateTimePicker는 Field 안에서 Popover에 Calendar와 시각 목록을 나란히, 좁은 화면에서는 목록을 달력 아래에 여는 버튼입니다. value는 Temporal.PlainDateTime이나 null입니다. onValueChange는 고른 뒤에 달력이 내놓는 날짜와 그 날짜의 범위 안 시각이 있을 때마다 그 날짜와 시각을 받습니다. 달력이 내놓는 날짜가 골라져 있을 때 시각을 고르면 팝오버가 닫힙니다. Field.DatePicker, Field.TimePicker와 같은 방식으로 동작하고, 뜻이 같은 props는 같은 이름으로 받습니다. PlainDateTime에는 시간대가 없습니다. 사용자의 시계에 보이는 날짜와 시각입니다. 시점을 저장하려면 value.toZonedDateTime(zone)처럼 직접 작성한 코드에서 시간대와 합치세요.

이 페이지의 내용
const id = useId();
const [start, setStart] = useState<Temporal.PlainDateTime | null>(null);
const [text, setText] = useState('');
const [invalid, setInvalid] = useState(false);

<Field.Group>
  <Label.Root htmlFor={id}>Meeting starts</Label.Root>
  <Field.Root id={id} invalid={invalid}>
    <Field.TextInput
      value={text}
      onChange={(event) => {
        setText(event.target.value);
        setStart(parseDateTime(event.target.value));
        setInvalid(false);
      }}
      onBlur={() => {
        if (start) setText(formatDateTime(start));
        else setInvalid(text.trim() !== '');
      }}
    />
    <Field.DateTimePicker
      value={start}
      onValueChange={(next) => {
        setStart(next);
        setText(formatDateTime(next));
        setInvalid(false);
      }}
      step={15}
      showValue={false}
    />
  </Field.Root>
</Field.Group>

여기서는 같은 필드의 Field.TextInput으로 날짜와 시각을 입력할 수도 있습니다. 고른 날짜와 시각은 formatDateTime으로 텍스트에 쓰입니다. Field.TextInput 옆에서는 그 입력이 필드의 컨트롤입니다. 입력이 필드의 id, name, 설명을 받고, 필드의 다른 곳을 누르면 입력에 포커스가 갑니다. showValue={false}이면 버튼은 달력 글리프만 보여 줍니다. 버튼은 자신의 name이 있으면 그 이름으로 폼에 날짜와 시각을 보냅니다. 없으면 필드의 name으로 보내는데, Field.ColorPicker, Field.DatePicker, Field.DateRangePicker, Field.TimePicker, 다른 Field.DateTimePicker가 아닌 Field 파트나 useField로 연결한 컨트롤이 그 name을 가지고 있으면 보내지 않습니다. 자신의 name이 없는 Field.TextInput이 그런 경우입니다. 입력한 텍스트 대신 2026-10-13T09:30 형식으로 보내려면 Root 대신 선택기에 name을 두세요.

입력한 날짜와 시각

formatDateTime(value, { locale })은 날짜를 formatDate처럼 숫자로, 시각을 시와 분으로, 초가 있으면 초까지 씁니다. 예: en-US에서는 10/13/2026, 9:30 PM, de에서는 13.10.2026, 21:30, ko에서는 2026. 10. 13. 오후 9:30. parseDateTime(text, { locale })은 입력한 텍스트를 Temporal.PlainDateTime으로 읽고, 날짜와 시각이 아니면 null을 돌려줍니다. 날짜는 parseDate처럼 읽되 연도는 전부 쓰거나 끝의 두 자리로 쓰고, 이어서 시각은 parseTime처럼 로캘이 쓰는 순서로 읽고, 로캘이 둘 사이에 쓰는 단어는 있어도 없어도 됩니다. 예: en-US에서는 Oct 13, 2026 at 9:30 PM이나 10/13/2026 9:30 pm. 시각에는 21:30이나 2130처럼 분이 있거나 9 pm처럼 단어가 있어야 하므로, 13.10.2026 21처럼 시만 있는 텍스트는 읽지 않고, 시각이 없는 날짜도 읽지 않습니다. en-US의 10/13 12 05 pm처럼 연도 없는 월과 일, 그리고 시각으로도 읽히는 텍스트는 읽지 않습니다. locale이 없으면 둘 다 런타임의 로캘을 씁니다.

예시에서는 날짜와 시각이 아닌 텍스트가 값을 비우므로 폼은 값을 보내지 않고, 필드는 포커스를 잃을 때 유효하지 않음으로 표시됩니다. parseDateTime은 선택기의 min, max, isDateDisabled를 확인하지 않습니다. min과 max를 벗어난 입력 값은 폼을 유효하지 않게 하지만, isDateDisabled는 직접 확인하세요.

단독으로

단독으로 쓰면 Field.DateTimePicker가 필드의 컨트롤입니다. 날짜는 로캘의 medium 날짜 스타일로, 시각은 short 시각 스타일로, 초가 있으면 medium 스타일로 보여 주고, 값이 없으면 placeholder를 보여 주며, 필드의 다른 곳을 누르면 팝오버가 열립니다. Root에 name이 있으면 폼이 HTML이 쓰는 형식으로 값을 보냅니다. 예: 2026-10-13T09:30.

팝오버는 open과 onOpenChange, 또는 defaultOpen으로 열고 닫습니다. onOpenChange는 닫힌 이유를 받습니다. 고른 시각이 값이 되면 'select', 그 밖에는 'escape', 'outside', 'trigger', 필드가 비활성화되면 'disabled'입니다. 열리면 달력이 그 날짜에, 없으면 오늘이나 범위 안의 가장 가까운 날짜에 포커스를 둡니다. 날짜를 고르면 포커스가 시각 목록으로 가고, 날짜보다 시각을 먼저 고르면 달력으로 갑니다. Escape를 누르면 팝오버가 닫히고, 포커스는 열리기 전에 있던 곳으로 돌아갑니다.

min과 max는 두 쪽을 다 제한합니다. 달력은 그 사이의 날짜만 내놓고, 그중 첫 날짜와 마지막 날짜에는 목록이 범위 안의 시각만 내놓습니다. 고른 날짜에 대해 시각이 범위 밖이면, 범위 안의 시각을 고를 때까지 값을 알리지 않습니다. 선택기 옆에 입력한 날짜처럼 달력이 내놓지 않는 날짜는 날짜가 없는 것으로 보므로, 그때 시각을 고르면 포커스가 달력으로 갑니다. isDateDisabled와 firstDayOfWeek는 Field.DatePicker에서처럼, step은 Field.TimePicker에서처럼 동작합니다.

required는 날짜와 시각이 없는 폼을, min과 max는 그보다 이르거나 늦은 값이 있는 폼을 유효하지 않게 하며, 달력과 목록의 범위도 제한합니다. 브라우저는 선택기가 버튼 옆에 렌더링하는 네이티브 <input type="datetime-local">에서 세 가지를 모두 검사합니다. 이 입력은 화면과 보조 기술에서 숨겨지고 탭 정지점이 아니며, 폼이 필드의 name으로 보내는 값입니다. Form.Root 안에서는 문제가 필드의 ErrorMessage.Root에 페이지의 언어로 나타나고 min이나 max는 버튼이 날짜와 시각을 쓰는 방식으로 표기되며, 포커스는 숨은 입력이 아니라 버튼으로 갑니다. 요약의 해당 필드 행도 버튼으로 연결됩니다. Form.Root 밖에서도 브라우저가 제출을 막고, 포커스는 마찬가지로 버튼에 놓입니다. 네이티브 날짜 입력은 1년부터 275760년까지만 담을 수 있으므로, 그 밖의 값이나 범위는 날짜 없음으로 읽힙니다. 빈 값으로 전송되고 required, min, max는 이를 무시하며, 개발 빌드는 한 번 경고합니다. isDateDisabled는 검사에 포함되지 않습니다.

Temporal

Todae는 Temporal 폴리필을 포함하지 않습니다. 선택기의 값과 formatDateTime, parseDateTime은 Temporal.PlainDateTime이므로, 브라우저에 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.TimePicker, 다른 Field.DateTimePicker가 아닌 Field 파트나 useField로 연결한 컨트롤이 필드에 함께 있으면 그 컨트롤이 대신 id와 aria-describedby를 받고, 버튼은 disabled와 aria-invalid를 유지하며, 자체 aria-label이 없으면 StringsProvider의 dateTimePicker 문자열로 이름이 붙습니다. 버튼에는 aria-haspopup="dialog"와 aria-expanded가, 팝오버가 열려 있는 동안에는 aria-controls가 붙습니다. 팝오버는 dateTimePicker 문자열로 이름이 붙는 role="dialog"입니다. 안에는 키보드 조작이 해당 페이지에 설명된 Calendar와, timePicker 문자열로 이름이 붙는 시각의 OptionList가 있으며, 목록에서는 화살표 키, Home, End로 옮기고 Enter나 Space로 고릅니다. 글리프는 GlyphsProvider의 calendar입니다. 필수 선택기의 버튼은 버튼에 aria-required를 쓸 수 없으므로, StringsProvider의 required 문자열로 설명됩니다.

직접 제공할 것: 이름. htmlFor가 Root id인 Label이나, Root의 aria-label / aria-labelledby입니다. 텍스트 입력과 그 옆의 선택기에는 같은 값을 주고, formatDateTime으로 쓰고 parseDateTime으로 읽으세요.

주의할 점: 서버 렌더는 버튼 옆에 다른 컨트롤이 있는지 알 수 없으므로, 페이지가 하이드레이션될 때까지 버튼과 그 컨트롤 둘 다 필드의 id를 가지고 필드의 name으로 값을 보냅니다. value가 있으면 서버는 그 날짜와 시각을 서버의 로캘로 쓰므로, 서버에서 렌더링할 때는 locale을 넘기세요.

언제 쓰나요

Field.DateTimePicker

언제: 회의 시작이나 항공편 출발처럼 날짜와 하루 중 시각을 하나의 값으로.

쓰지 않을 때: 날짜만 → Field.DatePicker. 시각만 → Field.TimePicker. 시간대가 있는 시점 → 이 선택기를 쓰고 시간대는 직접 작성한 코드에서 더하세요.

formatDateTime

언제: 피커 옆 텍스트 입력처럼, 날짜와 시간을 로캘에 맞는 텍스트로 쓸 때.

쓰지 않을 때: 요일이나 월 이름이 있는 날짜와 시간 → Intl.DateTimeFormat.

parseDateTime

언제: 사람이 입력한 날짜와 시간을 읽을 때.

쓰지 않을 때: 2026-10-13T09:30처럼 형식이 정해진 텍스트 → Temporal.PlainDateTime.from.

Props

Field.DateTimePicker props
이름타입설명
value 필수Temporal.PlainDateTime | null

날짜와 시각, 없으면 null.

onValueChange(value: Temporal.PlainDateTime) => void

고른 뒤에 달력이 내놓는 날짜와 그 날짜의 범위 안 시각이 있으면 그 날짜와 시각을 받아 호출됩니다.

minTemporal.PlainDateTime

고를 수 있는 가장 이른 날짜와 시각. 이보다 이른 값이 있으면 폼이 유효하지 않습니다.

maxTemporal.PlainDateTime

고를 수 있는 가장 늦은 날짜와 시각. 이보다 늦은 값이 있으면 폼이 유효하지 않습니다.

requiredboolean

날짜와 시각이 없으면 폼이 유효하지 않습니다.

isDateDisabled(date: Temporal.PlainDate) => boolean

공휴일처럼 고를 수 없는 날짜에 true를 돌려줍니다.

firstDayOfWeek1 | 2 | 3 | 4 | 5 | 6 | 7

달력의 첫 열, 1(월요일)부터 7(일요일)까지. 없으면 브라우저가 알려 주는 로캘의 첫 요일, 그것도 없으면 7.

stepnumber

자정부터 센, 목록의 시각 사이 간격(분).

기본값: 30

localestring

버튼 텍스트, 달력의 이름, 시각의 로캘, 예: "ko". 없으면 브라우저의 로캘이므로, 서버에서 렌더링할 때는 넘기세요.

placeholderstring

showValue가 true일 때, 날짜와 시각이 없는 동안 버튼에 보입니다.

showValueboolean

버튼이 글리프 옆에 날짜와 시각이나 placeholder를 보여 줄지 여부. 날짜와 시각을 보여 주는 텍스트 입력 옆에서는 false로 두세요.

기본값: true

openboolean

팝오버의 제어 열림 상태. onOpenChange와 함께 전달하세요.

defaultOpenboolean

비제어일 때 처음 열림 상태.

onOpenChange(open: boolean, reason: 'escape' | 'outside' | 'trigger' | 'select' | 'disabled') => void

팝오버가 열리거나 닫힐 때 호출되며, 닫힌 이유를 함께 받습니다.

formatDateTime props
이름타입설명
value 필수Temporal.PlainDateTime

쓸 날짜와 시간.

options.localestring

ko 같은 로캘. 지정하지 않으면 런타임의 로캘입니다.

parseDateTime props
이름타입설명
text 필수string

입력된 텍스트.

options.localestring

ko 같은 로캘. 지정하지 않으면 런타임의 로캘입니다.