Skip to content

Commit 812b8c5

Browse files
Merge pull request Hacker0x01#3086 from pedraja/fix/accessibility-year
Support keyboard navigation on Year picker.
2 parents 8621a56 + 64e0785 commit 812b8c5

File tree

2 files changed

+176
-0
lines changed

2 files changed

+176
-0
lines changed

src/year.jsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export default class Year extends React.Component {
1010
disabledKeyboardNavigation: PropTypes.bool,
1111
onDayClick: PropTypes.func,
1212
preSelection: PropTypes.instanceOf(Date),
13+
setPreSelection: PropTypes.func,
1314
selected: PropTypes.object,
1415
inline: PropTypes.bool,
1516
maxDate: PropTypes.instanceOf(Date),
@@ -21,12 +22,42 @@ export default class Year extends React.Component {
2122
super(props);
2223
}
2324

25+
YEAR_REFS = [...Array(this.props.yearItemNumber)].map(() =>
26+
React.createRef()
27+
);
28+
29+
isDisabled = (date) => utils.isDayDisabled(date, this.props);
30+
31+
isExcluded = (date) => utils.isDayExcluded(date, this.props);
32+
33+
updateFocusOnPaginate = (refIndex) => {
34+
const waitForReRender = function () {
35+
this.YEAR_REFS[refIndex].current.focus();
36+
}.bind(this);
37+
38+
window.requestAnimationFrame(waitForReRender);
39+
};
40+
2441
handleYearClick = (day, event) => {
2542
if (this.props.onDayClick) {
2643
this.props.onDayClick(day, event);
2744
}
2845
};
2946

47+
handleYearNavigation = (newYear, newDate) => {
48+
const { date, yearItemNumber } = this.props;
49+
const { startPeriod } = utils.getYearsPeriod(date, yearItemNumber);
50+
51+
if (this.isDisabled(newDate) || this.isExcluded(newDate)) return;
52+
this.props.setPreSelection(newDate);
53+
54+
if (newYear - startPeriod === -1) {
55+
this.updateFocusOnPaginate(yearItemNumber - 1);
56+
} else if (newYear - startPeriod === yearItemNumber) {
57+
this.updateFocusOnPaginate(0);
58+
} else this.YEAR_REFS[newYear - startPeriod].current.focus();
59+
};
60+
3061
isSameDay = (y, other) => utils.isSameDay(y, other);
3162

3263
isKeyboardSelected = (y) => {
@@ -44,6 +75,30 @@ export default class Year extends React.Component {
4475
this.handleYearClick(utils.getStartOfYear(utils.setYear(date, y)), e);
4576
};
4677

78+
onYearKeyDown = (e, y) => {
79+
const { key } = e;
80+
if (!this.props.disabledKeyboardNavigation) {
81+
switch (key) {
82+
case "Enter":
83+
this.onYearClick(e, y);
84+
this.props.setPreSelection(this.props.selected);
85+
break;
86+
case "ArrowRight":
87+
this.handleYearNavigation(
88+
y + 1,
89+
utils.addYears(this.props.preSelection, 1)
90+
);
91+
break;
92+
case "ArrowLeft":
93+
this.handleYearNavigation(
94+
y - 1,
95+
utils.subYears(this.props.preSelection, 1)
96+
);
97+
break;
98+
}
99+
}
100+
};
101+
47102
getYearClassNames = (y) => {
48103
const { minDate, maxDate, selected } = this.props;
49104
return classnames("react-datepicker__year-text", {
@@ -56,6 +111,13 @@ export default class Year extends React.Component {
56111
});
57112
};
58113

114+
getYearTabIndex = (y) => {
115+
if (this.props.disabledKeyboardNavigation) return "-1";
116+
const preSelected = utils.getYear(this.props.preSelection);
117+
118+
return y === preSelected ? "0" : "-1";
119+
};
120+
59121
render() {
60122
const yearsList = [];
61123
const { date, yearItemNumber } = this.props;
@@ -67,16 +129,22 @@ export default class Year extends React.Component {
67129
for (let y = startPeriod; y <= endPeriod; y++) {
68130
yearsList.push(
69131
<div
132+
ref={this.YEAR_REFS[y - startPeriod]}
70133
onClick={(ev) => {
71134
this.onYearClick(ev, y);
72135
}}
136+
onKeyDown={(ev) => {
137+
this.onYearKeyDown(ev, y);
138+
}}
139+
tabIndex={this.getYearTabIndex(y)}
73140
className={this.getYearClassNames(y)}
74141
key={y}
75142
>
76143
{y}
77144
</div>
78145
);
79146
}
147+
80148
return (
81149
<div className="react-datepicker__year">
82150
<div className="react-datepicker__year-wrapper">{yearsList}</div>

test/year_picker_test.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { mount } from "enzyme";
33
import DatePicker from "../src/index.jsx";
44
import Year from "../src/year";
55
import TestUtils from "react-dom/test-utils";
6+
import { create } from "react-test-renderer";
67
import ReactDOM from "react-dom";
78
import * as utils from "../src/date_utils";
89
import Calendar from "../src/calendar";
@@ -166,4 +167,111 @@ describe("YearPicker", () => {
166167
expect(allPreselectedYears.length).to.equal(1);
167168
});
168169
});
170+
171+
describe("Keyboard navigation", () => {
172+
let preSelected;
173+
const setPreSelection = (preSelection) => {
174+
preSelected = preSelection;
175+
};
176+
177+
let selectedDay;
178+
const onDayClick = (day) => {
179+
selectedDay = day;
180+
};
181+
182+
const getPicker = (initialDate, props) =>
183+
TestUtils.renderIntoDocument(
184+
<Year
185+
selected={utils.newDate(initialDate)}
186+
date={utils.newDate(initialDate)}
187+
setPreSelection={setPreSelection}
188+
preSelection={utils.newDate(initialDate)}
189+
onDayClick={onDayClick}
190+
yearItemNumber={12}
191+
{...props}
192+
/>
193+
);
194+
195+
const simulateLeft = (target) =>
196+
TestUtils.Simulate.keyDown(target, {
197+
key: "ArrowLeft",
198+
keyCode: 37,
199+
which: 37,
200+
});
201+
const simulateRight = (target) =>
202+
TestUtils.Simulate.keyDown(target, {
203+
key: "ArrowRight",
204+
keyCode: 39,
205+
which: 39,
206+
});
207+
208+
it("should preSelect and set 2020 on left arrow press", () => {
209+
const yearPicker = getPicker("2021-01-01");
210+
211+
const target = TestUtils.findRenderedDOMComponentWithClass(
212+
yearPicker,
213+
"react-datepicker__year-text--selected"
214+
);
215+
simulateLeft(target);
216+
217+
expect(utils.getYear(preSelected)).to.equal(2020);
218+
});
219+
it("should preSelect and set 2022 on left arrow press", () => {
220+
const yearPicker = getPicker("2021-01-01");
221+
222+
const target = TestUtils.findRenderedDOMComponentWithClass(
223+
yearPicker,
224+
"react-datepicker__year-text--selected"
225+
);
226+
simulateRight(target);
227+
228+
expect(utils.getYear(preSelected)).to.equal(2022);
229+
});
230+
it("should paginate from 2017 to 2016", () => {
231+
const yearPicker = getPicker("2017-01-01");
232+
233+
const target = TestUtils.findRenderedDOMComponentWithClass(
234+
yearPicker,
235+
"react-datepicker__year-text--selected"
236+
);
237+
simulateLeft(target);
238+
239+
expect(utils.getYear(preSelected)).to.equal(2016);
240+
});
241+
it("should paginate from 2028 to 2029", () => {
242+
const yearPicker = getPicker("2028-01-01");
243+
244+
const target = TestUtils.findRenderedDOMComponentWithClass(
245+
yearPicker,
246+
"react-datepicker__year-text--selected"
247+
);
248+
simulateRight(target);
249+
250+
expect(utils.getYear(preSelected)).to.equal(2029);
251+
});
252+
it("should select 2021 when Enter key is pressed", () => {
253+
const yearPicker = getPicker("2021-01-01");
254+
255+
const target = TestUtils.findRenderedDOMComponentWithClass(
256+
yearPicker,
257+
"react-datepicker__year-text--selected"
258+
);
259+
260+
TestUtils.Simulate.keyDown(target, { key: "Enter", code: 13, which: 13 });
261+
expect(utils.getYear(selectedDay)).to.equal(2021);
262+
});
263+
it("should disable keyboard navigation", () => {
264+
const yearPicker = getPicker("2021-01-01", {
265+
disabledKeyboardNavigation: true,
266+
});
267+
268+
const target = TestUtils.findRenderedDOMComponentWithClass(
269+
yearPicker,
270+
"react-datepicker__year-text--selected"
271+
);
272+
simulateRight(target);
273+
274+
expect(utils.getYear(preSelected)).to.equal(2021);
275+
});
276+
});
169277
});

0 commit comments

Comments
 (0)