fix(calendar): don't range-highlight adjacent-month spillover days#3574
fix(calendar): don't range-highlight adjacent-month spillover days#3574cixzhang wants to merge 3 commits into
Conversation
Outside (adjacent-month) days now never receive selection, range, or preview state. In the two-month range view the same date renders in both panes, so the spillover copy on the neighbouring month's pane was drawn as part of the selection. (#2715)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR Analysis Report📚 Storybook PreviewView Storybook for this PR 🧪 Sandbox PreviewView Sandbox for this PR No new or modified components detected. Bundle Size Summary
Accessibility AuditStatus: No accessibility violations detected. Generated by PR Enrichment workflow | Storybook | Sandbox | View full report |
Address review follow-up: a highlighted range day adjacent to a disabled or adjacent-month (outside) day now gets a rounded end cap on that side, so the run reads as terminating at the gap instead of running square-edged into it. computeRangeRounding gains optional neighbour continuity info; a new isRangeHighlighted helper derives whether a neighbouring cell continues the run (enabled, in-month, in-range). MonthGrid computes prev/next continuity per day and passes it down.
|
Added the end caps in d708fd2. A highlighted range day that sits next to a disabled or adjacent-month (outside) day now gets a rounded cap on that side, so the run terminates cleanly at the gap instead of running square-edged into it. Mechanism: One interpretation call I want to flag: I kept the rounding change only — a disabled day that falls inside the selected range still paints its own range background (unchanged behavior), and its highlighted neighbours are what get capped. If your screenshot intends the disabled day itself to be a true gap (no fill at all), that's a one-line follow-up ( |
|
It's looking better but the |
| const prevDay = week[dayIndex - 1]; | ||
| const nextDay = week[dayIndex + 1]; | ||
| // A neighbour continues the highlighted run only if it is an | ||
| // enabled, in-month range day. A disabled or adjacent-month | ||
| // (outside) neighbour breaks continuity, so this day gets an | ||
| // end cap on that side (#2715). | ||
| const prevInRange = prevDay | ||
| ? isRangeHighlighted({ | ||
| date: prevDay.date, | ||
| mode, | ||
| rangeStart, | ||
| rangeEnd, | ||
| isDisabled: isDateDisabled(prevDay.date), | ||
| isOutside: prevDay.isOutside, | ||
| }) | ||
| : false; | ||
| const nextInRange = nextDay | ||
| ? isRangeHighlighted({ | ||
| date: nextDay.date, | ||
| mode, | ||
| rangeStart, | ||
| rangeEnd, | ||
| isDisabled: isDateDisabled(nextDay.date), | ||
| isOutside: nextDay.isOutside, | ||
| }) | ||
| : false; |
There was a problem hiding this comment.
This logic is best broken out into a util and tested as well.
There was a problem hiding this comment.
Done in 65a895e — pulled the neighbour logic out of the render path into computeDayNeighborContinuity in dayCellUtils.ts, with an isPreviewHighlighted helper alongside the existing isRangeHighlighted. Both are unit-tested (continuity through disabled/outside neighbours, row edges, and preview vs range independence). DayCell now just receives a single neighbors object.
Address review: break the per-day neighbour-continuity logic out of the render path into a tested util (computeDayNeighborContinuity) and apply the same disabled/outside end-cap rounding to the hover PREVIEW highlight, not just the committed range — the preview was previously missing the caps. - new computeDayNeighborContinuity + isPreviewHighlighted helpers, unit tested - computePreviewRounding now takes optional neighbour info (mirrors range) - DayCell takes a single neighbors object instead of two booleans
|
Good catch — the caps only ran on the committed range, not the hover preview. Fixed in 65a895e: |

Summary
In the two-month
DateRangeInput/Calendarrange view, spillover days from the neighbouring month were rendered with the same range-highlight styling as the real selection. With July 1–31 selected, the August pane showed July 26–31 in its first row and highlighted that row as part of the selection; symmetrically the July pane highlighted early-August spillover days. Closes #2715.Root cause
computeDayCellState(packages/core/src/Calendar/dayCellUtils.ts) derivedisInRange/isRangeStart/isRangeEnd/ preview /isSelectedpurely from date membership, ignoring the cell'sisOutsideflag. Because every visible pane renders a fixed grid that includes adjacent-month days (hasOutsideDays, defaulttrue), the same date appears in two panes — and both copies matched the range predicate, so the outside/spillover copy got the range-highlight background anddata-in-rangestate.Fix
Outside (adjacent-month) days no longer receive selection, range, or preview state. Outside days are already non-interactive (
effectivelyDisabled), so this is the minimal, consistent change: the in-month copy in the correct pane still highlights, and the spillover copy in the other pane renders as a plain grayed outside day.Validation
dayCellUtils.test.tsasserting anisOutsideday gets no range/preview/selected state. Failed before the fix, passes after.Calendar.test.tsxrendering the two-month range (July 1–31, July+August visible) and asserting the August-pane spillover copies of July 26–31 do not carrydata-in-range. This exercises the real render path; it failed before the fix (expected 'in-range' to be null) and passes after.Why this validation: the bug is deterministic rendering logic, so a failing→passing automated assertion on the highlight state is the definitive proof — no visual snapshot needed for correctness.
What to eyeball
In Storybook / the sandbox, open the two-month range calendar, select a range within one month (e.g. July 1–31), and confirm the neighbouring pane's spillover row is a plain grayed row with no range background or endpoint highlight.