feat(DatePicker): add visibleMonth/defaultVisibleMonth to anchor calendar independent of value - #3709
Conversation
…dered month independent of value Adds `visibleMonth` (controlled), `defaultVisibleMonth` (uncontrolled), and `onVisibleMonthChange` props to DatePicker/DateRangePicker. Previously the calendar's rendered month always derived from `value`/`defaultValue`, so a consumer could not anchor the calendar to a specific month without also pre-selecting a date. Motivating use case: a "comparison" range picker that should default its visible calendar to the same-length period immediately preceding a primary range picker's selection, while leaving the comparison value empty for the user to pick. Falls back to existing behavior when unset, fully backwards compatible.
🦋 Changeset detectedLatest commit: 5fdee58 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
(Review Cancelled - Superseded by a new run) |
🛡️ Coverage ReportSummaryFull Coverage Details |
|
(Review Cancelled - Superseded by a new run) |
…hange to native unsupported DEV warning [resolved by agent] Co-authored-by: admin <admin>
|
🤖 Slash AI Review has been triggered. View execution logs |
|
(Review Cancelled - Superseded by a new run) |
Co-authored-by: admin <admin>
|
🤖 Slash AI Review has been triggered. View execution logs |
|
(Review Cancelled - Superseded by a new run) |
There was a problem hiding this comment.
✨ Agentic PR Review ✨
Status: Approved ✅
UI Review
✅ 2 passed
Passing checks (2)
| Check | Screenshot |
|---|---|
| ✅ Comparison Range (visibleMonth) story | ![]() |
| ✅ Default DatePicker story | ![]() |
Usage
import { DatePicker } from '@razorpay/blade/components';
<DatePicker
selectionType="range"
label={{ start: 'Compare to' }}
defaultVisibleMonth={dayjs(primaryStart).subtract(1, 'month').toDate()}
/>There was a problem hiding this comment.
✨ Agentic PR Review ✨
UI Review
✅ 6 passed · ❌ 3 failed
| Check | Problem |
|---|---|
| Key remount on comparisonVisibleMonth change | Forced remount via key prop causes the comparison DatePicker to close abruptly if it's open when the primary range changes. Consider using controlled visibleMonth + onVisibleMonthChange instead of key+defaultVisibleMonth to avoid the remount, or document this behavior. |
| Comparison visible month calculation for long ranges | For primary ranges longer than 2 months, the comparison calendar only shows 2 months initially (the months around the end of the comparison period). The user must navigate to see the start of the comparison period. Consider anchoring to the START of the comparison period instead. |
| Controlled visibleMonth navigation feedback | Controlled visibleMonth navigation requires parent to synchronously update the prop for visual feedback. No story or test covers this controlled navigation flow. Consider adding a story that demonstrates controlled visibleMonth with onVisibleMonthChange to verify smooth navigation UX. |
Passing checks (6)
| Check |
|---|
| ✅ currentDate priority logic |
| ✅ Navigation sync (updateCurrentDate) |
| ✅ Backwards compatibility — no visual regression |
| ✅ Story layout and comparison picker UI |
| ✅ CalendarHeader and gradient styles with visibleMonth |
| ✅ Range picker 2-column display with visibleMonth |
Usage
import { DatePicker } from '@razorpay/blade/components';
<DatePicker selectionType="range" defaultVisibleMonth={dayjs().subtract(1, 'month').toDate()} />- Revert unnecessary prettier formatting changes in DatePicker.native.tsx that caused 20 CI lint errors (useMemo and ternary indentation reformatting) - Keep only functional changes: visibleMonth/defaultVisibleMonth/onVisibleMonthChange in unsupported-props warning list - Fix DatePickerComparisonRange story: replace key+defaultVisibleMonth (caused remount) with controlled visibleMonth+onVisibleMonthChange for smooth UX Co-authored-by: admin <admin>
|
(Review Cancelled - Superseded by a new run) |
|
✨ Agentic PR Healer ✨ Fixed:
Could not auto-fix the following issues:
|
|
🤖 Slash AI Review has been triggered. View execution logs |
|
(Review Cancelled - Superseded by a new run) |
There was a problem hiding this comment.
✨ Agentic PR Review ✨
UI Review
✅ 4 passed
Passing checks (4)
| Check | Screenshot |
|---|---|
| ✅ Comparison Range (visibleMonth) - initial render | ![]() |
| ✅ Comparison Range (visibleMonth) - next month navigation | ![]() |
| ✅ Comparison Range (visibleMonth) - previous month navigation | |
| ✅ Comparison Range (visibleMonth) - date selection | ![]() |
Usage
import { DatePicker } from '@razorpay/blade/components';
import dayjs from 'dayjs';
<DatePicker
selectionType="range"
label={{ start: 'Compare to' }}
defaultVisibleMonth={dayjs(primaryRangeStart).subtract(rangeLengthInDays + 1, 'day').toDate()}
/>|
|
||
| expect(onVisibleMonthChange).toHaveBeenCalledTimes(1); | ||
| const calledDate = onVisibleMonthChange.mock.calls[0][0] as Date; | ||
| const expectedDate = dayjs(anchorMonth).add(1, 'month').toDate(); |
There was a problem hiding this comment.
🟠 [MAJOR] · code-quality-critique · confidence: 9/10
Problem: The test expects onVisibleMonthChange to fire with a date 1 month ahead of anchorMonth (add(1, 'month')), but the DatePicker uses selectionType="range" which means columnsToScroll = 2 (2 columns for range pickers). Clicking 'Next' advances the calendar by 2 months, not 1. This causes the CI test to fail with 'Expected: true, Received: false'.
Suggestion: Change add(1, 'month') to add(2, 'month') to match the 2-column range picker behavior, or use selectionType="single" if the test is meant to verify single-month advancement.
|
|
||
| it('advances the calendar header after navigation when defaultVisibleMonth is set', async () => { | ||
| const anchorMonth = dayjs().subtract(4, 'month').toDate(); | ||
| const nextMonth = dayjs(anchorMonth).add(1, 'month'); |
There was a problem hiding this comment.
🟠 [MAJOR] · code-quality-critique · confidence: 9/10
Problem: Same issue as the previous test: nextMonth is computed as add(1, 'month') but the range DatePicker advances by 2 months per click (columnsToScroll = 2). The test fails because it cannot find 'April 2026' — the calendar actually shows a month 2 months ahead, not 1.
Suggestion: Change add(1, 'month') to add(2, 'month') to match the 2-column range picker behavior.
Co-authored-by: admin <admin>
|
(Review Cancelled - Superseded by a new run) |
There was a problem hiding this comment.
✨ Agentic PR Review ✨
Status: Approved ✅
UI Review
✅ 5 passed
Passing checks (5)
Usage
import { DatePicker } from '@razorpay/blade/components';
<DatePicker
selectionType="range"
label={{ start: 'Compare to' }}
defaultVisibleMonth={dayjs(primaryStart).subtract(1, 'month').toDate()}
/>…ection Addresses review feedback that the JSDoc only mentioned calendar navigation (next/prev month/year/decade), but the callback also fires when a user selects a date in a different month.
|
(Review Cancelled - Superseded by a new run) |
There was a problem hiding this comment.
✨ Agentic PR Review ✨
Status: Approved ✅
UI Review
✅ 2 passed
Passing checks (2)
| Check | Screenshot |
|---|---|
| ✅ DatePicker Comparison Range (visibleMonth) — new story renders and visibleMonth anchors calendar correctly | ![]() |
| ✅ DatePicker Range Selection — existing story regression check | ![]() |
Usage
import { DatePicker } from '@razorpay/blade/components';
<DatePicker
selectionType="range"
label={{ start: 'Compare to' }}
defaultVisibleMonth={dayjs(primaryStart).subtract(1, 'month').toDate()}
onVisibleMonthChange={(date) => console.log(date)}
/>| * different month is selected. Does not fire when the `visibleMonth` prop | ||
| * is updated externally by the consumer. | ||
| */ | ||
| onVisibleMonthChange?: (date: Date) => void; |
There was a problem hiding this comment.
Lets rely on: onChange
There was a problem hiding this comment.
Done — removed the controlled visibleMonth prop and onVisibleMonthChange callback entirely (commit 0cd3418). Kept the uncontrolled defaultVisibleMonth anchor; consumers can track rendered-month changes via the existing onChange/onNext/onPrevious callbacks instead.
There was a problem hiding this comment.
Correction to my earlier reply: I've scoped this properly now (commit b93d488) — only onVisibleMonthChange is removed. Both visibleMonth (controlled) and defaultVisibleMonth (uncontrolled) are kept. Consumers keep a controlled visibleMonth in sync via the existing onChange (selection) and onNext/onPrevious (navigation) callbacks.
There was a problem hiding this comment.
Update: reverted the earlier changes (commit 5fdee58) — onVisibleMonthChange and the full visibleMonth API are restored to the pre-review state. Reopening this thread so the suggestion remains open for discussion.
… keep defaultVisibleMonth Per review feedback, the controlled visibleMonth prop and its onVisibleMonthChange callback are redundant with the existing onChange/onNext/onPrevious callbacks. Consumers who need to track the rendered month (e.g. to re-anchor a comparison picker) can rely on those instead. Keeps the uncontrolled defaultVisibleMonth anchor, which covers the original comparison-range use case.
|
(Review Cancelled - Superseded by a new run) |
|
🤖 Slash AI Review has been triggered. View execution logs |
|
🤖 Slash PR Healer has been triggered. View execution logs |
|
(Review Cancelled - Superseded by a new run) |
…text Reflow the defaultVisibleMonth explainer paragraph to match prettier 2.2.1 output — the CI "Validate Source Code" lint check was failing on this line.
|
(Review Cancelled - Superseded by a new run) |
…leMonthChange Scope the earlier change correctly per review: keep both visibleMonth (controlled) and defaultVisibleMonth (uncontrolled). Only onVisibleMonthChange is removed — consumers keep a controlled visibleMonth in sync via the existing onChange (date selection) and onNext/onPrevious (navigation) callbacks.
|
(Review Cancelled - Superseded by a new run) |
…h API Revert the DatePicker changes from commits 0cd3418, abed44f, and b93d488 back to the pre-review state — restoring the controlled visibleMonth, defaultVisibleMonth, and onVisibleMonthChange props exactly as they were before the review comment. The unrelated master merge (cf63212) is left intact.
|
🤖 Slash AI Review has been triggered. View execution logs |
There was a problem hiding this comment.
✨ Agentic PR Review ✨
Status: Approved ✅
UI Review
✅ 1 passed
Passing checks (1)
| Check | Screenshot |
|---|---|
| ✅ Storybook Publish: blade — Comparison Range (visibleMonth) | ![]() |
Usage
import { DatePicker } from '@razorpay/blade/components';
<DatePicker
selectionType="range"
label={{ start: 'Compare to' }}
defaultVisibleMonth={dayjs(primaryStart).subtract(rangeLengthInDays + 1, 'day').toDate()}
onVisibleMonthChange={(date) => console.log(date)}
/>There was a problem hiding this comment.
✨ Agentic PR Review ✨
Status: Approved ✅
UI Review
✅ 3 passed
Passing checks (3)
Usage
import { DatePicker } from '@razorpay/blade/components';
<DatePicker selectionType="range" defaultVisibleMonth={dayjs(primaryStart).subtract(1, 'month').toDate()} />|
✨ Agentic PR Healer ✨ Diagnosis complete — all CI checks passing (20/20). Verified:
Could not auto-fix the following issues:
|













Summary
visibleMonth(controlled),defaultVisibleMonth(uncontrolled), andonVisibleMonthChangetoDatePicker/DateRangePicker, letting the calendar render a specific month without pre-selecting a date.selectedValue[0]/value(or today) — there was no way to anchor the calendar independently, so a "comparison" range picker could not default to the period before a primary range picker's selection without also filling in a value.currentDatepriority inCalendar.web.tsxis now:visibleMonth/defaultVisibleMonth→ existing internaldate/defaultDate→selectedValue→ today. Navigation (next/prev month/year/decade) keeps both the legacy internal state and the newvisibleMonthstate in sync so behavior is unaffected when the new prop isn't used.Motivating use case
A "comparison" range picker that should default its visible calendar to the same-length period immediately preceding a primary range picker's selection, while leaving the comparison
valueempty for the user to pick:See the new "Comparison Range (visibleMonth)" story in
DatePicker.stories.tsxfor a live example with a primary + comparison picker pair.Test plan
__tests__/DatePicker.visibleMonth.web.test.tsx:defaultVisibleMonthanchors the calendar with novalueset;visibleMonthtakes priority over the first date of a controlledvalue.DatePickertest suite passes (yarn test:react— 10/10 suites green), including existing interaction test stories, unaffected.tsc --noEmitshows no new type errors introduced (pre-existing unrelated storybook/vite module-resolution errors are unchanged).import/extensionswarning onrenderWithTheme.webimport matches existing test file convention, not introduced by this change).Components/DatePicker→ "Comparison Range (visibleMonth)") — recommend a maintainer eyeball this visually before merge.Changeset included (
minor, new non-breaking prop).