Skip to content

Commit

Permalink
a11y: Sends focus back to input on select
Browse files Browse the repository at this point in the history
* Adds sendFocusBackToInput method for handling returning focus.
* Adds tests around possible consumer implementations.
  • Loading branch information
justinseiter committed Sep 29, 2023
1 parent 2864c5a commit bd1cd27
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 27 deletions.
55 changes: 29 additions & 26 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ export default class DatePicker extends React.Component {
constructor(props) {
super(props);
this.state = this.calcInitialState();
this.preventFocusTimeout = null;
}

componentDidMount() {
Expand Down Expand Up @@ -463,6 +464,23 @@ export default class DatePicker extends React.Component {
this.setState({ focused: true });
};

sendFocusBackToInput = () => {
// Clear previous timeout if it exists
if (this.preventFocusTimeout) {
this.clearPreventFocusTimeout();
}

// close the popper and refocus the input
// stop the input from auto opening onFocus
// setFocus to the input
this.setState({ preventFocus: true }, () => {
this.preventFocusTimeout = setTimeout(() => {
this.setFocus();
this.setState({ preventFocus: false });
});
});
};

cancelFocusInput = () => {
clearTimeout(this.inputFocusTimeout);
this.inputFocusTimeout = null;
Expand Down Expand Up @@ -543,15 +561,11 @@ export default class DatePicker extends React.Component {
};

handleSelect = (date, event, monthSelectedIn) => {
// Preventing onFocus event to fix issue
// https://github.com/Hacker0x01/react-datepicker/issues/628
this.setState({ preventFocus: true }, () => {
this.preventFocusTimeout = setTimeout(
() => this.setState({ preventFocus: false }),
50,
);
return this.preventFocusTimeout;
});
if (this.props.shouldCloseOnSelect && !this.props.showTimeSelect) {
// Preventing onFocus event to fix issue
// https://github.com/Hacker0x01/react-datepicker/issues/628
this.sendFocusBackToInput();
}
if (this.props.onChangeRaw) {
this.props.onChangeRaw(event);
}
Expand Down Expand Up @@ -699,6 +713,7 @@ export default class DatePicker extends React.Component {

this.props.onChange(changedDate);
if (this.props.shouldCloseOnSelect) {
this.sendFocusBackToInput();
this.setOpen(false);
}
if (this.props.showTimeInput) {
Expand Down Expand Up @@ -765,6 +780,7 @@ export default class DatePicker extends React.Component {
}
} else if (eventKey === "Escape") {
event.preventDefault();
this.sendFocusBackToInput();
this.setOpen(false);
} else if (eventKey === "Tab") {
this.setOpen(false);
Expand Down Expand Up @@ -875,24 +891,8 @@ export default class DatePicker extends React.Component {
onPopperKeyDown = (event) => {
const eventKey = event.key;
if (eventKey === "Escape") {
// close the popper and refocus the input
// stop the input from auto opening onFocus
// close the popper
// setFocus to the input
// allow input auto opening onFocus
event.preventDefault();
this.setState(
{
preventFocus: true,
},
() => {
this.setOpen(false);
setTimeout(() => {
this.setFocus();
this.setState({ preventFocus: false });
});
},
);
this.sendFocusBackToInput();
}
};

Expand All @@ -902,6 +902,9 @@ export default class DatePicker extends React.Component {
event.preventDefault();
}
}

this.sendFocusBackToInput();

if (this.props.selectsRange) {
this.props.onChange([null, null], event);
} else {
Expand Down
107 changes: 106 additions & 1 deletion test/datepicker_test.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe("DatePicker", () => {
expect(datePicker.state.open).toBe(false);
});

it("should close the popper and return focus to the date input.", (done) => {
it("should close the popper and return focus to the date input on Escape.", (done) => {
// https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/datepicker-dialog.html
// Date Picker Dialog | Escape | Closes the dialog and returns focus to the Choose Date button.
var div = document.createElement("div");
Expand All @@ -123,6 +123,53 @@ describe("DatePicker", () => {
});
});

it("should close the popper and return focus to the date input on Enter.", (done) => {
// https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/datepicker-dialog.html
// Date Picker Dialog | Date Grid | Enter | Closes the dialog and returns focus to the Choose Date button.
var div = document.createElement("div");
document.body.appendChild(div);
var datePicker = ReactDOM.render(<DatePicker />, div);

// user focuses the input field, the calendar opens
var dateInput = div.querySelector("input");
TestUtils.Simulate.focus(dateInput);

// user may tab or arrow down to the current day (or some other element in the popper)
var today = div.querySelector(".react-datepicker__day--today");
today.focus();

// user hits Enter
TestUtils.Simulate.keyDown(today, getKey("Enter"));
defer(() => {
expect(datePicker.calendar).toBeFalsy();
expect(datePicker.state.preventFocus).toBe(false);
expect(document.activeElement).toBe(div.querySelector("input"));
done();
});
});

it("should not close the popper and keep focus on selected date if showTimeSelect is enabled.", (done) => {
var div = document.createElement("div");
document.body.appendChild(div);
var datePicker = ReactDOM.render(<DatePicker showTimeSelect />, div);

// user focuses the input field, the calendar opens
var dateInput = div.querySelector("input");
TestUtils.Simulate.focus(dateInput);

// user may tab or arrow down to the current day (or some other element in the popper)
var today = div.querySelector(".react-datepicker__day--today");
today.focus();

// user hits Enter
TestUtils.Simulate.keyDown(today, getKey("Enter"));
defer(() => {
expect(datePicker.calendar).toBeTruthy();
expect(document.activeElement).toBe(today);
done();
});
});

it("should not re-focus the date input when focusing the year dropdown", () => {
const onBlurSpy = jest.fn();
const datePicker = mount(
Expand Down Expand Up @@ -214,6 +261,27 @@ describe("DatePicker", () => {
expect(datePicker.state.open).toBe(true);
});

it("should keep focus within calendar when clicking a day on the calendar and shouldCloseOnSelect prop is false", () => {
var div = document.createElement("div");
document.body.appendChild(div);
var datePicker = ReactDOM.render(
<DatePicker shouldCloseOnSelect={false} />,
div,
);

// user focuses the input field, the calendar opens
var dateInput = div.querySelector("input");
TestUtils.Simulate.focus(dateInput);

// user may tab or arrow down to the current day (or some other element in the popper)
var today = div.querySelector(".react-datepicker__day--today");
today.focus();

// user hits Enter
TestUtils.Simulate.keyDown(today, getKey("Enter"));
expect(document.activeElement).toBe(today);
});

it("should set open to true if showTimeInput is true", () => {

Check warning on line 285 in test/datepicker_test.test.js

View workflow job for this annotation

GitHub Actions / update (18.x)

'datePicker' is assigned a value but never used. Allowed unused vars must match /^_/u
var datePicker = TestUtils.renderIntoDocument(
<DatePicker shouldCloseOnSelect={false} showTimeInput />,
Expand Down Expand Up @@ -325,6 +393,23 @@ describe("DatePicker", () => {
expect(datePicker.calendar).toBeFalsy();
});

it("should hide the calendar and keep focus on input when pressing escape in the date input", (done) => {
var div = document.createElement("div");
document.body.appendChild(div);
var datePicker = ReactDOM.render(<DatePicker />, div);

// user focuses the input field, the calendar opens
var dateInput = div.querySelector("input");
TestUtils.Simulate.focus(dateInput);

TestUtils.Simulate.keyDown(dateInput, getKey("Escape"));
defer(() => {
expect(datePicker.calendar).toBeFalsy();
expect(document.activeElement).toBe(dateInput);
done();
});
});

it("should hide the calendar when the pressing Shift + Tab in the date input", () => {
var datePicker = TestUtils.renderIntoDocument(
<DatePicker onBlur={onBlurSpy} />,
Expand Down Expand Up @@ -403,6 +488,26 @@ describe("DatePicker", () => {
expect(datePicker.state.inputValue).toBeNull();
});

it("should return focus to input when clear button is used", (done) => {
var div = document.createElement("div");
document.body.appendChild(div);
var datePicker = ReactDOM.render(
<DatePicker selected={utils.newDate("2015-12-15")} isClearable />,
div,
);

var clearButton = TestUtils.findRenderedDOMComponentWithClass(
datePicker,
"react-datepicker__close-icon",
);
TestUtils.Simulate.click(clearButton);

defer(() => {
expect(document.activeElement).toBe(div.querySelector("input"));
done();
});
});

it("should set the title attribute on the clear button if clearButtonTitle is supplied", () => {
const datePicker = TestUtils.renderIntoDocument(
<DatePicker
Expand Down
16 changes: 16 additions & 0 deletions test/timepicker_test.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from "react";
import defer from "lodash/defer";
import DatePicker from "../src/index.jsx";
import TestUtils from "react-dom/test-utils";
import ReactDOM from "react-dom";
Expand Down Expand Up @@ -165,6 +166,21 @@ describe("TimePicker", () => {
expect(getInputString()).toBe("February 28, 2018 12:30 AM");
});

it("should return focus to input once time is selected", (done) => {
document.body.appendChild(div); // So we can check the dom later for activeElement
renderDatePicker("February 28, 2018 4:43 PM");
const dateInput = ReactDOM.findDOMNode(datePicker.input);
TestUtils.Simulate.focus(dateInput);
const time = TestUtils.findRenderedComponentWithType(datePicker, Time);
const lis = TestUtils.scryRenderedDOMComponentsWithTag(time, "li");
TestUtils.Simulate.keyDown(lis[1], getKey("Enter"));

defer(() => {
expect(document.activeElement).toBe(dateInput);
done();
});
});

it("should not select time when Escape is pressed", () => {
renderDatePicker("February 28, 2018 4:43 PM");
TestUtils.Simulate.focus(ReactDOM.findDOMNode(datePicker.input));
Expand Down

0 comments on commit bd1cd27

Please sign in to comment.