From 314d0029e58fc80918c5483bf0c6ddb25f236637 Mon Sep 17 00:00:00 2001 From: balajis-qb Date: Wed, 10 Jun 2026 10:45:37 +0530 Subject: [PATCH 1/2] =?UTF-8?q?=E2=99=BF=EF=B8=8F=20accessibility:=20Updat?= =?UTF-8?q?e=20month=20dropdown=20components=20for=20improved=20accessibil?= =?UTF-8?q?ity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Changed month option buttons from
to
  • elements to better reflect their role in a list. - Updated ARIA roles and attributes for better screen reader support, including setting role="option" and aria-haspopup="listbox". - Added aria-hidden to the down arrow and check mark for improved accessibility. - Enhanced test cases to verify ARIA attributes and accessibility features in the month dropdown. Closes #6223 --- src/month_dropdown.tsx | 8 ++- src/month_dropdown_options.tsx | 19 ++++-- src/test/month_dropdown_test.test.tsx | 96 +++++++++++++++++++++++++-- 3 files changed, 111 insertions(+), 12 deletions(-) diff --git a/src/month_dropdown.tsx b/src/month_dropdown.tsx index 9efd3c7279..c58d6223ff 100644 --- a/src/month_dropdown.tsx +++ b/src/month_dropdown.tsx @@ -62,8 +62,14 @@ export default class MonthDropdown extends Component< style={{ visibility: visible ? "visible" : "hidden" }} className="react-datepicker__month-read-view" onClick={this.toggleDropdown} + aria-expanded={this.state.dropdownVisible} + aria-haspopup="listbox" > - +
  • ), ); }; @@ -82,7 +87,7 @@ export default class MonthDropdownOptions extends Component - {this.renderOptions()} +
    {this.renderOptions()}
    ); } diff --git a/src/test/month_dropdown_test.test.tsx b/src/test/month_dropdown_test.test.tsx index 0fb5f4abad..39e4adf2ce 100644 --- a/src/test/month_dropdown_test.test.tsx +++ b/src/test/month_dropdown_test.test.tsx @@ -40,12 +40,100 @@ describe("MonthDropdown", () => { }); describe("scroll mode", () => { + const selectedMonthIndex = 11; + beforeEach(() => { - monthDropdown = getMonthDropdown(); + monthDropdown = getMonthDropdown({ + month: selectedMonthIndex, + }); + }); + + 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-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("marks the down arrow as aria-hidden so it is excluded from the accessibility tree", () => { + const downArrow = safeQuerySelector( + monthDropdown, + ".react-datepicker__month-read-view--down-arrow", + ); + + expect(downArrow.getAttribute("aria-hidden")).toBe("true"); + expect(downArrow.textContent).toBe(""); + }); + + it("renders a sr-only Month label for screen readers inside the read view button", () => { + const monthReadView = safeQuerySelector( + monthDropdown, + ".react-datepicker__month-read-view", + ); + const srOnlyLabel = safeQuerySelector( + monthReadView, + ".react-datepicker__sr-only", + ); + + expect(srOnlyLabel.textContent).toBe("Month"); + expect(srOnlyLabel.classList.contains("react-datepicker__sr-only")).toBe( + true, + ); + expect(srOnlyLabel.getAttribute("aria-hidden")).not.toBe("true"); + }); + + it("applies aria-selected to the selected month option in scroll dropdown", () => { + const monthReadView = safeQuerySelector( + monthDropdown, + ".react-datepicker__month-read-view", + ); + fireEvent.click(monthReadView); + + const allMonthOptions = safeQuerySelectorAll( + monthDropdown, + ".react-datepicker__month-option", + ); + allMonthOptions.forEach((option, idx) => { + expect(option.getAttribute("aria-selected")).toBe( + idx === selectedMonthIndex ? "true" : "false", + ); + }); + }); + + it("applies aria-hidden to the selected month option's check mark in scroll dropdown", () => { + const monthReadView = safeQuerySelector( + monthDropdown, + ".react-datepicker__month-read-view", + ); + fireEvent.click(monthReadView); + const allMonthOptions = safeQuerySelectorAll( + monthDropdown, + ".react-datepicker__month-option", + ); + + const selectedMonthOption = allMonthOptions[selectedMonthIndex]; + expect(selectedMonthOption).not.toBeNull(); + + const checkSpan = selectedMonthOption?.querySelector( + "span.react-datepicker__month-option--selected", + ); + expect(checkSpan).not.toBeNull(); + expect(checkSpan?.getAttribute("aria-hidden")).toBe("true"); }); it("shows the selected month in the initial view", () => { - expect(monthDropdown?.textContent).toContain("December"); + const expectedMonthName = getMonthInLocale(selectedMonthIndex); + expect(monthDropdown?.textContent).toContain(expectedMonthName); }); it("opens a list when read view is clicked", () => { @@ -102,9 +190,9 @@ describe("MonthDropdown", () => { expect(notSelectedMonth?.textContent).not.toContain("December"); }); - it("does not add aria-selected property to the selected month", () => { + it("should have aria-selected set to false", () => { const ariaSelected = notSelectedMonth?.getAttribute("aria-selected"); - expect(ariaSelected).toBeNull(); + expect(ariaSelected).toBe("false"); }); }); From 2e42f63d2f96b1357d64b4fa05866746a453daab Mon Sep 17 00:00:00 2001 From: balajis-qb Date: Wed, 10 Jun 2026 10:48:17 +0530 Subject: [PATCH 2/2] =?UTF-8?q?=E2=99=BF=EF=B8=8F=20accessibility:=20Enhan?= =?UTF-8?q?ce=20year=20dropdown=20components=20for=20improved=20usability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated year option buttons from
    to
  • elements to better represent their role in a list. - Changed ARIA roles and attributes for better screen reader support, including setting role="option" and aria-haspopup="listbox". - Added aria-hidden to the down arrow and check mark for improved accessibility. - Introduced a new
      wrapper for year options to enhance semantic structure. - Enhanced test cases to verify ARIA attributes and accessibility features in the year dropdown. Closes #6223 --- src/stylesheets/datepicker.scss | 7 + src/test/year_dropdown_options_test.test.tsx | 185 +++++++++++++------ src/test/year_dropdown_test.test.tsx | 77 +++++++- src/year_dropdown.tsx | 8 +- src/year_dropdown_options.tsx | 53 +++--- 5 files changed, 254 insertions(+), 76 deletions(-) diff --git a/src/stylesheets/datepicker.scss b/src/stylesheets/datepicker.scss index 57b8445f95..5ed74b3395 100644 --- a/src/stylesheets/datepicker.scss +++ b/src/stylesheets/datepicker.scss @@ -222,6 +222,7 @@ h2.react-datepicker__current-month { display: block; margin-left: auto; margin-right: auto; + width: 100%; &-previous { top: 4px; @@ -676,6 +677,12 @@ h2.react-datepicker__current-month { } } +.react-datepicker__year-options-list { + margin: 0; + padding: 0; + list-style: none; +} + .react-datepicker__year-option, .react-datepicker__month-option, .react-datepicker__month-year-option { diff --git a/src/test/year_dropdown_options_test.test.tsx b/src/test/year_dropdown_options_test.test.tsx index 59cddd894f..2c1e0e8b1d 100644 --- a/src/test/year_dropdown_options_test.test.tsx +++ b/src/test/year_dropdown_options_test.test.tsx @@ -1,4 +1,5 @@ import { render, fireEvent } from "@testing-library/react"; +import { userEvent } from "@testing-library/user-event"; import React from "react"; import { addYears, getYear, newDate, subYears } from "../date_utils"; @@ -6,6 +7,12 @@ import YearDropdownOptions from "../year_dropdown_options"; import { safeQuerySelector, safeQuerySelectorAll } from "./test_utils"; +function getYearOptionTextContents(container: HTMLElement): string[] { + return Array.from( + container.querySelectorAll(".react-datepicker__year-option"), + ).map((node) => node.textContent ?? ""); +} + describe("YearDropdownOptions", () => { let yearDropdown: HTMLElement, handleChangeResult: number; const mockHandleChange = function (changeInput: number) { @@ -57,64 +64,136 @@ describe("YearDropdownOptions", () => { expect(yearsListLength).toBe(11); }); - it("increments the available years when the 'upcoming years' button is clicked", () => { - const navigationYearsUpcoming = safeQuerySelector( - yearDropdown, - ".react-datepicker__navigation--years-upcoming", - ); - fireEvent.click(navigationYearsUpcoming); + describe("year navigation buttons", () => { + it("renders upcoming and previous year controls as buttons with accessible labels", () => { + const upcomingButton = safeQuerySelector( + yearDropdown, + ".react-datepicker__navigation--years-upcoming", + ); + const previousButton = safeQuerySelector( + yearDropdown, + ".react-datepicker__navigation--years-previous", + ); - const textContents = Array.from( - yearDropdown?.querySelectorAll(".react-datepicker__year-option") ?? [], - ).map((node) => node.textContent); + expect(upcomingButton.tagName).toBe("BUTTON"); + expect(upcomingButton.getAttribute("aria-label")).toBe( + "Show later years", + ); + expect(previousButton.tagName).toBe("BUTTON"); + expect(previousButton.getAttribute("aria-label")).toBe( + "Show earlier years", + ); + }); - expect(textContents).toEqual( - expect.arrayContaining([ - "", - "2021", - "2020", - "2019", - "2018", - "2017", - "2016", - "✓2015", - "2014", - "2013", - "2012", - "2011", - "", - ]), - ); - }); + it("increments the available years when the upcoming years button is clicked", () => { + const upcomingButton = safeQuerySelector( + yearDropdown, + ".react-datepicker__navigation--years-upcoming", + ); + fireEvent.click(upcomingButton); + + expect(getYearOptionTextContents(yearDropdown)).toEqual( + expect.arrayContaining([ + "", + "2021", + "2020", + "2019", + "2018", + "2017", + "2016", + "✓2015", + "2014", + "2013", + "2012", + "2011", + "", + ]), + ); + }); - it("decrements the available years when the 'previous years' button is clicked", () => { - const navigationYearsPrevious = safeQuerySelector( - yearDropdown, - ".react-datepicker__navigation--years-previous", - ); - fireEvent.click(navigationYearsPrevious); + it("decrements the available years when the previous years button is clicked", () => { + const previousButton = safeQuerySelector( + yearDropdown, + ".react-datepicker__navigation--years-previous", + ); + fireEvent.click(previousButton); + + expect(getYearOptionTextContents(yearDropdown)).toEqual( + expect.arrayContaining([ + "", + "2019", + "2018", + "2017", + "2016", + "✓2015", + "2014", + "2013", + "2012", + "2011", + "2010", + "2009", + "", + ]), + ); + }); - const textContents = Array.from( - yearDropdown?.querySelectorAll(".react-datepicker__year-option") ?? [], - ).map((node) => node.textContent); + it("increments the available years when Enter is pressed on the upcoming years button", async () => { + const user = userEvent.setup(); + const upcomingButton = safeQuerySelector( + yearDropdown, + ".react-datepicker__navigation--years-upcoming", + ); - expect(textContents).toEqual( - expect.arrayContaining([ - "", - "2019", - "2018", - "2017", - "2016", - "✓2015", - "2014", - "2013", - "2012", - "2011", - "2010", - "2009", - "", - ]), - ); + upcomingButton.focus(); + await user.keyboard("{Enter}"); + + expect(getYearOptionTextContents(yearDropdown)).toEqual( + expect.arrayContaining([ + "", + "2021", + "2020", + "2019", + "2018", + "2017", + "2016", + "✓2015", + "2014", + "2013", + "2012", + "2011", + "", + ]), + ); + }); + + it("decrements the available years when Enter is pressed on the previous years button", async () => { + const user = userEvent.setup(); + const previousButton = safeQuerySelector( + yearDropdown, + ".react-datepicker__navigation--years-previous", + ); + + previousButton.focus(); + await user.keyboard("{Enter}"); + + expect(getYearOptionTextContents(yearDropdown)).toEqual( + expect.arrayContaining([ + "", + "2019", + "2018", + "2017", + "2016", + "✓2015", + "2014", + "2013", + "2012", + "2011", + "2010", + "2009", + "", + ]), + ); + }); }); it("calls the supplied onChange function when a year is clicked", () => { diff --git a/src/test/year_dropdown_test.test.tsx b/src/test/year_dropdown_test.test.tsx index 83a3db63ac..89dba5af0d 100644 --- a/src/test/year_dropdown_test.test.tsx +++ b/src/test/year_dropdown_test.test.tsx @@ -34,8 +34,24 @@ 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( + yearDropdown, + ".react-datepicker__year-read-view", + ); + expect(yearReadView.getAttribute("aria-haspopup")).toBe("listbox"); + 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", () => { @@ -49,6 +65,65 @@ describe("YearDropdown", () => { expect(optionsView).toHaveLength(0); }); + it("marks the down arrow as aria-hidden so it is excluded from the accessibility tree", () => { + const downArrow = safeQuerySelector( + yearDropdown, + ".react-datepicker__year-read-view--down-arrow", + ); + + expect(downArrow.getAttribute("aria-hidden")).toBe("true"); + expect(downArrow.textContent).toBe(""); + }); + + it("renders a sr-only Year label for screen readers inside the read view button", () => { + const yearReadView = safeQuerySelector( + yearDropdown, + ".react-datepicker__year-read-view", + ); + const srOnlyLabel = safeQuerySelector( + yearReadView, + ".react-datepicker__sr-only", + ); + + expect(srOnlyLabel.textContent).toBe("Year"); + expect(srOnlyLabel.classList.contains("react-datepicker__sr-only")).toBe( + true, + ); + expect(srOnlyLabel.getAttribute("aria-hidden")).not.toBe("true"); + }); + + it("applies aria-selected to the selected year option in scroll dropdown", () => { + const yearReadView = safeQuerySelector( + yearDropdown, + ".react-datepicker__year-read-view", + ); + fireEvent.click(yearReadView); + + const selectedYearOption = safeQuerySelector( + yearDropdown, + ".react-datepicker__year-option--selected_year", + ); + expect(selectedYearOption.getAttribute("aria-selected")).toBe("true"); + }); + + it("applies aria-hidden to the selected year option's check mark in scroll dropdown", () => { + const yearReadView = safeQuerySelector( + yearDropdown, + ".react-datepicker__year-read-view", + ); + fireEvent.click(yearReadView); + const selectedYearOption = safeQuerySelector( + yearDropdown, + ".react-datepicker__year-option--selected_year", + ); + + const checkSpan = selectedYearOption?.querySelector( + "span.react-datepicker__year-option--selected", + ); + expect(checkSpan).not.toBeNull(); + expect(checkSpan?.getAttribute("aria-hidden")).toBe("true"); + }); + it("opens a list when read view is clicked", () => { const yearReadView = safeQuerySelector( yearDropdown, diff --git a/src/year_dropdown.tsx b/src/year_dropdown.tsx index c3bd83169d..7286d15d55 100644 --- a/src/year_dropdown.tsx +++ b/src/year_dropdown.tsx @@ -71,8 +71,14 @@ export default class YearDropdown extends Component< style={{ visibility: visible ? "visible" : "hidden" }} className="react-datepicker__year-read-view" onClick={this.toggleDropdown} + aria-expanded={this.state.dropdownVisible} + aria-haspopup="listbox" > - +