diff --git a/src/month_dropdown.tsx b/src/month_dropdown.tsx
index fa5afcd22e..a1ed659efd 100644
--- a/src/month_dropdown.tsx
+++ b/src/month_dropdown.tsx
@@ -56,8 +56,9 @@ export default class MonthDropdown extends Component<
visible: boolean,
monthNames: string[],
): React.ReactElement => (
-
{monthNames[this.props.month]}
-
+
);
renderDropdown = (monthNames: string[]): React.ReactElement => (
diff --git a/src/month_dropdown_options.tsx b/src/month_dropdown_options.tsx
index 17af165ab3..e9d2cf12cb 100644
--- a/src/month_dropdown_options.tsx
+++ b/src/month_dropdown_options.tsx
@@ -10,12 +10,47 @@ interface MonthDropdownOptionsProps {
}
export default class MonthDropdownOptions extends Component {
+ monthOptionButtonsRef: Record = {};
+
isSelectedMonth = (i: number): boolean => this.props.month === i;
+ handleOptionKeyDown = (i: number, e: React.KeyboardEvent): void => {
+ switch (e.key) {
+ case "Enter":
+ e.preventDefault();
+ this.onChange(i);
+ break;
+ case "Escape":
+ e.preventDefault();
+ this.props.onCancel();
+ break;
+ case "ArrowUp":
+ case "ArrowDown": {
+ e.preventDefault();
+ const newMonth =
+ (i + (e.key === "ArrowUp" ? -1 : 1) + this.props.monthNames.length) %
+ this.props.monthNames.length;
+ this.monthOptionButtonsRef[newMonth]?.focus();
+ break;
+ }
+ }
+ };
+
renderOptions = (): React.ReactElement[] => {
+ // Clear refs to prevent memory leaks on re-render
+ this.monthOptionButtonsRef = {};
+
return this.props.monthNames.map(
(month: string, i: number): React.ReactElement => (
{
+ this.monthOptionButtonsRef[i] = el;
+ if (this.isSelectedMonth(i)) {
+ el?.focus();
+ }
+ }}
+ role="button"
+ tabIndex={0}
className={
this.isSelectedMonth(i)
? "react-datepicker__month-option react-datepicker__month-option--selected_month"
@@ -23,6 +58,7 @@ export default class MonthDropdownOptions extends Component
{this.isSelectedMonth(i) ? (
diff --git a/src/test/month_dropdown_test.test.tsx b/src/test/month_dropdown_test.test.tsx
index ac3d918c1c..0fb5f4abad 100644
--- a/src/test/month_dropdown_test.test.tsx
+++ b/src/test/month_dropdown_test.test.tsx
@@ -192,6 +192,105 @@ describe("MonthDropdown", () => {
dropdownDateFormat = getMonthDropdown({ locale: "ru" });
expect(dropdownDateFormat.textContent).toContain("декабрь");
});
+
+ it("calls the supplied onChange function when a month is selected using arrows and enter key", () => {
+ const monthReadView = safeQuerySelector(
+ monthDropdown,
+ ".react-datepicker__month-read-view",
+ );
+ fireEvent.click(monthReadView);
+
+ const monthOptions = safeQuerySelectorAll(
+ monthDropdown,
+ ".react-datepicker__month-option",
+ );
+
+ const monthOption = monthOptions[3]!;
+ fireEvent.keyDown(monthOption, { key: "ArrowDown" });
+
+ const nextMonthOption = monthOptions[4];
+ expect(document.activeElement).toEqual(nextMonthOption);
+
+ fireEvent.keyDown(document.activeElement!, { key: "Enter" });
+ expect(handleChangeResult).toEqual(4);
+ });
+
+ it("handles ArrowUp key navigation correctly", () => {
+ const monthReadView = safeQuerySelector(
+ monthDropdown,
+ ".react-datepicker__month-read-view",
+ );
+ fireEvent.click(monthReadView);
+
+ const monthOptions = safeQuerySelectorAll(
+ monthDropdown,
+ ".react-datepicker__month-option",
+ );
+
+ const monthOption = monthOptions[5]!;
+ fireEvent.keyDown(monthOption, { key: "ArrowUp" });
+
+ const prevMonthOption = monthOptions[4];
+ expect(document.activeElement).toEqual(prevMonthOption);
+ });
+
+ it("handles Escape key to cancel dropdown", () => {
+ const monthReadView = safeQuerySelector(
+ monthDropdown,
+ ".react-datepicker__month-read-view",
+ );
+ fireEvent.click(monthReadView);
+
+ const monthOptions = safeQuerySelectorAll(
+ monthDropdown,
+ ".react-datepicker__month-option",
+ );
+
+ const monthOption = monthOptions[5]!;
+ fireEvent.keyDown(monthOption, { key: "Escape" });
+
+ expect(
+ monthDropdown?.querySelectorAll(".react-datepicker__month-dropdown"),
+ ).toHaveLength(0);
+ });
+
+ it("wraps around when using ArrowUp on first month", () => {
+ const monthReadView = safeQuerySelector(
+ monthDropdown,
+ ".react-datepicker__month-read-view",
+ );
+ fireEvent.click(monthReadView);
+
+ const monthOptions = safeQuerySelectorAll(
+ monthDropdown,
+ ".react-datepicker__month-option",
+ );
+
+ const firstMonthOption = monthOptions[0]!;
+ fireEvent.keyDown(firstMonthOption, { key: "ArrowUp" });
+
+ const lastMonthOption = monthOptions[11];
+ expect(document.activeElement).toEqual(lastMonthOption);
+ });
+
+ it("wraps around when using ArrowDown on last month", () => {
+ const monthReadView = safeQuerySelector(
+ monthDropdown,
+ ".react-datepicker__month-read-view",
+ );
+ fireEvent.click(monthReadView);
+
+ const monthOptions = safeQuerySelectorAll(
+ monthDropdown,
+ ".react-datepicker__month-option",
+ );
+
+ const lastMonthOption = monthOptions[11]!;
+ fireEvent.keyDown(lastMonthOption, { key: "ArrowDown" });
+
+ const firstMonthOption = monthOptions[0];
+ expect(document.activeElement).toEqual(firstMonthOption);
+ });
});
describe("select mode", () => {
diff --git a/src/test/year_dropdown_options_test.test.tsx b/src/test/year_dropdown_options_test.test.tsx
index 370124cddd..59cddd894f 100644
--- a/src/test/year_dropdown_options_test.test.tsx
+++ b/src/test/year_dropdown_options_test.test.tsx
@@ -146,6 +146,76 @@ describe("YearDropdownOptions", () => {
expect(onCancelSpy).toHaveBeenCalledTimes(2);
});
+ it("handles Enter key to select year", () => {
+ const yearOptions = safeQuerySelectorAll(
+ yearDropdown,
+ ".react-datepicker__year-option",
+ );
+ const year2014Option = yearOptions.find((node) =>
+ node.textContent?.includes("2014"),
+ );
+
+ if (!year2014Option) {
+ throw new Error("Year 2014 not found!");
+ }
+
+ fireEvent.keyDown(year2014Option, { key: "Enter" });
+ expect(handleChangeResult).toBe(2014);
+ });
+
+ it("handles Escape key to cancel dropdown", () => {
+ const yearOptions = safeQuerySelectorAll(
+ yearDropdown,
+ ".react-datepicker__year-option",
+ );
+ const year2014Option = yearOptions.find((node) =>
+ node.textContent?.includes("2014"),
+ );
+
+ if (!year2014Option) {
+ throw new Error("Year 2014 not found!");
+ }
+
+ fireEvent.keyDown(year2014Option, { key: "Escape" });
+ expect(onCancelSpy).toHaveBeenCalled();
+ });
+
+ it("handles ArrowUp key navigation", () => {
+ const yearOptions = safeQuerySelectorAll(
+ yearDropdown,
+ ".react-datepicker__year-option",
+ );
+ const year2015Option = yearOptions.find((node) =>
+ node.textContent?.includes("✓2015"),
+ );
+
+ if (!year2015Option) {
+ throw new Error("Year 2015 not found!");
+ }
+
+ fireEvent.keyDown(year2015Option, { key: "ArrowUp" });
+ // ArrowUp should focus year 2016 (year + 1 in the code)
+ expect(document.activeElement?.textContent).toContain("2016");
+ });
+
+ it("handles ArrowDown key navigation", () => {
+ const yearOptions = safeQuerySelectorAll(
+ yearDropdown,
+ ".react-datepicker__year-option",
+ );
+ const year2015Option = yearOptions.find((node) =>
+ node.textContent?.includes("✓2015"),
+ );
+
+ if (!year2015Option) {
+ throw new Error("Year 2015 not found!");
+ }
+
+ fireEvent.keyDown(year2015Option, { key: "ArrowDown" });
+ // ArrowDown should focus year 2014 (year - 1 in the code)
+ expect(document.activeElement?.textContent).toContain("2014");
+ });
+
describe("selected", () => {
const className = "react-datepicker__year-option--selected_year";
let yearOptions: HTMLElement[];
diff --git a/src/test/year_dropdown_test.test.tsx b/src/test/year_dropdown_test.test.tsx
index 784a7ac21f..83a3db63ac 100644
--- a/src/test/year_dropdown_test.test.tsx
+++ b/src/test/year_dropdown_test.test.tsx
@@ -116,6 +116,28 @@ describe("YearDropdown", () => {
fireEvent.click(yearOption);
expect(lastOnChangeValue).toEqual(2014);
});
+
+ it("calls the supplied onChange function when a year is selected using arrows and enter key", () => {
+ const yearReadView = safeQuerySelector(
+ yearDropdown,
+ ".react-datepicker__year-read-view",
+ );
+ fireEvent.click(yearReadView);
+ const minYearOptionsLen = 7;
+ const yearOptions = safeQuerySelectorAll(
+ yearDropdown,
+ ".react-datepicker__year-option",
+ minYearOptionsLen,
+ );
+ const yearOption = yearOptions[6]!;
+ fireEvent.keyDown(yearOption, { key: "ArrowUp" });
+
+ const previousYearOption = yearOptions[5]!;
+ expect(document.activeElement).toBe(previousYearOption);
+
+ fireEvent.keyDown(document.activeElement!, { key: "Enter" });
+ expect(lastOnChangeValue).toEqual(2016);
+ });
});
describe("select mode", () => {
diff --git a/src/year_dropdown.tsx b/src/year_dropdown.tsx
index ac5e3da6bf..28678c0fad 100644
--- a/src/year_dropdown.tsx
+++ b/src/year_dropdown.tsx
@@ -12,7 +12,7 @@ interface YearDropdownProps
dropdownMode: "scroll" | "select";
onChange: (year: number) => void;
date: Date;
- onSelect?: (date: Date, event?: React.MouseEvent) => void;
+ onSelect?: (date: Date, event?: React.MouseEvent) => void;
setOpen?: (open: boolean) => void;
}
@@ -62,19 +62,18 @@ export default class YearDropdown extends Component<
);
renderReadView = (visible: boolean): React.ReactElement => (
- ): void =>
- this.toggleDropdown(event)
- }
+ onClick={this.toggleDropdown}
>
{this.props.year}
-
+
);
renderDropdown = (): React.ReactElement => (
@@ -101,7 +100,7 @@ export default class YearDropdown extends Component<
this.props.onChange(year);
};
- toggleDropdown = (event?: React.MouseEvent): void => {
+ toggleDropdown = (event?: React.MouseEvent): void => {
this.setState(
{
dropdownVisible: !this.state.dropdownVisible,
@@ -116,13 +115,16 @@ export default class YearDropdown extends Component<
handleYearChange = (
date: Date,
- event?: React.MouseEvent,
+ event?: React.MouseEvent,
): void => {
this.onSelect?.(date, event);
this.setOpen();
};
- onSelect = (date: Date, event?: React.MouseEvent): void => {
+ onSelect = (
+ date: Date,
+ event?: React.MouseEvent,
+ ): void => {
this.props.onSelect?.(date, event);
};
diff --git a/src/year_dropdown_options.tsx b/src/year_dropdown_options.tsx
index fcd9918501..773419b4d4 100644
--- a/src/year_dropdown_options.tsx
+++ b/src/year_dropdown_options.tsx
@@ -88,11 +88,46 @@ export default class YearDropdownOptions extends Component<
}
dropdownRef: React.RefObject;
+ yearOptionButtonsRef: Record = {};
+
+ handleOptionKeyDown = (year: number, e: React.KeyboardEvent): void => {
+ switch (e.key) {
+ case "Enter":
+ e.preventDefault();
+ this.onChange(year);
+ break;
+ case "Escape":
+ e.preventDefault();
+ this.props.onCancel();
+ break;
+ case "ArrowUp":
+ case "ArrowDown": {
+ e.preventDefault();
+ const newYear = year + (e.key === "ArrowUp" ? 1 : -1);
+ // Add bounds checking to ensure the year exists in our refs
+ if (this.yearOptionButtonsRef[newYear]) {
+ this.yearOptionButtonsRef[newYear]?.focus();
+ }
+ break;
+ }
+ }
+ };
renderOptions = (): React.ReactElement[] => {
+ // Clear refs to prevent memory leaks on re-render
+ this.yearOptionButtonsRef = {};
+
const selectedYear = this.props.year;
const options = this.state.yearsList.map((year) => (
{
+ this.yearOptionButtonsRef[year] = el;
+ if (year === selectedYear) {
+ el?.focus();
+ }
+ }}
+ role="button"
+ tabIndex={0}
className={
selectedYear === year
? "react-datepicker__year-option react-datepicker__year-option--selected_year"
@@ -100,6 +135,7 @@ export default class YearDropdownOptions extends Component<
}
key={year}
onClick={this.onChange.bind(this, year)}
+ onKeyDown={this.handleOptionKeyDown.bind(this, year)}
aria-selected={selectedYear === year ? "true" : undefined}
>
{selectedYear === year ? (