Skip to content

Commit 311cc86

Browse files
committed
accessibility: ♿️ Improve calendar header accessibility by adding screen reader text for week numbers
Closes #3797
1 parent e7e26d1 commit 311cc86

File tree

3 files changed

+70
-5
lines changed

3 files changed

+70
-5
lines changed

src/calendar.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -477,8 +477,9 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
477477
const dayNames: React.ReactElement[] = [];
478478
if (this.props.showWeekNumbers) {
479479
dayNames.push(
480-
<div key="W" className="react-datepicker__day-name">
481-
{this.props.weekLabel || "#"}
480+
<div key="W" className="react-datepicker__day-name" role="columnheader">
481+
<span className="sr-only">Week number</span>
482+
<span aria-hidden="true">{this.props.weekLabel || "#"}</span>
482483
</div>,
483484
);
484485
}
@@ -494,10 +495,13 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
494495
return (
495496
<div
496497
key={offset}
497-
aria-label={formatDate(day, "EEEE", this.props.locale)}
498+
role="columnheader"
498499
className={clsx("react-datepicker__day-name", weekDayClassName)}
499500
>
500-
{weekDayName}
501+
<span className="sr-only">
502+
{formatDate(day, "EEEE", this.props.locale)}
503+
</span>
504+
<span aria-hidden="true">{weekDayName}</span>
501505
</div>
502506
);
503507
}),
@@ -852,7 +856,7 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
852856
{this.renderMonthYearDropdown(i !== 0)}
853857
{this.renderYearDropdown(i !== 0)}
854858
</div>
855-
<div className="react-datepicker__day-names">
859+
<div className="react-datepicker__day-names" role="row">
856860
{this.header(monthDate)}
857861
</div>
858862
</div>

src/stylesheets/datepicker.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22
@use "variables" as *;
33
@use "mixins" as *;
44

5+
/* sr-only utility class for accessibility */
6+
.sr-only {
7+
position: absolute;
8+
width: 1px;
9+
height: 1px;
10+
padding: 0;
11+
margin: -1px;
12+
overflow: hidden;
13+
clip: rect(0, 0, 0, 0);
14+
white-space: nowrap;
15+
border: 0;
16+
}
17+
518
.react-datepicker-wrapper {
619
display: inline-block;
720
padding: 0;

src/test/datepicker_test.test.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3794,6 +3794,54 @@ describe("DatePicker", () => {
37943794
});
37953795
});
37963796

3797+
describe("Calendar Header Accessibility", () => {
3798+
it("renders day names with sr-only full weekday and visible short name", () => {
3799+
const { container } = render(<DatePicker />);
3800+
const input = safeQuerySelector(container, "input");
3801+
fireEvent.focus(input);
3802+
3803+
const headers = container.querySelectorAll(
3804+
'.react-datepicker__day-names > [role="columnheader"]',
3805+
);
3806+
expect(headers.length).toBe(7);
3807+
3808+
headers.forEach((header) => {
3809+
// Should have a visually hidden span with the full weekday name
3810+
const srOnly = header.querySelector(".sr-only");
3811+
expect(srOnly).toBeTruthy();
3812+
expect(srOnly?.textContent?.length).toBeGreaterThan(2);
3813+
3814+
// Should have a visible short name
3815+
const visible = header.querySelector('span[aria-hidden="true"]');
3816+
expect(visible).toBeTruthy();
3817+
expect(visible?.textContent?.length).toBeLessThanOrEqual(3);
3818+
});
3819+
});
3820+
3821+
it("renders week number column header with sr-only label and visible #", () => {
3822+
const { container } = render(<DatePicker showWeekNumbers />);
3823+
const input = safeQuerySelector(container, "input");
3824+
fireEvent.focus(input);
3825+
3826+
const headers = container.querySelectorAll(
3827+
'.react-datepicker__day-names > [role="columnheader"]',
3828+
);
3829+
expect(headers.length).toBe(8);
3830+
3831+
const weekNumberHeader = headers[0] as Element;
3832+
const srOnly = weekNumberHeader.querySelector(".sr-only");
3833+
expect(srOnly).toBeTruthy();
3834+
expect(srOnly?.textContent?.trim()?.toLowerCase()).toEqual("week number");
3835+
3836+
// Should have a visible short name
3837+
const visible = weekNumberHeader.querySelector(
3838+
'span[aria-hidden="true"]',
3839+
);
3840+
expect(visible).toBeTruthy();
3841+
expect(visible?.textContent?.trim()?.toLowerCase()).toEqual("#");
3842+
});
3843+
});
3844+
37973845
it("should show the correct start of week for GB locale", () => {
37983846
registerLocale("en-GB", enGB);
37993847

0 commit comments

Comments
 (0)