Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/month_dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ export default class MonthDropdown extends Component<
visible: boolean,
monthNames: string[],
): React.ReactElement => (
<div
<button
key="read"
type="button"
style={{ visibility: visible ? "visible" : "hidden" }}
className="react-datepicker__month-read-view"
onClick={this.toggleDropdown}
Expand All @@ -66,7 +67,7 @@ export default class MonthDropdown extends Component<
<span className="react-datepicker__month-read-view--selected-month">
{monthNames[this.props.month]}
</span>
</div>
</button>
);

renderDropdown = (monthNames: string[]): React.ReactElement => (
Expand Down
36 changes: 36 additions & 0 deletions src/month_dropdown_options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,55 @@ interface MonthDropdownOptionsProps {
}

export default class MonthDropdownOptions extends Component<MonthDropdownOptionsProps> {
monthOptionButtonsRef: Record<number, HTMLDivElement | null> = {};

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<React.ReactElement>(
(month: string, i: number): React.ReactElement => (
<div
ref={(el) => {
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"
: "react-datepicker__month-option"
}
key={month}
onClick={this.onChange.bind(this, i)}
onKeyDown={this.handleOptionKeyDown.bind(this, i)}
aria-selected={this.isSelectedMonth(i) ? "true" : undefined}
>
{this.isSelectedMonth(i) ? (
Expand Down
99 changes: 99 additions & 0 deletions src/test/month_dropdown_test.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
70 changes: 70 additions & 0 deletions src/test/year_dropdown_options_test.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
22 changes: 22 additions & 0 deletions src/test/year_dropdown_test.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
20 changes: 11 additions & 9 deletions src/year_dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface YearDropdownProps
dropdownMode: "scroll" | "select";
onChange: (year: number) => void;
date: Date;
onSelect?: (date: Date, event?: React.MouseEvent<HTMLDivElement>) => void;
onSelect?: (date: Date, event?: React.MouseEvent<HTMLButtonElement>) => void;
setOpen?: (open: boolean) => void;
}

Expand Down Expand Up @@ -62,19 +62,18 @@ export default class YearDropdown extends Component<
);

renderReadView = (visible: boolean): React.ReactElement => (
<div
<button
key="read"
type="button"
style={{ visibility: visible ? "visible" : "hidden" }}
className="react-datepicker__year-read-view"
onClick={(event: React.MouseEvent<HTMLDivElement>): void =>
this.toggleDropdown(event)
}
onClick={this.toggleDropdown}
>
<span className="react-datepicker__year-read-view--down-arrow" />
<span className="react-datepicker__year-read-view--selected-year">
{this.props.year}
</span>
</div>
</button>
);

renderDropdown = (): React.ReactElement => (
Expand All @@ -101,7 +100,7 @@ export default class YearDropdown extends Component<
this.props.onChange(year);
};

toggleDropdown = (event?: React.MouseEvent<HTMLDivElement>): void => {
toggleDropdown = (event?: React.MouseEvent<HTMLButtonElement>): void => {
this.setState(
{
dropdownVisible: !this.state.dropdownVisible,
Expand All @@ -116,13 +115,16 @@ export default class YearDropdown extends Component<

handleYearChange = (
date: Date,
event?: React.MouseEvent<HTMLDivElement>,
event?: React.MouseEvent<HTMLButtonElement>,
): void => {
this.onSelect?.(date, event);
this.setOpen();
};

onSelect = (date: Date, event?: React.MouseEvent<HTMLDivElement>): void => {
onSelect = (
date: Date,
event?: React.MouseEvent<HTMLButtonElement>,
): void => {
this.props.onSelect?.(date, event);
};

Expand Down
Loading
Loading