Skip to content

Commit 0cd3418

Browse files
committed
fix(DatePicker): remove controlled visibleMonth/onVisibleMonthChange, 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.
1 parent b63f47b commit 0cd3418

7 files changed

Lines changed: 38 additions & 65 deletions

File tree

.changeset/datepicker-visible-month.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
'@razorpay/blade': minor
33
---
44

5-
feat(DatePicker): add `visibleMonth`/`defaultVisibleMonth` to control the rendered calendar month independently of `value`
5+
feat(DatePicker): add `defaultVisibleMonth` to anchor the rendered calendar month independently of `value`
66

7-
`DatePicker`/`DateRangePicker` now accept `visibleMonth` (controlled), `defaultVisibleMonth` (uncontrolled), and `onVisibleMonthChange` props. These let the calendar open on a specific month without pre-selecting a date — useful for a "comparison" range picker that should default to the period immediately preceding a primary range picker's selection, while leaving the comparison `value` empty for the user to pick.
7+
`DatePicker`/`DateRangePicker` now accept a `defaultVisibleMonth` prop. This lets the calendar open on a specific month without pre-selecting a date — useful for a "comparison" range picker that should default to the period immediately preceding a primary range picker's selection, while leaving the comparison `value` empty for the user to pick.
88

99
```tsx
1010
<DatePicker
@@ -14,4 +14,6 @@ feat(DatePicker): add `visibleMonth`/`defaultVisibleMonth` to control the render
1414
/>
1515
```
1616

17+
`defaultVisibleMonth` only sets the initial anchor — subsequent calendar navigation is uncontrolled from that point on. Use the existing `onNext`/`onPrevious` callbacks to observe navigation, and `onChange` to observe date selection.
18+
1719
Falls back to the existing behavior (first date of `value`/`defaultValue`, or today) when not set — fully backwards compatible.

packages/blade/src/components/DatePicker/BaseDatePicker.web.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,7 @@ const BaseDatePicker = <Type extends DateSelectionType = 'single'>({
7878
defaultPicker = 'day',
7979
picker,
8080
onPickerChange,
81-
visibleMonth,
8281
defaultVisibleMonth,
83-
onVisibleMonthChange,
8482
zIndex = componentZIndices.popover,
8583
format = 'DD/MM/YYYY',
8684
inputPlaceHolder,
@@ -369,9 +367,7 @@ const BaseDatePicker = <Type extends DateSelectionType = 'single'>({
369367
{...props}
370368
selectionType={_selectionType}
371369
defaultValue={defaultValue}
372-
visibleMonth={visibleMonth}
373370
defaultVisibleMonth={defaultVisibleMonth}
374-
onVisibleMonthChange={onVisibleMonthChange}
375371
onMouseLeave={onRootMouseLeave}
376372
__onDayMouseEnter={(_event, date) => {
377373
onHoveredDateChange(date);

packages/blade/src/components/DatePicker/Calendar.web.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@ const Calendar = <Type extends DateSelectionType>({
2626
date,
2727
defaultDate,
2828
onDateChange,
29-
visibleMonth,
3029
defaultVisibleMonth,
31-
onVisibleMonthChange,
3230
onNext,
3331
onPrevious,
3432
presets,
@@ -69,11 +67,7 @@ const Calendar = <Type extends DateSelectionType>({
6967
});
7068

7169
const [_visibleMonth, setVisibleMonth] = useControllableState<Date | undefined>({
72-
value: visibleMonth,
7370
defaultValue: defaultVisibleMonth,
74-
onChange: (date) => {
75-
if (date) onVisibleMonthChange?.(date);
76-
},
7771
});
7872

7973
const dateContext = useDatesContext();
@@ -97,8 +91,9 @@ const Calendar = <Type extends DateSelectionType>({
9791
const numberOfColumns = isMobile || !isRange ? 1 : 2;
9892
const columnsToScroll = numberOfColumns;
9993

100-
// Keeps the legacy `_date` state and the new `_visibleMonth` state in sync so that
101-
// navigation keeps working regardless of which one is currently driving `currentDate`.
94+
// Keeps the legacy `_date` state and `_visibleMonth` (the defaultVisibleMonth anchor)
95+
// in sync so navigation keeps working regardless of which one is currently driving
96+
// `currentDate`.
10297
// Note: Not wrapped in useCallback because `setDate` from mantine's `useUncontrolled`
10398
// is not memoized, so useCallback would provide no benefit.
10499
const updateCurrentDate = (nextDate: Date): void => {

packages/blade/src/components/DatePicker/DatePicker.native.tsx

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -382,16 +382,12 @@ function _DatePicker<Type extends DateSelectionType = 'single'>(
382382
picker,
383383
defaultPicker,
384384
onPickerChange,
385-
visibleMonth: visibleMonthProp,
386385
defaultVisibleMonth,
387-
onVisibleMonthChange,
388386
} = props as DatePickerNativeProps<DateSelectionType> & {
389387
picker?: unknown;
390388
defaultPicker?: unknown;
391389
onPickerChange?: unknown;
392-
visibleMonth?: unknown;
393390
defaultVisibleMonth?: unknown;
394-
onVisibleMonthChange?: unknown;
395391
successText?: unknown;
396392
};
397393

@@ -401,9 +397,7 @@ function _DatePicker<Type extends DateSelectionType = 'single'>(
401397
if (picker !== undefined) unsupported.push('picker');
402398
if (defaultPicker !== undefined) unsupported.push('defaultPicker');
403399
if (onPickerChange !== undefined) unsupported.push('onPickerChange');
404-
if (visibleMonthProp !== undefined) unsupported.push('visibleMonth');
405400
if (defaultVisibleMonth !== undefined) unsupported.push('defaultVisibleMonth');
406-
if (onVisibleMonthChange !== undefined) unsupported.push('onVisibleMonthChange');
407401
if (unsupported.length > 0) {
408402
logger({
409403
type: 'warn',
@@ -413,14 +407,7 @@ function _DatePicker<Type extends DateSelectionType = 'single'>(
413407
)}.`,
414408
});
415409
}
416-
}, [
417-
picker,
418-
defaultPicker,
419-
onPickerChange,
420-
visibleMonthProp,
421-
defaultVisibleMonth,
422-
onVisibleMonthChange,
423-
]);
410+
}, [picker, defaultPicker, onPickerChange, defaultVisibleMonth]);
424411

425412
const isError = validationState === 'error';
426413
const isSuccess = validationState === 'success';
@@ -732,8 +719,8 @@ function _DatePicker<Type extends DateSelectionType = 'single'>(
732719
* #### Not yet supported on native
733720
*
734721
* Month/year picker variants (`picker`, `defaultPicker`, `onPickerChange`),
735-
* `visibleMonth`/`defaultVisibleMonth`/`onVisibleMonthChange`, masked text
736-
* input, and the form-validation props (`validationState`, `helpText`,
722+
* `defaultVisibleMonth`, masked text input, and the form-validation props
723+
* (`validationState`, `helpText`,
737724
* `errorText`, `isRequired`, `necessityIndicator`, `labelPosition`). Passing
738725
* any of these emits a `__DEV__` warning and they are otherwise ignored at
739726
* runtime.

packages/blade/src/components/DatePicker/DatePicker.stories.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,7 @@ export default {
4747
maxDate: baseProp,
4848
excludeDate: baseProp,
4949
picker: baseProp,
50-
visibleMonth: baseProp,
5150
defaultVisibleMonth: baseProp,
52-
onVisibleMonthChange: baseProp,
5351
onOpenChange: baseProp,
5452
allowSingleDateInRange: baseProp,
5553
defaultIsOpen: baseProp,
@@ -1370,11 +1368,14 @@ export const DatePickerComparisonRange: StoryFn<typeof DatePickerComponent> = ()
13701368
return (
13711369
<Box>
13721370
<Text marginBottom="spacing.5">
1373-
Use <Code size="medium">visibleMonth</Code>/<Code size="medium">defaultVisibleMonth</Code>{' '}
1374-
to anchor a comparison <Code size="medium">DatePicker</Code> on the months immediately
1375-
preceding a primary range&apos;s selection — without pre-filling the comparison{' '}
1376-
<Code size="medium">value</Code>. Pick a primary range below, then open &quot;Compare
1377-
to&quot; and notice the calendar opens on the same-length period right before it.
1371+
Use <Code size="medium">defaultVisibleMonth</Code> to anchor a comparison{' '}
1372+
<Code size="medium">DatePicker</Code> on the months immediately preceding a primary
1373+
range&apos;s selection — without pre-filling the comparison{' '}
1374+
<Code size="medium">value</Code>. Since <Code size="medium">defaultVisibleMonth</Code>{' '}
1375+
only sets the initial anchor, remounting via a <Code size="medium">key</Code> re-anchors
1376+
the calendar whenever the computed comparison month changes. Pick a primary range below,
1377+
then open &quot;Compare to&quot; and notice the calendar opens on the same-length period
1378+
right before it.
13781379
</Text>
13791380
<Box display="flex" flexDirection="column" gap="spacing.5" maxWidth="400px">
13801381
<DatePickerComponent
@@ -1384,18 +1385,18 @@ export const DatePickerComparisonRange: StoryFn<typeof DatePickerComponent> = ()
13841385
onChange={(date) => setPrimaryRange(date)}
13851386
/>
13861387
<DatePickerComponent
1388+
key={comparisonVisibleMonth?.toISOString()}
13871389
label={{ start: 'Compare to' }}
13881390
selectionType="range"
13891391
value={comparisonRange}
1390-
visibleMonth={comparisonVisibleMonth}
1391-
onVisibleMonthChange={setComparisonVisibleMonth}
1392+
defaultVisibleMonth={comparisonVisibleMonth}
13921393
onChange={(date) => setComparisonRange(date)}
13931394
/>
13941395
</Box>
13951396
</Box>
13961397
);
13971398
};
13981399

1399-
DatePickerComparisonRange.storyName = 'Comparison Range (visibleMonth)';
1400+
DatePickerComparisonRange.storyName = 'Comparison Range (defaultVisibleMonth)';
14001401

14011402
DatePickerWithTimePickerInModal.storyName = 'DatePicker with TimePicker (Modal)';

packages/blade/src/components/DatePicker/__tests__/DatePicker.visibleMonth.web.test.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe('<DatePicker/> visibleMonth', () => {
2727
expect(getAllByText(dayjs(anchorMonth).format('MMMM YYYY')).length).toBeGreaterThan(0);
2828
});
2929

30-
it('gives visibleMonth priority over the first date of a controlled value', async () => {
30+
it('gives defaultVisibleMonth priority over the first date of a controlled value', async () => {
3131
const anchorMonth = dayjs().subtract(6, 'month').toDate();
3232
const selectedRange = [dayjs().toDate(), dayjs().add(3, 'day').toDate()] as [Date, Date];
3333

@@ -36,7 +36,7 @@ describe('<DatePicker/> visibleMonth', () => {
3636
selectionType="range"
3737
label={{ start: 'Compare to' }}
3838
value={selectedRange}
39-
visibleMonth={anchorMonth}
39+
defaultVisibleMonth={anchorMonth}
4040
onChange={() => undefined}
4141
/>,
4242
);
@@ -49,16 +49,19 @@ describe('<DatePicker/> visibleMonth', () => {
4949
expect(getAllByText(dayjs(anchorMonth).format('MMMM YYYY')).length).toBeGreaterThan(0);
5050
});
5151

52-
it('fires onVisibleMonthChange when navigating to the next month in controlled mode', async () => {
52+
it('fires onNext with the correct date when navigating to the next month', async () => {
53+
// Consumers who need to track the rendered month (e.g. to re-anchor a
54+
// comparison picker) should rely on the existing onNext/onPrevious
55+
// callbacks rather than a dedicated visible-month callback.
5356
const anchorMonth = dayjs().subtract(4, 'month').toDate();
54-
const onVisibleMonthChange = jest.fn();
57+
const onNext = jest.fn();
5558

5659
const { getByRole, queryByText } = renderWithTheme(
5760
<DatePickerComponent
5861
selectionType="range"
5962
label={{ start: 'Compare to' }}
60-
visibleMonth={anchorMonth}
61-
onVisibleMonthChange={onVisibleMonthChange}
63+
defaultVisibleMonth={anchorMonth}
64+
onNext={onNext}
6265
/>,
6366
);
6467

@@ -71,10 +74,11 @@ describe('<DatePicker/> visibleMonth', () => {
7174
const nextButton = getByRole('button', { name: /Next/i });
7275
await user.click(nextButton);
7376

74-
expect(onVisibleMonthChange).toHaveBeenCalledTimes(1);
75-
const calledDate = onVisibleMonthChange.mock.calls[0][0] as Date;
77+
expect(onNext).toHaveBeenCalledTimes(1);
78+
const { date: calledDate, type } = onNext.mock.calls[0][0] as { date: Date; type: string };
7679
// Range picker on desktop shows 2 columns, so "Next" advances by 2 months
7780
const expectedDate = dayjs(anchorMonth).add(2, 'month').toDate();
81+
expect(type).toBe('month');
7882
expect(dayjs(calledDate).isSame(expectedDate, 'month')).toBe(true);
7983
});
8084

packages/blade/src/components/DatePicker/types.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -60,34 +60,22 @@ type CalendarProps<SelectionType extends DateSelectionType> = Pick<
6060
onPickerChange?: (picker: PickerType) => void;
6161

6262
/**
63-
* Controlled month that the calendar renders, independent of the selected `value`.
63+
* Sets the initial month the calendar renders, independent of the selected `value`.
6464
*
6565
* Useful when you want the calendar to open on a specific month without pre-selecting
6666
* a date — e.g. a "comparison" range picker that should default to the period
6767
* immediately preceding a primary range picker's selection.
6868
*
6969
* Falls back to the first date of `value`/`defaultValue` (or today) when not set.
70-
*
71-
* When `visibleMonth` is controlled, you must handle `onVisibleMonthChange` to keep
72-
* the prop in sync — otherwise calendar navigation will not work.
70+
* Only sets the initial anchor — subsequent calendar navigation (next/previous
71+
* month, year, decade) is uncontrolled from that point on. Use `onChange` to
72+
* observe date selection.
7373
*
7474
* @example
7575
* // Anchor a comparison picker to the month before the primary range's start
76-
* <DatePicker selectionType="range" visibleMonth={dayjs(primaryStart).subtract(1, 'month').toDate()} />
77-
*/
78-
visibleMonth?: Date;
79-
/**
80-
* Uncontrolled variant of `visibleMonth`. Sets the initial month the calendar renders,
81-
* without pre-selecting a date.
76+
* <DatePicker selectionType="range" defaultVisibleMonth={dayjs(primaryStart).subtract(1, 'month').toDate()} />
8277
*/
8378
defaultVisibleMonth?: Date;
84-
/**
85-
* Callback which fires when the rendered month changes, either via calendar
86-
* navigation (next/previous month, year, or decade) or when a date in a
87-
* different month is selected. Does not fire when the `visibleMonth` prop
88-
* is updated externally by the consumer.
89-
*/
90-
onVisibleMonthChange?: (date: Date) => void;
9179

9280
/**
9381
* Controlled isOpen state

0 commit comments

Comments
 (0)