diff --git a/src/calendar.tsx b/src/calendar.tsx index 3385481ea..c667c9e39 100644 --- a/src/calendar.tsx +++ b/src/calendar.tsx @@ -477,8 +477,9 @@ export default class Calendar extends Component { const dayNames: React.ReactElement[] = []; if (this.props.showWeekNumbers) { dayNames.push( -
- {this.props.weekLabel || "#"} +
+ Week number +
, ); } @@ -494,10 +495,13 @@ export default class Calendar extends Component { return (
- {weekDayName} + + {formatDate(day, "EEEE", this.props.locale)} + +
); }), @@ -852,7 +856,7 @@ export default class Calendar extends Component { {this.renderMonthYearDropdown(i !== 0)} {this.renderYearDropdown(i !== 0)}
-
+
{this.header(monthDate)}
diff --git a/src/stylesheets/datepicker.scss b/src/stylesheets/datepicker.scss index abc7531a2..659f5de4e 100644 --- a/src/stylesheets/datepicker.scss +++ b/src/stylesheets/datepicker.scss @@ -2,6 +2,19 @@ @use "variables" as *; @use "mixins" as *; +/* sr-only utility class for accessibility */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + .react-datepicker-wrapper { display: inline-block; padding: 0; diff --git a/src/test/calendar_test.test.tsx b/src/test/calendar_test.test.tsx index 89737c60e..a4ef50189 100644 --- a/src/test/calendar_test.test.tsx +++ b/src/test/calendar_test.test.tsx @@ -294,7 +294,9 @@ describe("Calendar", () => { it("should correctly format weekday using formatWeekDay prop", () => { const { calendar } = getCalendar({ formatWeekDay: (day) => day.charAt(0) }); calendar - .querySelectorAll(".react-datepicker__day-name") + .querySelectorAll( + ".react-datepicker__day-name > span[aria-hidden='true']", + ) .forEach((dayName) => expect(dayName.textContent).toHaveLength(1)); }); @@ -1130,7 +1132,9 @@ describe("Calendar", () => { it("should use a hash for week label if weekLabel is NOT provided", () => { const { calendar } = getCalendar({ showWeekNumbers: true }); - const weekLabel = calendar.querySelectorAll(".react-datepicker__day-name"); + const weekLabel = calendar.querySelectorAll( + ".react-datepicker__day-name > span[aria-hidden='true']", + ); expect(weekLabel[0]?.textContent).toBe("#"); }); @@ -1139,7 +1143,9 @@ describe("Calendar", () => { showWeekNumbers: true, weekLabel: "Foo", }); - const weekLabel = calendar.querySelectorAll(".react-datepicker__day-name"); + const weekLabel = calendar.querySelectorAll( + ".react-datepicker__day-name > span[aria-hidden='true']", + ); expect(weekLabel[0]?.textContent).toBe("Foo"); }); @@ -1252,13 +1258,13 @@ describe("Calendar", () => { ).container; const daysNamesShort = calendarShort.querySelectorAll( - ".react-datepicker__day-name", + ".react-datepicker__day-name > span[aria-hidden='true']", ); expect(daysNamesShort[0]?.textContent).toBe("Sun"); expect(daysNamesShort[6]?.textContent).toBe("Sat"); const daysNamesMin = calendarMin.querySelectorAll( - ".react-datepicker__day-name", + ".react-datepicker__day-name > span[aria-hidden='true']", ); expect(daysNamesMin[0]?.textContent).toBe("Su"); expect(daysNamesMin[6]?.textContent).toBe("Sa"); @@ -1614,7 +1620,9 @@ describe("Calendar", () => { calendarStartDay, ); const firstWeekDayMin = getWeekdayMinInLocale(firstDateOfWeek, locale); - const firstHeader = calendar.querySelector(".react-datepicker__day-name"); + const firstHeader = calendar.querySelector( + ".react-datepicker__day-name > span[aria-hidden='true']", + ); expect(firstHeader?.textContent).toBe(firstWeekDayMin); } @@ -2236,13 +2244,11 @@ describe("Calendar", () => { const header = container.querySelector(".react-datepicker__header"); const dayNameElements = header?.querySelectorAll( - ".react-datepicker__day-name", + ".react-datepicker__day-name > span.sr-only", ); dayNameElements?.forEach((element, index) => { - expect(element.getAttribute("aria-label")).toBe( - expectedAriaLabels[index], - ); + expect(element.textContent).toBe(expectedAriaLabels[index]); }); }); @@ -2500,7 +2506,7 @@ describe("Calendar", () => { it("should have default sunday as start day if No prop passed", () => { const { calendar } = getCalendar(); const calendarDays = calendar.querySelectorAll( - ".react-datepicker__day-name", + ".react-datepicker__day-name > span[aria-hidden='true']", ); expect(calendarDays[0]?.textContent).toBe("Su"); expect(calendarDays[6]?.textContent).toBe("Sa"); @@ -2509,7 +2515,7 @@ describe("Calendar", () => { it("should have default wednesday as start day if No prop passed", () => { const { calendar } = getCalendar({ calendarStartDay: 3 }); const calendarDays = calendar.querySelectorAll( - ".react-datepicker__day-name", + ".react-datepicker__day-name > span[aria-hidden='true']", ); expect(calendarDays[0]?.textContent).toBe("We"); expect(calendarDays[6]?.textContent).toBe("Tu"); diff --git a/src/test/datepicker_test.test.tsx b/src/test/datepicker_test.test.tsx index d69a3919d..e9a76330c 100644 --- a/src/test/datepicker_test.test.tsx +++ b/src/test/datepicker_test.test.tsx @@ -3794,6 +3794,54 @@ describe("DatePicker", () => { }); }); + describe("Calendar Header Accessibility", () => { + it("renders day names with sr-only full weekday and visible short name", () => { + const { container } = render(); + const input = safeQuerySelector(container, "input"); + fireEvent.focus(input); + + const headers = container.querySelectorAll( + '.react-datepicker__day-names > [role="columnheader"]', + ); + expect(headers.length).toBe(7); + + headers.forEach((header) => { + // Should have a visually hidden span with the full weekday name + const srOnly = header.querySelector(".sr-only"); + expect(srOnly).toBeTruthy(); + expect(srOnly?.textContent?.length).toBeGreaterThan(2); + + // Should have a visible short name + const visible = header.querySelector('span[aria-hidden="true"]'); + expect(visible).toBeTruthy(); + expect(visible?.textContent?.length).toBeLessThanOrEqual(3); + }); + }); + + it("renders week number column header with sr-only label and visible #", () => { + const { container } = render(); + const input = safeQuerySelector(container, "input"); + fireEvent.focus(input); + + const headers = container.querySelectorAll( + '.react-datepicker__day-names > [role="columnheader"]', + ); + expect(headers.length).toBe(8); + + const weekNumberHeader = headers[0] as Element; + const srOnly = weekNumberHeader.querySelector(".sr-only"); + expect(srOnly).toBeTruthy(); + expect(srOnly?.textContent?.trim()?.toLowerCase()).toEqual("week number"); + + // Should have a visible short name + const visible = weekNumberHeader.querySelector( + 'span[aria-hidden="true"]', + ); + expect(visible).toBeTruthy(); + expect(visible?.textContent?.trim()?.toLowerCase()).toEqual("#"); + }); + }); + it("should show the correct start of week for GB locale", () => { registerLocale("en-GB", enGB); @@ -3802,9 +3850,10 @@ describe("DatePicker", () => { jest.spyOn(input, "focus"); fireEvent.focus(input); - const firstDay = container.querySelector(".react-datepicker__day-names") - ?.childNodes[0]?.textContent; - expect(firstDay).toBe("Mo"); + const firstDay = container.querySelector( + ".react-datepicker__day-names > div[role='columnheader'] > span[aria-hidden='true']", + ); + expect(firstDay?.textContent).toBe("Mo"); }); it("should show the correct start of week for US locale", () => { @@ -3815,9 +3864,10 @@ describe("DatePicker", () => { jest.spyOn(input, "focus"); fireEvent.focus(input); - const firstDay = container.querySelector(".react-datepicker__day-names") - ?.childNodes[0]?.textContent; - expect(firstDay).toBe("Su"); + const firstDay = container.querySelector( + ".react-datepicker__day-names > div[role='columnheader'] > span[aria-hidden='true']", + ); + expect(firstDay?.textContent).toBe("Su"); }); describe("when update the datepicker input text while props.showTimeSelectOnly is set and dateFormat has only time related format", () => {