date-field: opt out of value→placeholder sync in range pickers

Root cause of date-range-picker incidencia 2026-05-20 #1 (auto-paging
to next month when the popup opens with a complete range): both
endpoint inputs of a DateRangeField create their own DateFieldProvider
sharing a single placeholder. Each provider has a `$effect` that
mirrors value → placeholder so the calendar jumps to the value's
month. With two endpoints, the END field's effect overrides the
START's on every render and the popup auto-pages to the end's month.

Fix: add an explicit `syncPlaceholderToValue?: boolean` opt on
`DateFieldOpts` (default `true` — preserves single-field UX). The
DateRangeField endpoint Input passes `false`; range placeholder
coordination stays with the range provider.

Verified in browser: value `{ start: 2026-05-31, end: 2026-06-09 }`
with `placeholder = 2026-05-31` now keeps the calendar on
"May 2026 – June 2026" instead of jumping to "June – July". Both
endpoints render correctly with the start/end stripes.

The earlier `reanchorInitialSelection` removal handled the same
symptom inside the range-calendar provider for fresh selections;
this commit handles the OTHER source — the field provider auto-sync
on already-set values.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent 5f8adbeca6
commit fb47f0475b

@ -168,7 +168,21 @@ export interface DateFieldOpts
hideTimeZone: boolean;
required: boolean;
errorMessageId: string | undefined;
}> {}
}> {
/**
* When `true` (default) the field mirrors any `value` change back into
* `placeholder` so the calendar (or any sibling that consumes
* `placeholder`) jumps to the month of the entered value. Standalone
* date fields and single-date pickers want this.
*
* Range pickers MUST pass `false` for their two endpoint fields, because
* both fields share a single `placeholder` and would fight: the END
* field would override the START field on every render, auto-paging the
* popup to the END's month (incidencia 2026-05-20 #1). Range placeholder
* coordination lives in the range provider.
*/
syncPlaceholderToValue?: boolean;
}
export class DateFieldProvider {
readonly opts: DateFieldOpts;
@ -266,13 +280,20 @@ export class DateFieldProvider {
target: opts.ref?.current
});
}
const placeholder = untrack(() => this.opts.placeholder.current);
if (this.opts.value.current && placeholder !== this.opts.value.current) {
untrack(() => {
if (this.opts.value.current) {
this.opts.placeholder.current = this.opts.value.current;
}
});
// Mirror value → placeholder for single-field cases. Range pickers
// opt out via `syncPlaceholderToValue: false` (incidencia 2026-05-20
// #1) because their two endpoint fields share one placeholder and
// would fight: the end input would auto-page the popup to its own
// month on every render.
if (this.opts.syncPlaceholderToValue !== false) {
const placeholder = untrack(() => this.opts.placeholder.current);
if (this.opts.value.current && placeholder !== this.opts.value.current) {
untrack(() => {
if (this.opts.value.current) {
this.opts.placeholder.current = this.opts.value.current;
}
});
}
}
});

@ -59,6 +59,11 @@
}
),
placeholder: provider.opts.placeholder,
// Range endpoints share a single placeholder owned by the range
// provider; do NOT let each endpoint mirror its own value back into
// placeholder. Otherwise the end input continuously overrides the
// start input's anchor and the popup auto-pages to the end month.
syncPlaceholderToValue: false,
validate: readableActive(() => undefined), // Range-level validation lives on the parent
onInvalid: readableActive(() => undefined),
minValue: provider.opts.minValue,

Loading…
Cancel
Save

Powered by TurnKey Linux.