Skip to content

fix(bulma-ui): stop the date pickers at year 1 - #919

Merged
allxsmith merged 2 commits into
mainfrom
fix/902-year-floor
Oct 7, 2026
Merged

allxsmith merged 2 commits into
mainfrom
fix/902-year-floor

Conversation

@allxsmith

Copy link
Copy Markdown
Owner

HTML's date, month and datetime-local inputs hold no year below 1, but the date pickers accepted dates in year 0 and earlier. With no min, the calendar's year list reached far enough below the focused year that a picker focused on a year below 100 listed negative years. Segmented typing, the default parse and the time wheels committed such dates, which the native inputs then couldn't show and the YYYY token rendered with a minus sign inside its padding.

This bounds the pickers at year 1, as decided on the issue. A shared helper raises min to midnight on 1 January of year 1 when it's earlier or absent, and each path applies it the way it already applies min. The calendar disables the days before year 1, keeps keyboard focus and its previous-month and previous-year buttons from going past it, and starts its default year list there. DateInput, DateTimeInput and TimeInput reject a typed, stepped or parsed date before year 1, including one from a custom parse, and the time wheels leave a value dated before year 1 unchanged. An explicit min before year 1 counts as 1 January of year 1, including in the native date, month and datetime-local inputs' min attribute. The min TSDoc on all three pickers says so, and the DateInput and DateTimeInput docs pages mention it under Min and Max.

This is a narrow change to what the pickers accept: only year 0 and earlier move out of range. Years 1 to 99 stay in range as #890 made them, and its tests pass unchanged. The YYYY token keeps padding years 1 to 999 to four digits, now covered by a test.

Some things are left as they are. The native inputs' read-back needed no change: HTML's value format has no year 0 or negative year, so the browser empties such a value before the picker reads it, and a test covers that alongside year 1 reading back. A controlled value dated before year 1 still displays as given, the same way a value before min does; the pickers just won't commit one. TimeInput's native time input takes only a time of day, so its min attribute keeps the given min's time rather than the floor's.

pnpm all passes locally.

Fixes #902

HTML's date, month and datetime-local inputs hold no year below 1, but the pickers let a
date in year 0 or earlier through. With no min, the calendar's year list reached negative
years, and typing, the default parse and the time wheels committed such dates.

Every picker path now treats a date before year 1 the way it already treats one before min.
The calendar disables those days and starts its year list at year 1, typing and parsing
reject them, and a min before year 1 counts as 1 January of year 1, on the native inputs
too. TimeInput applies the same floor to the date its value carries. Years 1 to 99 stay in
range as written, and the YYYY token still pads them to four digits.
@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 46 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: allxsmith/bestax/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1eb135c9-590d-4b5a-bc13-8aba4c2f8654
📥 Commits

Reviewing files that changed from the base of the PR and between 2ce64e1 and 4f6eb18.

📒 Files selected for processing (18)
  • bestax-mcp/data/components/DateInput.json
  • bestax-mcp/data/components/DateTimeInput.json
  • bestax-mcp/data/components/TimeInput.json
  • bulma-ui/src/form/DateInputBase.tsx
  • bulma-ui/src/form/DateTimeInputBase.tsx
  • bulma-ui/src/form/TimeInputBase.tsx
  • bulma-ui/src/form/__tests__/Calendar.test.tsx
  • bulma-ui/src/form/__tests__/DateInput.test.tsx
  • bulma-ui/src/form/__tests__/DateTimeInput.test.tsx
  • bulma-ui/src/form/__tests__/TimeInput.test.tsx
  • bulma-ui/src/form/__tests__/dateUtils.test.ts
  • bulma-ui/src/form/__tests__/formatters.test.ts
  • bulma-ui/src/form/_pickerInternals/Calendar.tsx
  • bulma-ui/src/form/_pickerInternals/dateUtils.ts
  • bulma-ui/src/form/_pickerInternals/formatters.ts
  • docs/docs/api/form/datetime/dateinput.md
  • docs/docs/api/form/datetime/datetimeinput.md
  • docs/docs/api/form/datetime/timeinput.md
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://31296273.bestax.pages.dev

Comment thread bulma-ui/src/form/_pickerInternals/Calendar.tsx
Comment thread bulma-ui/src/form/DateInputBase.tsx

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep review — 0 blocking · 2 advisory

# Severity Area Finding Location
1 🔵 Advisory Correctness A max before year 1 leaves focusedDate in year 0, so the day header walks into negative years and the year dropdown renders empty bulma-ui/src/form/_pickerInternals/Calendar.tsx:467
2 🔵 Advisory Robustness The floor raises min but not toIsoValue, so the hidden form input of an inline picker still submits 0000-… / 00-1-… for a caller-supplied value before year 1 bulma-ui/src/form/DateInputBase.tsx:535

Overall: The change is sound and unusually well bounded. One helper (floorMin) carries the whole decision, each path applies it exactly where it already applied min, and for years ≥ 1 every edit is a provable no-op — the day-granularity prevDisabled simplification is equivalent once min is always defined, periodMin / lowerBound only add a lower comparison that normal dates pass, and the yearList default gains a Math.max(FIRST_YEAR, …) that is inert above year 101. The riskiest spot is that the floor is one-sided: min is raised, max is not, and clampDate prefers max when the two cross (finding 1). A human should look first at whether a max below the floor is worth defending at all, and second at whether a caller-supplied value before year 1 should still serialize as 00-1-06-15 into a submitted form field (finding 2) — both are product calls rather than defects, and a reasoned "caller error" settles either.

Residual risk:

  • Calendar header below the floor — open, posted as finding 1. Reachable only with max before 1 January of year 1, where nothing is selectable either way; pre-PR the same config also focused year 0, so it is not a regression.
  • Hidden and native value serialization — open, posted as finding 2. Entered only through a caller-supplied value or defaultValue, which the PR body scopes out.
  • Segmented typing, every granularity — refuted. useSegmentedEntry.isAllowed gates every manual commit on isWithin(d, min, max) with min: periodMin (useSegmentedEntry.ts:210), and periodMin is now unconditionally the floor, so arrow stepping, digit entry, Enter and the blur re-parse all reject year ≤ 0 — including a date from a custom parse. Covered by the new tests on DateInput, DateTimeInput and TimeInput; 615 tests across the six touched suites pass.
  • isBlocked still built from the raw min (DateInputBase.tsx:432) — refuted as a gap. It is only ever consumed by isAllowed, which ANDs it with the floored isWithin, so the weaker predicate changes no outcome: a month or year in year 0 is rejected by the bounds check before isBlocked could matter.
  • focusedDate drifting below year 1 by some other route — refuted. It is clamped at mount and re-clamped when lowerBound or max change; moveFocus and moveMonthFocus return at the floor; the year list starts at 1; a day cell takes onFocus only for in-current-month cells; and commitValue sets it from an already-gated value. The one exception is the max below min crossing in finding 1.
  • The TimeInput native min keeping the given time rather than the floor — refuted as a defect. The native time input carries no date, the divergence predates this change (a min dated in year 0 never constrained a today-dated value either), and the TSDoc scopes its claim to "the wheels and typing", so it is not false about the native path.
  • Year-list cost at year granularity — refuted. isPeriodUnselectable short-circuits on the first in-range day, so the floored min does not turn the 201-year list into a 73k-day walk.
  • Generated artefacts drifting — refuted by inspection: the three bestax-mcp/data/components rows and the three API-page min rows are the only copies of those descriptions in the tree, every table row in timeinput.md is a uniform 456 characters so Prettier is satisfied, and nothing in skills/ states a picker year range.
  • Coverage — refuted. The changed files sit at 100% lines and 99.3% branches, with only pre-existing partials (DateInputBase.tsx:395,632, Calendar.tsx:794) uncovered; every new branch — both floorMin arms, both minYear arms, both prevDisabled arms, both periodMin arms — is exercised.

🏄 Clean little set wave, this one — one tiny helper paddles out and every picker drops in right behind it, no wipeouts for anybody living after year 1. The only thing still bobbing past the break is a max set before the dawn of time, and nobody is surfing that. Good to go.

A max before year 1 crosses the floor, and clampDate returned max when the bounds crossed, so
clamping twice swapped between them. Under StrictMode the focus re-clamp runs twice on mount,
so the calendar opened on December of year 0 and its header stepped back from there.

When min is after max, clampDate now lands on min, so the focus stays at year 1. The day
grid's header also turns off once the month on show holds min or max or lies past it, rather
than only when it holds it, which picking a year from the list could get around.

A value before year 1 has no shape in HTML's date strings, so the inline pickers' hidden form
input now submits it empty, as a native date input would, rather than a malformed string such
as 00-1-06-15. The value still shows as given and is never committed.
@allxsmith

Copy link
Copy Markdown
Owner Author

deep-review: verify

Every open thread has a reply: fixed, with the commit, or refuted with a reason. This settles them on 4f6eb18.

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://c89cd4a7.bestax.pages.dev

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep review (verify) — 2 resolved · 0 open

N Severity Area Finding Location
1 🔵 Advisory Correctness A max before year 1 still puts the calendar below the floor — verified fixed bulma-ui/src/form/_pickerInternals/Calendar.tsx:470
2 🔵 Advisory Robustness The floor reaches min but not the value the hidden form input submits — verified fixed bulma-ui/src/form/DateInputBase.tsx:538

Overall: This pass settled the two threads the earlier review left open and reviewed no commits. Both were answered by the PR author (admin) with a fix in 4f6eb18b plus a reasoned decision on the part left alone, and both check out against the current code: clampDate now caps against max then floors against min, so min wins on crossed bounds and clamping is idempotent; the day-grid header compares startOfMonth(focusedDate) to the bound instead of requiring isSameMonth, closing the year-list route past a bound as well as the crossed-bounds case; and toIsoValue/toIsoDateTime return an empty string below FIRST_YEAR, so an inline picker hidden input submits nothing rather than 00-1-06-15. The two declined halves — an empty year list when no year is in range, and not clamping a caller-supplied value on submit — are the deliberate calls the original advisories offered as settlements. Nothing is left for a human here.

Residual risk:

  • Header reachable below the floor: refuted — prevDisabled/nextDisabled are now pure startOfMonth comparisons against the bound (Calendar.tsx:467-473), and the keyboard path already returned early on month < startOfMonth(min) (Calendar.tsx:403-405). The new Calendar cases are load-bearing: the Mar-2024-focus / Jun-2024-min case was false under the old isSameMonth guard.
  • Clamp swapping between crossed bounds: refuted — clampDate is idempotent by construction and dateUtils.test.ts pins min winning; the DateInput and DateTimeInput cases render under React.StrictMode, which is what exposed the double re-clamp.
  • Malformed year serialized anywhere else: refuted for the paths in the diff — toIsoValue and toIsoDateTime are the only producers of the YYYY-… shape, and lowerBound is always floored, so the native min attribute still emits 0001-01-01 rather than an empty string.
  • All 577 tests across Calendar, DateInput, DateTimeInput, TimeInput and dateUtils pass on 4f6eb18b, and each of the six new assertions was confirmed to run by name rather than silently skip.

🏄 Author caught both advisories on the inside and rode them clean — the clamp no longer flip-flops between crossed bounds, and nothing malformed makes it onto the wire. All threads closed out, zero open, this one is good to go.

@allxsmith allxsmith added the needs-human-review Loop converged (or contested): awaiting owner review + manual merge label Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

📸 Story screenshots at handoff — 4f6eb18

No Storybook stories map to this PR's changed files — nothing to screenshot. (workflow run)

@allxsmith allxsmith removed the needs-human-review Loop converged (or contested): awaiting owner review + manual merge label Oct 6, 2026
@github-actions github-actions Bot added the review-converged Deep review converged: no blocking findings, all review threads resolved, checks green label Oct 6, 2026
@allxsmith allxsmith added the needs-human-review Loop converged (or contested): awaiting owner review + manual merge label Oct 6, 2026
@github-actions github-actions Bot added review-converged Deep review converged: no blocking findings, all review threads resolved, checks green and removed review-converged Deep review converged: no blocking findings, all review threads resolved, checks green labels Oct 6, 2026
@allxsmith
allxsmith merged commit 57a6242 into main Oct 7, 2026
64 checks passed
@allxsmith
allxsmith deleted the fix/902-year-floor branch October 7, 2026 05:44
@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 5.26.4 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 2.25.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 4.2.13 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 1.14.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deep-review needs-human-review Loop converged (or contested): awaiting owner review + manual merge released review-converged Deep review converged: no blocking findings, all review threads resolved, checks green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The date pickers accept years the native inputs can't show

1 participant