Usage
packages/ui/docs/decisions/0021-single-choice-control.md. Keep the presets to the few durations people pick most and let the amount cover the rest. DurationField shows at most five presets: more is a type error, and in development a longer list warns once and renders its first five.{ amount, unit } or null when the amount is empty. It follows the usual contract: value for controlled, defaultValue for uncontrolled, and onValueChange(value, details) for every change. details.part is amount, unit, preset or form, and details.reason is the control's own. Every change is cancelable except a native form reset.
durationToSeconds and durationToMilliseconds turn a value into a number for a timer or a request. 24 hours and 1 day convert to the same seconds but are two different values: a preset selects only when both the amount and the unit match.
units lists one to five units with the labels your product uses. Two to five render as a segmented choice beside the amount, because short labels of similar length are a SegmentedControl under ADR 0021. One unit renders as plain text beside the amount and no choice. Select is not part of the system yet, so a unit list that would need one (more than five, or labels that vary widely) is not supported.
Picking a unit keeps the amount and clamps it into the new unit's range. Picking a unit while the amount is empty changes only the shown unit and reports nothing, because the value is still null. defaultUnit sets the unit shown while empty; it defaults to the default value's unit, then the first of units.
A preset sets the amount and the unit together. The preset that equals the value is selected, so the field opens with the default selected when defaultValue names it. Nothing marks a default with an icon or a star. Editing the amount or the unit changes the value, so the selection moves to the preset that matches, or clears when none does.
Keep preset labels short and alike (3 hrs, 12 hrs, 1 day). Long or uneven labels fail the fit test of ADR 0021 and belong in a different control.
Off is not a duration. When the change applies on its own, put a Switch beside the field and disable the field while it is off. Inside a form that needs Save, use a Checkbox. See the Composition stories. The field itself has no none preset and no hidden segment.
min and max take either a duration (an overall limit) or a number per unit.
max={{ amount: 7, unit: DurationUnit.Days }} limits the field to a week in whichever unit is shown: 168 hours, 10,080 minutes.max={{ [DurationUnit.Hours]: 12, [DurationUnit.Days]: 3 }} limits hours and days separately and leaves minutes unbounded.0, so a zero duration is valid unless min says otherwise.
name posts the amount and unitName posts the shown unit, both through hidden inputs; form reaches both. required blocks submit through the amount. A form reset restores defaultValue and its unit and calls onValueChange with reason form-reset. A draft the user is still typing commits before the form posts.
The unit and preset choices are SegmentedControls with no name, so they post nothing of their own; they reach the form through the value. A form reset reports one change for the whole field.
The label sits 8px above the controls, and the rows are 8px apart. The amount and the unit share a row and wrap when the container is narrow; the presets follow on their own row. The field fills its container; constrain it with a class in @layer chroma.overrides.
size is InputSize and defaults to Large (32px). The unit and preset choices take the matching height.