From bd1cd2739c84de35c26bc376110ba5309c5a3695 Mon Sep 17 00:00:00 2001 From: Justin Seiter Date: Thu, 28 Sep 2023 17:13:51 -0500 Subject: [PATCH] a11y: Sends focus back to input on select * Adds sendFocusBackToInput method for handling returning focus. * Adds tests around possible consumer implementations. --- src/index.jsx | 55 +++++++++--------- test/datepicker_test.test.js | 107 ++++++++++++++++++++++++++++++++++- test/timepicker_test.test.js | 16 ++++++ 3 files changed, 151 insertions(+), 27 deletions(-) diff --git a/src/index.jsx b/src/index.jsx index 8884d54972..9e1c86ba83 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -302,6 +302,7 @@ export default class DatePicker extends React.Component { constructor(props) { super(props); this.state = this.calcInitialState(); + this.preventFocusTimeout = null; } componentDidMount() { @@ -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; @@ -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); } @@ -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) { @@ -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); @@ -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(); } }; @@ -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 { diff --git a/test/datepicker_test.test.js b/test/datepicker_test.test.js index c941306ca9..e7994cf94d 100644 --- a/test/datepicker_test.test.js +++ b/test/datepicker_test.test.js @@ -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"); @@ -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(, 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(, 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( @@ -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( + , + 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", () => { var datePicker = TestUtils.renderIntoDocument( , @@ -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(, 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( , @@ -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( + , + 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( { 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));