Skip to content
Open
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
16 changes: 14 additions & 2 deletions src/month_dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ export default class MonthDropdown extends Component<
renderSelectOptions = (monthNames: string[]): React.ReactElement[] =>
monthNames.map<React.ReactElement>(
(m: string, i: number): React.ReactElement => (
<option key={m} value={i}>
<option
key={m}
value={i}
aria-label={`Select Month ${m}`}
aria-selected={i === this.props.month ? "true" : "false"}
>
{m}
</option>
),
Expand All @@ -47,6 +52,7 @@ export default class MonthDropdown extends Component<
value={this.props.month}
className="react-datepicker__month-select"
onChange={(e) => this.onChange(parseInt(e.target.value))}
aria-label="Select Month"
>
{this.renderSelectOptions(monthNames)}
</select>
Expand All @@ -62,8 +68,14 @@ export default class MonthDropdown extends Component<
style={{ visibility: visible ? "visible" : "hidden" }}
className="react-datepicker__month-read-view"
onClick={this.toggleDropdown}
aria-label="Select Month"
aria-expanded={this.state.dropdownVisible}
aria-haspopup="listbox"
>
<span className="react-datepicker__month-read-view--down-arrow" />
<span
className="react-datepicker__month-read-view--down-arrow"
aria-hidden="true"
/>
<span className="react-datepicker__month-read-view--selected-month">
{monthNames[this.props.month]}
</span>
Expand Down
1 change: 1 addition & 0 deletions src/month_dropdown_options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export default class MonthDropdownOptions extends Component<MonthDropdownOptions
}
}}
role="button"
aria-label={`Select Month ${month}`}
tabIndex={0}
className={
this.isSelectedMonth(i)
Expand Down
60 changes: 60 additions & 0 deletions src/test/month_dropdown_test.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,40 @@ describe("MonthDropdown", () => {
monthDropdown = getMonthDropdown();
});

it("sets proper ARIA on read view button and toggles aria-expanded", () => {
const monthReadView = safeQuerySelector(
monthDropdown,
".react-datepicker__month-read-view",
);
expect(monthReadView.getAttribute("aria-label")).toBe("Select Month");
expect(monthReadView.getAttribute("aria-haspopup")).toBe("listbox");
expect(monthReadView.getAttribute("aria-expanded")).toBe("false");

fireEvent.click(monthReadView);

const monthReadViewAfterOpen = safeQuerySelector(
monthDropdown,
".react-datepicker__month-read-view",
);
expect(monthReadViewAfterOpen.getAttribute("aria-expanded")).toBe("true");
});

it("applies aria-label to each month option in scroll dropdown", () => {
const monthReadView = safeQuerySelector(
monthDropdown,
".react-datepicker__month-read-view",
);
fireEvent.click(monthReadView);

const firstOption = safeQuerySelector(
monthDropdown,
".react-datepicker__month-option",
);
expect(firstOption.getAttribute("aria-label")).toBe(
"Select Month January",
);
});

it("shows the selected month in the initial view", () => {
expect(monthDropdown?.textContent).toContain("December");
});
Expand Down Expand Up @@ -307,6 +341,14 @@ describe("MonthDropdown", () => {
);
});

it("adds aria-label to select element", () => {
monthDropdown = getMonthDropdown({ dropdownMode: "select" });
const select = monthDropdown.querySelector<HTMLSelectElement>(
".react-datepicker__month-select",
);
expect(select?.getAttribute("aria-label")).toBe("Select Month");
});

it("renders month options with default locale", () => {
monthDropdown = getMonthDropdown({ dropdownMode: "select" });
const options = monthDropdown.querySelectorAll("option");
Expand All @@ -325,6 +367,24 @@ describe("MonthDropdown", () => {
"December",
]);
});
// Accessibility of options
it("adds aria-label and aria-selected to options in select mode", () => {
monthDropdown = getMonthDropdown({ dropdownMode: "select", month: 11 });
const select = monthDropdown.querySelector<HTMLSelectElement>(
".react-datepicker__month-select",
);
const options = Array.from(
select?.querySelectorAll("option") ?? [],
) as HTMLOptionElement[];
expect(options[0]?.getAttribute("aria-label")).toBe(
"Select Month January",
);
expect(options[11]?.getAttribute("aria-label")).toBe(
"Select Month December",
);
expect(options[11]?.getAttribute("aria-selected")).toBe("true");
expect(options[0]?.getAttribute("aria-selected")).toBe("false");
});
// Short Month Names
it("renders month options with short name and default locale", () => {
monthDropdown = getMonthDropdown({
Expand Down
142 changes: 140 additions & 2 deletions src/test/year_dropdown_test.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,25 @@ describe("YearDropdown", () => {
});

describe("scroll mode", () => {
const selectedYear = 2015;

beforeEach(function () {
yearDropdown = getYearDropdown();
yearDropdown = getYearDropdown({
year: selectedYear,
});
});

it("read view has correct ARIA attributes and toggles aria-expanded", () => {
const yearReadView = safeQuerySelector<HTMLButtonElement>(
yearDropdown,
".react-datepicker__year-read-view",
);
expect(yearReadView.getAttribute("aria-haspopup")).toBe("listbox");
expect(yearReadView.getAttribute("aria-label")).toBe("Select Year");
expect(yearReadView.getAttribute("aria-expanded")).toBe("false");

fireEvent.click(yearReadView);
expect(yearReadView.getAttribute("aria-expanded")).toBe("true");
});

it("shows the selected year in the initial view", () => {
Expand Down Expand Up @@ -138,15 +155,109 @@ describe("YearDropdown", () => {
fireEvent.keyDown(document.activeElement!, { key: "Enter" });
expect(lastOnChangeValue).toEqual(2016);
});

it("options expose correct ARIA attributes", () => {
const yearReadView = safeQuerySelector(
yearDropdown,
".react-datepicker__year-read-view",
);
fireEvent.click(yearReadView);

const yearOptions = safeQuerySelectorAll<HTMLDivElement>(
yearDropdown,
".react-datepicker__year-option",
7,
);

// Find the selected year option by text
const selected = yearOptions.find((el) =>
el.textContent?.includes(selectedYear.toString()),
)!;
expect(selected.getAttribute("aria-selected")).toBe("true");
expect(selected.getAttribute("aria-label")).toBe(
`Select Year ${selectedYear}`,
);

// Find a non-selected year option and ensure aria-selected is not present
const nonSelected =
yearOptions.find(
(el) => el.textContent?.trim() === (selectedYear - 1).toString(),
) ??
yearOptions.find(
(el) => el.textContent?.trim() === (selectedYear + 1).toString(),
);
expect(nonSelected).toBeTruthy();
expect(nonSelected!.getAttribute("aria-selected")).toBeNull();
const nonSelectedYear = nonSelected!.textContent!.trim();
expect(nonSelected!.getAttribute("aria-label")).toBe(
`Select Year ${nonSelectedYear}`,
);
});

it("pressing Escape closes the dropdown (onCancel)", () => {
const yearReadView = safeQuerySelector(
yearDropdown,
".react-datepicker__year-read-view",
);
fireEvent.click(yearReadView);

const yearOptions = safeQuerySelectorAll<HTMLDivElement>(
yearDropdown,
".react-datepicker__year-option",
7,
);
// Focus the selected option and press Escape
const selected = yearOptions.find((el) =>
el.textContent?.includes("2015"),
)!;
selected.focus();
fireEvent.keyDown(selected, { key: "Escape" });

const optionsView = yearDropdown?.querySelectorAll(
"react-datepicker__year-dropdown",
);
expect(optionsView).toHaveLength(0);
});

it("clicking 'Show later years' shifts the years forward by one", () => {
const yearReadView = safeQuerySelector(
yearDropdown,
".react-datepicker__year-read-view",
);
fireEvent.click(yearReadView);

// The first option is the 'Show later years' control when no maxDate is provided
const yearOptionsBefore = safeQuerySelectorAll<HTMLDivElement>(
yearDropdown,
".react-datepicker__year-option",
7,
);
const firstYearBefore = Number(
yearOptionsBefore[1]!.textContent?.trim(), // index 0 is the navigation control
);

// Click the navigation control to shift years
fireEvent.click(yearOptionsBefore[0]!);

const yearOptionsAfter = safeQuerySelectorAll<HTMLDivElement>(
yearDropdown,
".react-datepicker__year-option",
7,
);
const firstYearAfter = Number(yearOptionsAfter[1]!.textContent?.trim());
expect(firstYearAfter).toBe(firstYearBefore + 1);
});
});

describe("select mode", () => {
const selectedYear = 2015;

it("renders a select with default year range options", () => {
yearDropdown = getYearDropdown({ dropdownMode: "select" });
const select: NodeListOf<HTMLSelectElement> =
yearDropdown.querySelectorAll(".react-datepicker__year-select");
expect(select).toHaveLength(1);
expect(select[0]?.value).toBe("2015");
expect(select[0]?.value).toBe(selectedYear.toString());

const options = select[0]?.querySelectorAll("option") ?? [];
expect(Array.from(options).map((o) => o.textContent)).toEqual(
Expand Down Expand Up @@ -206,5 +317,32 @@ describe("YearDropdown", () => {
expect(onSelectSpy).toHaveBeenCalledTimes(1);
expect(setOpenSpy).toHaveBeenCalledTimes(1);
});

it("select and options expose correct ARIA attributes", () => {
yearDropdown = getYearDropdown({ dropdownMode: "select" });
const select: HTMLSelectElement =
yearDropdown.querySelector(".react-datepicker__year-select") ??
new HTMLSelectElement();

expect(select.getAttribute("aria-label")).toBe("Select Year");

const options = Array.from(
select.querySelectorAll("option"),
) as HTMLOptionElement[];
const opt2015 = options.find((o) => o.value === selectedYear.toString())!;
const opt2014 = options.find(
(o) => o.value === (selectedYear - 1).toString(),
)!;

expect(opt2015.getAttribute("aria-selected")).toBe("true");
expect(opt2015.getAttribute("aria-label")).toBe(
`Select Year ${selectedYear}`,
);

expect(opt2014.getAttribute("aria-selected")).toBe("false");
expect(opt2014.getAttribute("aria-label")).toBe(
`Select Year ${selectedYear - 1}`,
);
});
});
});
16 changes: 14 additions & 2 deletions src/year_dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ export default class YearDropdown extends Component<
const options: React.ReactElement[] = [];
for (let i = minYear; i <= maxYear; i++) {
options.push(
<option key={i} value={i}>
<option
key={i}
value={i}
aria-label={`Select Year ${i}`}
aria-selected={i === this.props.year ? "true" : "false"}
>
{i}
</option>,
);
Expand All @@ -59,6 +64,7 @@ export default class YearDropdown extends Component<
value={this.props.year}
className="react-datepicker__year-select"
onChange={this.onSelectChange}
aria-label="Select Year"
>
{this.renderSelectOptions()}
</select>
Expand All @@ -71,8 +77,14 @@ export default class YearDropdown extends Component<
style={{ visibility: visible ? "visible" : "hidden" }}
className="react-datepicker__year-read-view"
onClick={this.toggleDropdown}
aria-label="Select Year"
aria-expanded={this.state.dropdownVisible}
aria-haspopup="listbox"
>
<span className="react-datepicker__year-read-view--down-arrow" />
<span
className="react-datepicker__year-read-view--down-arrow"
aria-hidden="true"
/>
<span className="react-datepicker__year-read-view--selected-year">
{this.props.year}
</span>
Expand Down
5 changes: 5 additions & 0 deletions src/year_dropdown_options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export default class YearDropdownOptions extends Component<
key={year}
onClick={this.onChange.bind(this, year)}
onKeyDown={this.handleOptionKeyDown.bind(this, year)}
aria-label={`Select Year ${year}`}
aria-selected={selectedYear === year ? "true" : undefined}
>
{selectedYear === year ? (
Expand All @@ -156,6 +157,8 @@ export default class YearDropdownOptions extends Component<
className="react-datepicker__year-option"
key={"upcoming"}
onClick={this.incrementYears}
role="button"
aria-label="Show later years"
>
<a className="react-datepicker__navigation react-datepicker__navigation--years react-datepicker__navigation--years-upcoming" />
</div>,
Expand All @@ -168,6 +171,8 @@ export default class YearDropdownOptions extends Component<
className="react-datepicker__year-option"
key={"previous"}
onClick={this.decrementYears}
role="button"
aria-label="Show earlier years"
>
<a className="react-datepicker__navigation react-datepicker__navigation--years react-datepicker__navigation--years-previous" />
</div>,
Expand Down
Loading