feat(date-field): integrate date controls with Field wiring - #392
feat(date-field): integrate date controls with Field wiring#392IzumiSy wants to merge 18 commits into
Conversation
045166b to
ccdac38
Compare
interacsean
left a comment
There was a problem hiding this comment.
Great to start to align the interface with the rest of the form-based components we have already and with RHF. We should also bring updates to the Calendar component to keep them all aligned.
Ontop of this, I am concerned about whether we should be maintaining backwards compatibility at all and give future deprecation warnings, or try to survey if any of these components are in use already if we want to make a breaking change
- The field is not working within a Form. In the example page, there is a form to test this functionality and it has broken... AI analysis says:
"The refactor registers the date control as a Base UI form field via the internal useRegisterFieldControl. Base UI's Form.onSubmit only calls your onSubmit/onFormSubmit when zero registered fields are invalid; otherwise it preventDefault()s and focuses the first "invalid" field (Form.js, the if (invalidFields.length) branch). A field's validity is !invalid && validityData.state.valid, and validityData.state.valid starts at null and is only committed via queueMicrotask, gated behind shouldValidateOnChange() (date-field.tsx:156). Under the default validationMode="onSubmit" it never resolves to true, so the date field is permanently counted invalid"
A typed out-of-range / unavailable date shows a red border but no message, the control does its own validation internally but never reports that validity back up to Field.Root.
- Existing form components have { readOnly?: boolean, disabled?: boolean }
mode="editable" | "readonly" | "disabled" does not align, and should become two independent booleans
disabled and readOnly are orthogonal, not one axis. In HTML they mean different things — a disabled field isn't focusable, isn't submitted, and isn't validated; a read-only field is focusable, is submitted, and is validated
It breaks native/RHF interop. register() and a spread {...field} set disabled as a boolean; Base UI's own Field.Root cascades disabled as a boolean. With mode, every integration point needs a translation layer — and it's already caused a bug in this PR: Field.Root disabled flows to the group but not to calState/the state hook (they only read mode === "disabled")
-
Re constraints. Again { required?: boolean } is the existing convention, not nested. And we could shift min|maxValue to just min|max top-level props, which is how a native number-based input expects this type of range restriction. However we would be passing objects not strings so perhaps retaining Value is valid
-
There are direct imports from @base-ui/react/internals/* which is brittle as they are not semver protected, and sounds like it is linked to the form no longer submitting
5. We lost a few tests related to typing/auto-advance, day-clamping, leap years etc. Is there rationale / scope decision to this?
|
Thanks for your feedback. I should have separated the intentional API break from the regressions in the current branch more clearly. The intentional break in this PR is the composition model: I want That said, I agree that some of what landed here should not be part of that break. In particular, I'm going to keep For that reason, I'm treating
Separately, I agree the current branch still has regressions that I need to fix regardless of the API direction: the Base UI If that direction sounds reasonable, I'll narrow the PR to that shape. |
|
/review |
|
✅ API Design Review completed successfully! API Design Review complete for PR #392 (feat(date-field): integrate date controls with Field wiring). Summary: Impact analysis confirms all 3 changed Findings (Low only — no High/Medium issues): [1/3 — Low] [2/3 — Low] [3/3 — Low] Verdict: Approve |
|
|
Motivation
DateFieldandDatePickerhad drifted into an awkward spot within AppShell's form API.On one side, they are AppShell-owned composite widgets: segmented spinbuttons, custom keyboard behavior, an optional calendar popover, and a proxy input for form submission/validity. On the other side, consumers reasonably expect them to behave like the rest of AppShell's form controls inside
Field.Root— especially becauseInput,Select,Combobox, and related controls already do.Keeping the date controls standalone-only meant every consumer had to manually wire:
aria-labelledby/aria-describedbyThat made the date controls feel inconsistent with the rest of the library and forced the example page to carry a custom
DemoFieldwrapper just to recover the usual field UX.At the same time, we do not want to re-implement all of Base UI's field/form machinery from scratch, and we do not want the coupling to sprawl across the date control implementation.
Design Decision
Use Base UI's exported internals as a narrow bridge
Base UI does not currently provide a first-class public adapter for registering an arbitrary composite widget as a
Fieldcontrol, but it does export the internal building blocks needed to do so via package subpaths such as:@base-ui/react/internals/field-register-control@base-ui/react/internals/field-root-context@base-ui/react/internals/labelable-provider@base-ui/react/internals/form-contextThis PR intentionally depends on those internals for now because they match the behavior we want and let us support normal
Fieldcomposition without re-implementing form field management ourselves yet.The bridge is intentionally centralized in a single hook,
useDateFieldFieldBridge, so the dependency surface stays small and maintainable if we replace this approach later.Keep the date widgets as composite widgets
The date controls do not become thin wrappers around Base UI field controls.
They still own:
DateValueemissionThe bridge only adapts them to field-shell concerns:
Intentionally reset the prop surface
This PR intentionally changes the
DateField/DatePickerprop contract relative tomain.The previous component-owned
label,description,errorMessage, andhideTimeZoneprops are removed in favor of two explicit usage modes:id,aria-label,aria-labelledby, andaria-describedbyField.RootwithField.Label,Field.Description, andField.ErrorThis is a deliberate breaking change, but acceptable for now because these date controls do not yet have known external consumers and are only used internally by us at this stage.
The goal of this reset is to align the date controls with the rest of AppShell's form model instead of carrying a parallel label/error API indefinitely.
Preserve standalone usage
The bridge is additive at the behavior level.
Outside
Field.Root/Form, Base UI's exported internals resolve to inert default contexts, soDateFieldandDatePickercontinue to work as standalone controls with explicit ARIA wiring.That means consumers still keep a valid standalone path, even though this PR does not preserve full prop-level backward compatibility with
main.Summary
DateField/DatePickerwithField.Rootusing Base UI's exported internalsuseDateFieldFieldBridgeField.*compositionField/FormField.*composition