Skip to content

Commit 5e2f833

Browse files
fix: Navigate calendar view when typing partial dates (#5459)
When typing a partial date (e.g., just "2014") in the input field, the calendar now navigates to that year/month even though the full date cannot be parsed. This restores the v7 behavior where users could search through the calendar by typing partial dates. - Add parseDateForNavigation() to extract year/month from partial input - Update handleChange to use partial navigation when full parse fails - Respect minDate/maxDate constraints when navigating - Handle invalid input gracefully (returns null, calendar stays put) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 2ddeacb commit 5e2f833

File tree

3 files changed

+106
-0
lines changed

3 files changed

+106
-0
lines changed

src/date_utils.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,40 @@ export function parseDate(
150150
return null;
151151
}
152152

153+
/**
154+
* Parses a partial date string for calendar navigation purposes.
155+
* Unlike parseDate, this function attempts to extract whatever date
156+
* information is available (year, month) from a partial input,
157+
* returning a date suitable for navigating the calendar view.
158+
*
159+
* @param value - The date string to parse.
160+
* @param refDate - The reference date to use for missing components.
161+
* @returns - A date for navigation or null if no date info could be extracted.
162+
*/
163+
export function parseDateForNavigation(
164+
value: string,
165+
refDate: Date = newDate(),
166+
): Date | null {
167+
if (!value) return null;
168+
169+
// Try to extract a 4-digit year from the input
170+
const yearMatch = value.match(/\b(1\d{3}|2\d{3})\b/);
171+
if (!yearMatch || !yearMatch[1]) return null;
172+
173+
const year = parseInt(yearMatch[1], 10);
174+
175+
// Try to extract a month (1-12) from the input
176+
// Look for patterns like "03/", "/03", "03-", "-03" or standalone "03" at start
177+
const monthMatch = value.match(/(?:^|[/\-\s])?(0?[1-9]|1[0-2])(?:[/\-\s]|$)/);
178+
const month =
179+
monthMatch && monthMatch[1]
180+
? parseInt(monthMatch[1], 10) - 1
181+
: refDate.getMonth();
182+
183+
// Return a date with the extracted year and month, using day 1
184+
return new Date(year, month, 1);
185+
}
186+
153187
// ** Date "Reflection" **
154188

155189
export { isDate, set };

src/index.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
getEffectiveMinDate,
2929
getEffectiveMaxDate,
3030
parseDate,
31+
parseDateForNavigation,
3132
formatDate,
3233
safeDateFormat,
3334
safeDateRangeFormat,
@@ -757,6 +758,21 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
757758
// Update selection if either (1) date was successfully parsed, or (2) input field is empty
758759
if (date || !value) {
759760
this.setSelected(date, event, true);
761+
} else if (!this.props.inline) {
762+
// If full date parsing failed but we have partial input,
763+
// try to extract date info for calendar navigation
764+
const navDate = parseDateForNavigation(
765+
value,
766+
this.state.preSelection ?? undefined,
767+
);
768+
// Only update preSelection if navDate is valid and within min/max bounds
769+
if (
770+
navDate &&
771+
(!this.props.minDate || !isBefore(navDate, this.props.minDate)) &&
772+
(!this.props.maxDate || !isAfter(navDate, this.props.maxDate))
773+
) {
774+
this.setState({ preSelection: navDate });
775+
}
760776
}
761777
}
762778
};

src/test/datepicker_test.test.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2466,6 +2466,62 @@ describe("DatePicker", () => {
24662466
expect(instance!.state.preSelection?.getMonth()).toBe(1); // February (0-indexed)
24672467
expect(instance!.state.preSelection?.getDate()).toBe(10);
24682468
});
2469+
it("should update calendar view when typing a partial date (year only)", () => {
2470+
let instance: DatePicker | null = null;
2471+
2472+
render(
2473+
<DatePicker
2474+
ref={(node) => {
2475+
instance = node;
2476+
}}
2477+
selected={newDate("2024-06-15")}
2478+
onChange={jest.fn()}
2479+
dateFormat="MM/dd/yyyy"
2480+
open
2481+
/>,
2482+
);
2483+
expect(instance).toBeTruthy();
2484+
2485+
// Verify initial calendar shows 2024
2486+
expect(instance!.state.preSelection?.getFullYear()).toBe(2024);
2487+
2488+
// Type a partial date - just a year
2489+
fireEvent.change(instance!.input!, {
2490+
target: {
2491+
value: "2014",
2492+
},
2493+
});
2494+
2495+
// Calendar should navigate to 2014 even though it's not a complete date
2496+
expect(instance!.state.preSelection?.getFullYear()).toBe(2014);
2497+
});
2498+
it("should update calendar view when typing a partial date (month and year)", () => {
2499+
let instance: DatePicker | null = null;
2500+
2501+
render(
2502+
<DatePicker
2503+
ref={(node) => {
2504+
instance = node;
2505+
}}
2506+
selected={newDate("2024-06-15")}
2507+
onChange={jest.fn()}
2508+
dateFormat="MM/dd/yyyy"
2509+
open
2510+
/>,
2511+
);
2512+
expect(instance).toBeTruthy();
2513+
2514+
// Type a partial date - month and year
2515+
fireEvent.change(instance!.input!, {
2516+
target: {
2517+
value: "03/2014",
2518+
},
2519+
});
2520+
2521+
// Calendar should navigate to March 2014
2522+
expect(instance!.state.preSelection?.getFullYear()).toBe(2014);
2523+
expect(instance!.state.preSelection?.getMonth()).toBe(2); // March (0-indexed)
2524+
});
24692525
it("should correctly update the input when the value prop changes", () => {
24702526
let instance: DatePicker | null = null;
24712527
const { rerender } = render(

0 commit comments

Comments
 (0)