Date and time input
Choose calendar dates and wall-clock windows with explicit bounds, identity and recovery. Keep civil values distinct from instants.
Choose the temporal model first
| Task | Value and interface |
|---|---|
| Calendar date | A birthday, collection date or date-only filter is a calendar value. Use Input type=date and retain its normalized date string. Do not introduce the viewer’s zone by parsing it into an instant. |
| Time of day | A daily opening window or repeated wall-clock time is a civil time. Use Input type=time; state precision, limits and whether a window crosses midnight. |
| Local date and time | Input type=datetime-local combines civil date/time without a zone. Use when the task needs that combination and provide zone/disambiguation separately if scheduling an instant. |
| Instant | A unique point on the timeline needs an offset/zone-aware domain conversion. Resolve nonexistent/repeated local times through an explicit policy and show the result before confirming. |
| Date range | Expose labelled beginning and end, specify inclusivity, validate ordering and retain both drafts when correction is needed. |
| Visual calendar or clock | Native picker chrome depends on browser/OS. A custom calendar/clock requires a separate tested interaction model; this package does not export a Material calendar grid, clock dial or date-range popup. |
Web equivalent and anatomy
Material’s date and time picker families map here to native web input compositions, using the existing styled Input. The example includes actual range and window filtering, error focus, pending outcomes and empty recovery. It does not impersonate a Material picker or add an unimplemented package API.
| Part | Specification |
|---|---|
| Persistent label | Separate named first/last dates and earliest/latest times. Do not rely on locale placeholders or an unlabeled calendar glyph. |
| Editable control | Native type, required/name, min/max/step, value/onChange and HTMLInputElement ref. Minimum 44px height, full available width with min-width:0; browser owns internal segments and picker chrome. |
| Supporting text | State calendar bounds, increment, zone meaning, interval endpoints and midnight policy before input. Connect it with aria-describedby. |
| Validation | Visible linked error with aria-invalid on its field; move focus to the first invalid control. Keep the loaded result and useful values intact. |
| Applied result | Show normalized dates, times and named zone alongside actual records. Separate draft, requested and applied state. |
| Actions | Explicit Apply, Clear, external Restore and surviving completion/failure/cancellation. Actions wrap within narrow containers with at least 44px targets. |
| Direction | Logical layout and bdi isolate displayed temporal values. Native segment order follows browser/locale; never reverse date strings manually. |
Material date entry, calendar views and confirmation
The reference distinguishes docked calendar entry, modal calendar and modal input, with single-date, range and year views. Near dates may benefit from a calendar; distant dates such as a birthday need efficient typed or month/year entry. Compact native full-screen and larger docked arrangements are reference choices, not browser chrome supplied by this package.
The captured usage text says tapping the field opens a docked calendar, while its accessibility chapter makes the calendar icon the exclusive calendar entry. Preserve independently useful manual input and a deliberate optional launcher rather than making every text-entry focus open a custom modal. This lab uses native date controls and does not add a custom calendar launcher or claim its OS popup appearance.
| Reference decision | Local web contract and application choice |
|---|---|
| Date identity and selection | Distinguish today, selected day, disabled dates, and range endpoints/interior. Local applied dates are normalized civil strings; input drafts, requested windows and actual filtered records have separate owners. Browser/OS owns any calendar highlight, year/month menu, locale order and picker dismissal. |
| Entry and formatting | Native inputs provide their browser-supported editing format and serialize normalized values. The reference encourages flexible separators and formatting after completion, without masks that rewrite during typing. This example does not advertise an arbitrary locale-text parser; a custom text adapter must declare accepted formats and preserve partially entered drafts. |
| Anatomy, targets and contrast | Native reference targets are 48dp with date text contrast of at least 4.5:1. Its month/year/day menus and full-screen measurements are not local CSS presets. The local labelled fields have 44px minimum height, explicit Gregorian bounds and instructions; test actual internal native segments and OS picker targets separately. |
| Keyboard and naming | A custom calendar needs fully named dates, weekday headers, a grid, selected/today/disabled state and tested month/year navigation. The source keyboard table duplicates Enter and gives Home/End the same description; use the actual installed interaction rather than copying that defect. Native input editing keys stay native here, with persistent labels and connected help. |
| Clear and shortcuts | Keep optional clear and shortcut affordances only when they have real effects. The local Clear empties all drafts, preserves loaded results and focuses the first date; Restore is independent. No unimplemented month/year shortcut is advertised. |
| Adaptation and dismissal | Native range views can use vertical month browsing, and compact full-screen adds explicit close/save; the reference also warns against simply scaling fixed picker geometry across breakpoints. Web native controls remain bounded fields and delegate picker internals to the platform. A custom modal would need real confirm/cancel, retained draft, background/focus policy and viewport tests. |
Material time formats, dial alternatives and precision
The reference has dial and input variants, with 12-hour period selection or 24-hour time without a period control. It recommends manual entry from every dial view, and input-only presentation when a dial cannot fit. Native format preference normally comes from settings outside the picker. This web fixture uses native time inputs; it supplies no clock face, custom AM/PM radio or native orientation transition.
| Reference decision | Local web contract and application choice |
|---|---|
| Anatomy and native dimensions | The reference separates hour/minute fields, period selector, dial, mode switch and confirm/cancel. Examples include 256dp dial,48dp handle,24dp padding,80dp dial-selector height and 72dp input-selector height. These are native design references; local input dimensions and precision are the actual contract above. |
| Constrained viewport | Switch native orientation or fall back to text rather than compressing dial targets or forcing a scrolling clock face. This lab keeps independently labelled full-width fields and wrapping actions. Native picker layout and mobile keyboards require target-device checks; a DOM screenshot cannot certify them. |
| Precision and purpose | The usage chapter discourages millisecond-level selection while accessibility mentions finer precision in some cases. The local policy is explicitly quarter-hour civil windows, not stopwatch timing. Seconds, overnight windows and instant booking need deliberate supported models, validation and downstream precision. |
| Roles and keyboard | Reference SDKs differ on radio versus checkbox period semantics and dial roles. Keep actual native web time input semantics and ordinary Tab order; never imitate a dial keyboard table with global arrow handlers. Visible instructions and normalized payload remain available independently of picker chrome. |
| Commit and time-zone meaning | The reference separates temporary selection from OK/Cancel. Here Apply snapshots actual native form entries; only explicit completion changes records, and failure/cancel retain previous results. A wall-clock value plus zone still needs a maintained DST/disambiguation service before it can become a booked instant. |
Working civil-time window
This English-only page-memory dataset holds civil records in a named zone. The applied inclusive date and daily time windows filter real records. Native FormData captures normalized values; the zone is validated/canonicalized by the runtime’s Intl.DateTimeFormat. It identifies civil records, without converting them into instants.
Dates are bounded to 2026–2028 and times to 09:00–17:00 in 15-minute increments. Ordering is validated in addition to native constraints. Failure/cancellation preserve loaded records; only explicit completion applies the request. An empty result has working restore/apply recovery. No remote availability, durable booking, permission or cultural source authority is implied.
Inclusive calendar dates, 2026–2028. Daily inclusive wall-clock window, 09:00–17:00 in 15-minute steps. The zone identifies matching civil records; these values do not identify an instant.
Loaded civil-time records
2026-10-13 through 2026-10-15; 09:00 through 17:00; Australia/Brisbane
- English-only reading session: 2026-10-13 09:00 Australia/Brisbane
- English-only note session: 2026-10-14 12:00 Australia/Brisbane
- English-only example session: 2026-10-15 16:30 Australia/Brisbane
Actual native entries: No native form submission.
Initial civil-time window loaded. No instant conversion or remote request.
'use client';
import {useRef,useState,type CSSProperties} from 'react';
import {Button,Input} from '@mobtranslate/ui';
type WindowDraft={start:string;end:string;from:string;until:string;zone:string};
const initial:WindowDraft={start:'2026-10-13',end:'2026-10-15',from:'09:00',until:'17:00',zone:'Australia/Brisbane'};
const records=[{id:'reading',label:'English-only reading session',date:'2026-10-13',time:'09:00',zone:'Australia/Brisbane'},{id:'notes',label:'English-only note session',date:'2026-10-14',time:'12:00',zone:'Australia/Brisbane'},{id:'examples',label:'English-only example session',date:'2026-10-15',time:'16:30',zone:'Australia/Brisbane'}];
const action:CSSProperties={minWidth:0,maxWidth:'100%',minHeight:44,height:'auto',whiteSpace:'normal',overflowWrap:'anywhere'};
function canonicalZone(zone:string){return new Intl.DateTimeFormat('en',{timeZone:zone}).resolvedOptions().timeZone;}
export function DateTimeWindowExample(){
const [draft,setDraft]=useState(initial);
const [applied,setApplied]=useState(initial);
const [requested,setRequested]=useState<WindowDraft | null>(null);
const [disabled,setDisabled]=useState(false);
const [readOnly,setReadOnly]=useState(false);
const [rtl,setRtl]=useState(false);
const [error,setError]=useState<{field:keyof WindowDraft;message:string} | null>(null);
const [payload,setPayload]=useState('No native form submission.');
const [status,setStatus]=useState('Initial civil-time window loaded. No instant conversion or remote request.');
const form=useRef<HTMLFormElement>(null);
const heading=useRef<HTMLHeadingElement>(null);
const pending=requested!==null;
const visible=records.filter(record=>record.date>=applied.start && record.date<=applied.end && record.time>=applied.from && record.time<=applied.until && canonicalZone(record.zone)===applied.zone);
function change(field:keyof WindowDraft,value:string){setDraft(current=>({...current,[field]:value}));setError(null);}
function reject(field:keyof WindowDraft,message:string){setError({field,message});(form.current?.elements.namedItem(field) as HTMLInputElement)?.focus();}
function outcome(result:'complete'|'fail'|'cancel'){
if(!requested)return;
if(result==='complete'){setApplied(requested);setStatus('Actual local civil-time records filtered. No durable save or instant conversion.');}
else setStatus(`Local window ${result==='fail'?'failed':'cancelled'}. Draft values and loaded records retained; submit again to retry.`);
setRequested(null);heading.current?.focus();
}
const fields=[{key:'start' as const,label:'First calendar date',type:'date',min:'2026-01-01',max:'2028-12-31',step:1},{key:'end' as const,label:'Last calendar date',type:'date',min:'2026-01-01',max:'2028-12-31',step:1},{key:'from' as const,label:'Earliest wall-clock time',type:'time',min:'09:00',max:'17:00',step:900},{key:'until' as const,label:'Latest wall-clock time',type:'time',min:'09:00',max:'17:00',step:900}];
return <div data-date-time-lab className="space-y-4" dir={rtl?'rtl':'ltr'}>
<div className="flex flex-wrap gap-3"><Button type="button" style={action} variant="outline" disabled={pending} aria-pressed={disabled} onClick={()=>setDisabled(v=>!v)}>Disable civil-time fields</Button><Button type="button" style={action} variant="outline" disabled={pending} aria-pressed={readOnly} onClick={()=>setReadOnly(v=>!v)}>Make civil-time fields read only</Button><Button type="button" style={action} variant="outline" aria-pressed={rtl} onClick={()=>setRtl(v=>!v)}>Use RTL civil-time layout</Button><Button type="button" style={action} variant="outline" disabled={pending} onClick={()=>{setDraft(initial);setDisabled(false);setReadOnly(false);setError(null);}}>Restore initial civil-time draft</Button></div>
<p id="date-time-help">Inclusive calendar dates, 2026–2028. Daily inclusive wall-clock window, 09:00–17:00 in 15-minute steps. The zone identifies matching civil records; these values do not identify an instant.</p>
<form ref={form} noValidate className="space-y-4" onSubmit={event=>{
event.preventDefault();if(pending||disabled)return;
for(const field of fields){const input=event.currentTarget.elements.namedItem(field.key) as HTMLInputElement;if(!input.value || !input.validity.valid){reject(field.key,`${field.label}: enter a valid value within the stated bounds and increment.`);return;}}
if(draft.end<draft.start){reject('end','Last calendar date must be on or after the first date.');return;}
if(draft.until<draft.from){reject('until','Latest wall-clock time must be on or after the earliest time. This daily window does not cross midnight.');return;}
let zone:string;try{if(!draft.zone.trim())throw new Error('Missing zone');zone=canonicalZone(draft.zone.trim());}catch{reject('zone','Enter a time-zone identifier supported by this runtime, such as Australia/Brisbane.');return;}
setPayload(JSON.stringify([...new FormData(event.currentTarget).entries()]));setRequested({...draft,zone});setError(null);setStatus('Civil-time window requested; loaded records remain until explicit completion.');heading.current?.focus();
}}>
{fields.map(field=><div key={field.key}><label htmlFor={`civil-${field.key}`} className="block font-semibold mb-2">{field.label}</label><Input id={`civil-${field.key}`} name={field.key} type={field.type} required min={field.min} max={field.max} step={field.step} value={draft[field.key]} onChange={event=>change(field.key,event.target.value)} disabled={disabled} readOnly={readOnly||pending} aria-describedby={error?.field===field.key?'date-time-help date-time-error':'date-time-help'} aria-invalid={error?.field===field.key} style={{minWidth:0,maxWidth:'100%',minHeight:44}} /></div>)}
<div><label htmlFor="civil-zone" className="block font-semibold mb-2">Civil record time zone</label><Input id="civil-zone" name="zone" required value={draft.zone} onChange={event=>change('zone',event.target.value)} disabled={disabled} readOnly={readOnly||pending} aria-describedby={error?.field==='zone'?'date-time-help date-time-error':'date-time-help'} aria-invalid={error?.field==='zone'} style={{minWidth:0,maxWidth:'100%',minHeight:44}} /></div>
{error && <p role="alert" id="date-time-error">{error.message}</p>}
<div className="flex flex-wrap gap-3"><Button type="submit" style={action} disabled={disabled||pending}>Apply civil-time window</Button><Button type="button" style={action} variant="outline" disabled={disabled||readOnly||pending} onClick={()=>{setDraft({start:'',end:'',from:'',until:'',zone:''});setError(null);(form.current?.elements.namedItem('start') as HTMLInputElement)?.focus();}}>Clear civil-time draft</Button></div>
</form>
<h3 ref={heading} tabIndex={-1} data-date-time-heading className="font-semibold">Loaded civil-time records</h3><p dir="auto" data-date-time-applied style={{overflowWrap:'anywhere'}}><bdi>{applied.start}</bdi> through <bdi>{applied.end}</bdi>; <bdi>{applied.from}</bdi> through <bdi>{applied.until}</bdi>; <bdi>{applied.zone}</bdi></p>
<ul data-date-time-records className="guidance-list">{visible.map(record=><li key={record.id} data-record-id={record.id}>{record.label}: <bdi>{record.date} {record.time} {record.zone}</bdi></li>)}</ul>
{!visible.length && <p data-date-time-empty>No local civil records match. Restore the initial draft and apply it to recover.</p>}
<p data-date-time-payload style={{overflowWrap:'anywhere'}}>Actual native entries: {payload}</p>
{pending && <div className="flex flex-wrap gap-3"><Button type="button" style={action} onClick={()=>outcome('complete')}>Complete civil-time request</Button><Button type="button" style={action} variant="outline" onClick={()=>outcome('fail')}>Fail civil-time request</Button><Button type="button" style={action} variant="outline" onClick={()=>outcome('cancel')}>Cancel civil-time request</Button></div>}
<p role="status" data-date-time-status>{status}</p>
</div>;
}
Input API and form ownership
| Contract | Use |
|---|---|
| type | date, time or datetime-local select the native temporal control. Keep the task model explicit. |
| value / onChange | Controlled draft strings; reflect the actual input value. Empty string is a real draft state. Do not switch between controlled and uncontrolled ownership. |
| defaultValue | Uncontrolled initial value; later prop changes do not constitute an external reset. |
| name / FormData | Serialize successful native controls. Disabled fields are omitted; read-only fields remain registered. The example captures entries before entering pending state. |
| required / min / max / step | Native temporal constraints. The example calls native validity and also validates pair ordering. step is days for date and seconds for time/datetime-local. |
| readOnly / disabled | Pending uses read-only fields so values remain accessible and submitted; duplicate Apply is unavailable. An independent Restore can recover deliberately unavailable fields. |
| state / aria-invalid | Input state=error supplies visual styling; aria-invalid and described error convey field semantics. Validation copy must identify how to fix the value. |
| ref / id / aria-describedby | Ref reaches the native input; labels/descriptions/errors stay associated with the real control. |
| Custom picker boundary | No showPicker wrapper or custom grid is promised. A product that adds a picker launcher must test support, user activation, read-only/disabled behavior, naming and focus recovery. |
Calendar validation and precision
- Use the browser’s temporal parser/validity for native drafts, including leap days, out-of-range values and step mismatch. Validate incoming server/URL values through the domain model too; native controls can sanitize invalid strings to empty.
- The fixture compares normalized fixed-width in-range calendar/time strings after native validation. That ordering is specific to these Gregorian values, not a locale-text parser or a general historical-calendar algorithm.
- State whether endpoints are inclusive. This fixture applies its time window to each date independently; it is not a continuous instant interval from the first morning to the last evening.
- Reversed date/time pairs produce focused errors instead of silently swapping user intent. The fixture disallows midnight-crossing windows; overnight schedules need an explicit next-day model.
- Choose meaningful precision. A quarter-hour increment is a fixture policy, not a universal appointment rule. If seconds are meaningful, expose them and preserve that precision through storage.
- Do not enforce a frozen “today” or minimum age through a build-time constant. When the product needs relative limits, calculate them in its authoritative clock/zone and describe the boundary.
Zones, daylight saving and display
- A date-only value has no time-zone offset. A civil date/time plus a zone is still not always a unique instant: a transition can repeat or remove local clock times.
- Before booking an instant, resolve the selected civil values with a maintained zone-aware domain service. Require a deliberate repeated-time choice and explain nonexistent-time correction; never silently shift an appointment.
- Persist the task’s actual model: date-only strings for calendar facts, civil fields/zone for recurring schedules, and a resolved instant plus relevant zone/intent for a confirmed one-off event.
- The lab validates runtime-supported zone identifiers with Intl and uses the canonical result for matching. It does not manually maintain an alias or offset map. Unsupported zone input produces a recoverable field error.
- Intl formatting localizes an existing instant; it does not by itself interpret an arbitrary local date/time as an instant. Never derive that conversion from a formatted string.
- Native displayed segment order, month names and 12/24-hour presentation may differ from serialized values. Keep labels readable, display the zone near a scheduled result and test the actual target locales/calendars/devices.
Keyboard, accessibility and responsive behavior
- Use persistent native labels and ordinary document Tab order. Native segments and picker interactions are browser/OS responsibilities; do not replace arrow keys with a global calendar handler.
- Keep validation instructions available before submit. On error, focus the actual invalid field and retain useful sibling values. Pending and applied statuses live outside unavailable controls.
- Native picker appearance is not fully styled by these tokens. Test high contrast, zoom, date-segment names and chosen browser/assistive-technology pairs; DOM validity alone does not prove spoken behavior.
- At narrow widths, each labelled field occupies its own row and actions wrap. Check native intrinsic width rather than assuming width:100% prevents overflow.
- Show calendar values in isolated text and let locale/browser semantics govern entry. RTL layout does not imply reversing ISO serialization or a time-zone identifier.
- For a custom grid, separately implement named month/year navigation, roving day focus, selected/today/disabled distinctions, keyboard month/year movement and modal focus restoration. For a custom clock, supply a text/keyboard alternative to dragging. Test those implementations before documenting them as available.
States, recovery and acceptance evidence
| State | Verify |
|---|---|
| Empty / invalid | Clear really empties drafts; required correction focuses the first field. Impossible dates, boundaries and increments are rejected by actual controls/domain checks. |
| Reversed range / window | Focus the ending field, explain the ordering and preserve the loaded records. |
| Unsupported zone | A connected error explains the expected identifier; no partial request or new result. |
| Read-only / disabled | Editing is unavailable; native entries distinguish retained values from omission. External restoration remains reachable. |
| Pending / failed / cancelled | Snapshot actual values, reject duplicates, keep loaded content, offer explicit surviving outcomes and retry. |
| Completed / empty result | Change actual record content only on completion. Explain zero results and provide working recovery. |
| Locale / DST / device | Test leap boundaries, time precision, midnight, repeated/nonexistent times, locale formats, hardware pickers, spoken labels and service-side scheduling policy independently. |
Verification and primary references
Run test:date-time plus checked-source/token freshness, lint, types, production build and the full route/fragment crawl. The fixture’s browser checks verify native values, constraints, ordering, zone recovery, request outcomes and reflow; they do not certify OS picker chrome, every locale or an instant-booking service.
- HTML defines the native date/time states, normalized values and constraints. ECMA-402 defines runtime Intl formatting and time-zone options. Follow the actual installed web behavior alongside these standards.