Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve time picker accessibility #4181

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 49 additions & 21 deletions src/time.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,21 @@ export default class Time extends React.Component {
event.key = "Enter";
}

if (
(event.key === "ArrowUp" || event.key === "ArrowLeft") &&
event.target.previousSibling
) {
event.preventDefault();
event.target.previousSibling.focus();
}
if (
(event.key === "ArrowDown" || event.key === "ArrowRight") &&
event.target.nextSibling
) {
event.preventDefault();
event.target.nextSibling.focus();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about ctrl+arrow? What about wrapping when it reaches the beginning or end, should it wrap?

🔹 Reduce Future Bugs (Nice to have)

Image of Steven S Steven S

}

if (event.key === "Enter") {
this.handleClick(time);
}
Expand Down Expand Up @@ -175,27 +190,39 @@ export default class Time extends React.Component {
}
}

return times.map((time, i) => (
<li
key={i}
onClick={this.handleClick.bind(this, time)}
className={this.liClasses(time, currH, currM)}
ref={(li) => {
if (isBefore(time, activeTime) || isEqual(time, activeTime)) {
this.centerLi = li;
// Determine which time to focus and scroll into view when component mounts
const timeToFocus = times.reduce((prev, time) => {
if (isBefore(time, activeTime) || isEqual(time, activeTime)) {
return time;
} else {
return prev;
}
}, times[0]);

return times.map((time, i) => {
return (
<li
key={i}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general it's a React best practice to not use integer indexes as the key. It would be better to use the string representation of the time.

🔹 Best Practices (Nice to have)

Image of David K David K

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code was not modified in this PR (just whitespace changes)

onClick={this.handleClick.bind(this, time)}
className={this.liClasses(time, currH, currM)}
ref={(li) => {
if (time === timeToFocus) {
this.centerLi = li;
}
}}
onKeyDown={(ev) => {
this.handleOnKeyDown(ev, time);
}}
tabIndex={time === timeToFocus ? "0" : "-1"}
role="option"
aria-selected={
this.isSelectedTime(time, currH, currM) ? "true" : undefined
}
}}
onKeyDown={(ev) => {
this.handleOnKeyDown(ev, time);
}}
tabIndex="0"
aria-selected={
this.isSelectedTime(time, currH, currM) ? "true" : undefined
}
>
{formatDate(time, format, this.props.locale)}
</li>
));
>
{formatDate(time, format, this.props.locale)}
</li>
);
});
};

render() {
Expand Down Expand Up @@ -231,7 +258,8 @@ export default class Time extends React.Component {
this.list = list;
}}
style={height ? { height } : {}}
tabIndex="0"
role="listbox"
aria-label={this.props.timeCaption}
>
{this.renderTimes()}
</ul>
Expand Down
48 changes: 48 additions & 0 deletions test/time_format_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,21 @@ describe("TimeComponent", () => {
expect(timeListItem.at(0).prop("aria-selected")).to.eq("true");
});

it("should enable keyboard focus on the selected item", () => {
var timeComponent = mount(
<TimeComponent
format="HH:mm"
selected={new Date("1990-06-14 08:00")}
openToDate={new Date("1990-06-14 09:00")}
/>
);

var timeListItem = timeComponent.find(
".react-datepicker__time-list-item--selected"
);
expect(timeListItem.at(0).prop("tabIndex")).to.equal("0");
});

it("should not add the aria-selected property to a regular item", () => {
var timeComponent = mount(
<TimeComponent
Expand All @@ -125,6 +140,39 @@ describe("TimeComponent", () => {
expect(timeListItem.at(0).prop("aria-selected")).to.be.undefined;
});

it("should disable keyboard focus on a regular item", () => {
var timeComponent = mount(
<TimeComponent
format="HH:mm"
selected={new Date("1990-06-14 08:00")}
openToDate={new Date("1990-06-14 09:00")}
/>
);

var timeListItem = timeComponent.find(
".react-datepicker__time-list-item"
);
expect(timeListItem.at(0).prop("tabIndex")).to.equal("-1");
});

it("when no selected time, should focus the time closest to the opened time", () => {
var timeComponent = mount(
<TimeComponent
format="HH:mm"
openToDate={new Date("1990-06-14 09:11")}
/>
);

var timeListItem = timeComponent.find(
".react-datepicker__time-list-item"
);
expect(
timeListItem
.findWhere((node) => node.type() && node.text() === "09:00")
.prop("tabIndex")
).to.equal("0");
});

it("when no selected time, should call calcCenterPosition with centerLi ref, closest to the opened time", () => {
mount(
<TimeComponent
Expand Down
Loading