Skip to content

Commit ea33050

Browse files
committed
feat: add dropdown keyboard accessibility
1 parent e7e26d1 commit ea33050

File tree

4 files changed

+74
-11
lines changed

4 files changed

+74
-11
lines changed

src/month_dropdown.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@ export default class MonthDropdown extends Component<
5656
visible: boolean,
5757
monthNames: string[],
5858
): React.ReactElement => (
59-
<div
59+
<button
6060
key="read"
61+
type="button"
6162
style={{ visibility: visible ? "visible" : "hidden" }}
6263
className="react-datepicker__month-read-view"
6364
onClick={this.toggleDropdown}
@@ -66,7 +67,7 @@ export default class MonthDropdown extends Component<
6667
<span className="react-datepicker__month-read-view--selected-month">
6768
{monthNames[this.props.month]}
6869
</span>
69-
</div>
70+
</button>
7071
);
7172

7273
renderDropdown = (monthNames: string[]): React.ReactElement => (

src/month_dropdown_options.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,52 @@ interface MonthDropdownOptionsProps {
1010
}
1111

1212
export default class MonthDropdownOptions extends Component<MonthDropdownOptionsProps> {
13+
monthOptionButtonsRef: (HTMLDivElement | null)[] = [];
14+
1315
isSelectedMonth = (i: number): boolean => this.props.month === i;
1416

17+
handleOptionKeyDown = (i: number, e: React.KeyboardEvent): void => {
18+
switch (e.key) {
19+
case "Enter":
20+
e.preventDefault();
21+
this.onChange(i);
22+
break;
23+
case "Escape":
24+
e.preventDefault();
25+
this.props.onCancel();
26+
break;
27+
case "ArrowUp":
28+
case "ArrowDown": {
29+
e.preventDefault();
30+
const newMonth =
31+
(i + (e.key === "ArrowUp" ? -1 : 1) + this.props.monthNames.length) %
32+
this.props.monthNames.length;
33+
this.monthOptionButtonsRef[newMonth]?.focus();
34+
break;
35+
}
36+
}
37+
};
38+
1539
renderOptions = (): React.ReactElement[] => {
1640
return this.props.monthNames.map<React.ReactElement>(
1741
(month: string, i: number): React.ReactElement => (
1842
<div
43+
ref={(el) => {
44+
this.monthOptionButtonsRef?.push(el);
45+
if (this.isSelectedMonth(i)) {
46+
el?.focus();
47+
}
48+
}}
49+
role="button"
50+
tabIndex={0}
1951
className={
2052
this.isSelectedMonth(i)
2153
? "react-datepicker__month-option react-datepicker__month-option--selected_month"
2254
: "react-datepicker__month-option"
2355
}
2456
key={month}
2557
onClick={this.onChange.bind(this, i)}
58+
onKeyDown={this.handleOptionKeyDown.bind(this, i)}
2659
aria-selected={this.isSelectedMonth(i) ? "true" : undefined}
2760
>
2861
{this.isSelectedMonth(i) ? (

src/year_dropdown.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ interface YearDropdownProps
1212
dropdownMode: "scroll" | "select";
1313
onChange: (year: number) => void;
1414
date: Date;
15-
onSelect?: (date: Date, event?: React.MouseEvent<HTMLDivElement>) => void;
15+
onSelect?: (date: Date, event?: React.MouseEvent<HTMLButtonElement>) => void;
1616
setOpen?: (open: boolean) => void;
1717
}
1818

@@ -62,19 +62,18 @@ export default class YearDropdown extends Component<
6262
);
6363

6464
renderReadView = (visible: boolean): React.ReactElement => (
65-
<div
65+
<button
6666
key="read"
67+
type="button"
6768
style={{ visibility: visible ? "visible" : "hidden" }}
6869
className="react-datepicker__year-read-view"
69-
onClick={(event: React.MouseEvent<HTMLDivElement>): void =>
70-
this.toggleDropdown(event)
71-
}
70+
onClick={this.toggleDropdown}
7271
>
7372
<span className="react-datepicker__year-read-view--down-arrow" />
7473
<span className="react-datepicker__year-read-view--selected-year">
7574
{this.props.year}
7675
</span>
77-
</div>
76+
</button>
7877
);
7978

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

104-
toggleDropdown = (event?: React.MouseEvent<HTMLDivElement>): void => {
103+
toggleDropdown = (event?: React.MouseEvent<HTMLButtonElement>): void => {
105104
this.setState(
106105
{
107106
dropdownVisible: !this.state.dropdownVisible,
@@ -116,13 +115,13 @@ export default class YearDropdown extends Component<
116115

117116
handleYearChange = (
118117
date: Date,
119-
event?: React.MouseEvent<HTMLDivElement>,
118+
event?: React.MouseEvent<HTMLButtonElement>,
120119
): void => {
121120
this.onSelect?.(date, event);
122121
this.setOpen();
123122
};
124123

125-
onSelect = (date: Date, event?: React.MouseEvent<HTMLDivElement>): void => {
124+
onSelect = (date: Date, event?: React.MouseEvent<HTMLButtonElement>): void => {
126125
this.props.onSelect?.(date, event);
127126
};
128127

src/year_dropdown_options.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,18 +88,48 @@ export default class YearDropdownOptions extends Component<
8888
}
8989

9090
dropdownRef: React.RefObject<HTMLDivElement | null>;
91+
yearOptionButtonsRef: Record<number, HTMLDivElement | null> = {};
92+
93+
handleOptionKeyDown = (year: number, e: React.KeyboardEvent): void => {
94+
switch (e.key) {
95+
case "Enter":
96+
e.preventDefault();
97+
this.onChange(year);
98+
break;
99+
case "Escape":
100+
e.preventDefault();
101+
this.props.onCancel();
102+
break;
103+
case "ArrowUp":
104+
case "ArrowDown": {
105+
e.preventDefault();
106+
const newYear = year + (e.key === "ArrowUp" ? 1 : -1);
107+
this.yearOptionButtonsRef[newYear]?.focus();
108+
break;
109+
}
110+
}
111+
};
91112

92113
renderOptions = (): React.ReactElement[] => {
93114
const selectedYear = this.props.year;
94115
const options = this.state.yearsList.map((year) => (
95116
<div
117+
ref={(el) => {
118+
this.yearOptionButtonsRef[year] = el;
119+
if (year === selectedYear) {
120+
el?.focus();
121+
}
122+
}}
123+
role="button"
124+
tabIndex={0}
96125
className={
97126
selectedYear === year
98127
? "react-datepicker__year-option react-datepicker__year-option--selected_year"
99128
: "react-datepicker__year-option"
100129
}
101130
key={year}
102131
onClick={this.onChange.bind(this, year)}
132+
onKeyDown={this.handleOptionKeyDown.bind(this, year)}
103133
aria-selected={selectedYear === year ? "true" : undefined}
104134
>
105135
{selectedYear === year ? (

0 commit comments

Comments
 (0)