ADR 0116: One clock in every renderer
Status: Accepted — amended, see Amendment
Context
Section titled “Context”renderTimepickerField took format = "12h" as a parameter default, and fields/index.ts passes
undefined for it — so that default is what every document-driven Plain timepicker gets. A document
cannot ask for the other one: mode in spec/dynamic-form-v3.schema.json is multiselect-only,
enum: ["single","multi"], and no field member carries a clock format.
So editing that parameter default was the only way to get a 24-hour picker out of Plain at all. It was edited, and the four tests that broke were asserting the old default rather than a property.
The three renderers had each written the default down for themselves:
packages/plain renderTimepickerField(..., format = "12h")packages/angular format = input<MdyTimeFormat>("12h")packages/lit this.format = "12h"Three copies of one decision, which is the shape that lets one document render a different clock in each adapter — the divergence a shared contract exists to prevent.
Decision
Section titled “Decision”Every renderer defaults to the 24-hour clock. A host that wants the other passes format: "12h",
which every renderer already accepts.
24 rather than 12 because of what a default is for. A document cannot yet name a format, so the default is the only clock a document-driven form can get; the 24-hour clock is the one that names every hour it draws without a second control, and the one most of the world writes. A 12-hour picker needs its AM/PM control to be reachable to be usable at all, which is a second thing that has to be right.
Consequences
Section titled “Consequences”A behaviour change for every consumer of all three renderers. A form that showed 02:30 PM now
shows 14:30 unless it asks otherwise. The migration is one option: format: "12h" in Plain,
[format]="'12h'" in Angular, format="12h" in Lit.
Four tests in Plain and four in Lit were rewritten rather than fixed: 13 is a valid hour now, so
“an hour past 12 is marked invalid” was asserting the default. What survives is the property in
24-hour terms — an hour past 23 is marked invalid, the arrows wrap at 23 → 00, the segments advertise
0–23, clearing is not an error.
The gap this does not close: a document still cannot ask for either format. With 24-hour as the default the common case works, and the declarability gap is recorded as its own finding rather than answered by adding a schema member alongside a release.
Alternatives rejected
Section titled “Alternatives rejected”Plain only. It was Plain’s default that was changed and Plain’s wall that was hit. But a default that differs between adapters means one document renders a different clock in each of them, and the next reader finds three answers to one question with nothing saying which is right.
Keep 12-hour and make the format declarable first. The right long-term shape and the wrong order: it leaves the only reachable default as the one the reporter could not use, and adding a contract member is a batch of its own.
No default — require the host to choose. Turns every existing call site into a compile error to answer a question most of them do not care about.
Verification
Section titled “Verification”packages/plain/test/timepicker-bounds.test.mjsandpackages/lit/test/timepicker-bounds.test.mjs— the segment bounds, the invalid entry, the wrap and the clearing, all in 24-hour terms.packages/angular/src/lib/renderers/timepicker/— the renderer suite, which drives the segments and the dial through the shared contract.
Security and privacy
Section titled “Security and privacy”None. No boundary moves and no value leaves the process differently; a clock format is what a person reads.
Amendment, 2026-08-21: a document can name its clock
Section titled “Amendment, 2026-08-21: a document can name its clock”The gap this record left open — a document still cannot ask for either format — is closed.
MdyDynamicDateField gains format?: MdyTimeFormat, timepicker only, absent meaning the 24-hour
clock the decision above chose. Nothing about that decision changes: the default is still 24-hour in
all three renderers, and a host that passes nothing still gets it.
What the slot removes is a class of form that could not be expressed at all. A document-driven form had exactly one clock available, so a schema meaning half past two in the afternoon could be written only where a hand-written host was there to pass the parameter — which is to say, not in Studio, not in a document from a server, and not in the dynamic renderer of any adapter.
Refused where it is written, following the granularity: a format on a kind that draws no clock, or
a value that is neither "12h" nor "24h", is reported and dropped, and the field stays and draws
the default. Dropping the refinement rather than the field keeps a control the person can see for the
sake of a rule they cannot.
The range follows the clock. The same amendment folds hourControl’s announced range onto
timeFieldBounds, which the native min/max already came from. It had been written twice, and the
copy in the accessibility projection was still the 12-hour one: a 24-hour face declared max="23" to
the browser and aria-valuemax="12" to a reader, so a screen reader was told the wrong bound on the
clock this record made the default. Two declarations of one range is the shape that lets the second
one go stale, and one of them was silent about it.
The renderer ceiling was raised, once, deliberately
Section titled “The renderer ceiling was raised, once, deliberately”renderer-overrun-baseline.json says its numbers may shrink and may not grow, and this amendment
raised the total from 691 to 707. The rule is right and the exception is narrow enough to name: the
Angular timepicker grew 91 lines across the batches that gave it a granularity, a ghost hand and a
keyboard. 75 of those came back — four duplicated segment handlers collapsed onto one, the widget
runtime’s plumbing moved beside the select adapter’s, set-time replacing a conversion two renderers
each wrote out, three comments rewritten from history into invariants.
The 16 that remain are the four inputs the timepicker gained, with the documentation this repository
requires of them. They are capability, not restatement, and the two ways to make the measurement
accept them — deleting the documentation, or moving the inline template into an .html the audit
does not count — are both worse than the number being 16 higher. The second is the one to refuse
loudest: the sibling clock component does keep its template in a file, so the move would look like
housekeeping while removing a hundred lines from a measurement without removing anything from the
renderer.
Verification
Section titled “Verification”packages/core/test/dynamic-diagnostics.test.mjs—MDY_DYNAMIC_UNHONOURABLE_FORMAT, a clock that is not one of the two.npm run test:type-surface—MdyDynamicDateField.format was added (optional), classified minor.- Measured in the browser across the three renderers: a picker declaring
format: "12h"reads2:30 PMand holds14:30, refuses14:30; a picker declaring nothing does the reverse. The hour box declaresmax/aria-valuemaxas23/23on the default clock and12/12on the other.
Security and privacy
Section titled “Security and privacy”None, for the reason the record already gives: a clock format is what a person reads, and the stored
value is HH:mm either way.