diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3e15c9510..266e86b20 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,9 +26,10 @@ Local development configuration is pretty snappy. Here's how to get set up: 1. Install/use node >=11.10.1 2. Run `yarn link` from project root -3. Run `cd docs-sites && yarn link react-datepicker` -4. Run `yarn install` from project root -5. Run `yarn start` from project root -6. Open new terminal window -7. After each JS change run `yarn build:js` in project root -8. After each SCSS change run `yarn run css:dev && yarn run css:modules:dev` in project root +3. Run `cd docs-site && yarn link react-datepicker` +4. Run `yarn build` from project root (at least the first time, this will get you the `dist` directory that holds the code that will be linked to) +5. Run `yarn install` from project root +6. Run `yarn start` from project root +7. Open new terminal window +8. After each JS change run `yarn build:js` in project root +9. After each SCSS change run `yarn run css:dev && yarn run css:modules:dev` in project root diff --git a/src/calendar.jsx b/src/calendar.jsx index 8334452ba..95364458f 100644 --- a/src/calendar.jsx +++ b/src/calendar.jsx @@ -158,7 +158,8 @@ export default class Calendar extends React.Component { handleOnKeyDown: PropTypes.func, isInputFocused: PropTypes.bool, customTimeInput: PropTypes.element, - weekAriaLabelPrefix: PropTypes.string + weekAriaLabelPrefix: PropTypes.string, + setPreSelection: PropTypes.func }; constructor(props) { @@ -284,6 +285,8 @@ export default class Calendar extends React.Component { this.props.setOpen(true); } } + + this.props.setPreSelection && this.props.setPreSelection(date); }; handleMonthYearChange = date => { @@ -722,6 +725,7 @@ export default class Calendar extends React.Component { showMonthYearPicker={this.props.showMonthYearPicker} showQuarterYearPicker={this.props.showQuarterYearPicker} isInputFocused={this.props.isInputFocused} + containerRef={this.containerRef} /> ); diff --git a/src/day.jsx b/src/day.jsx index 2f066d1ec..26100d19d 100644 --- a/src/day.jsx +++ b/src/day.jsx @@ -38,32 +38,18 @@ export default class Day extends React.Component { startDate: PropTypes.instanceOf(Date), renderDayContents: PropTypes.func, handleOnKeyDown: PropTypes.func, - isInputFocused: PropTypes.bool + containerRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ current: PropTypes.instanceOf(Element) }) + ]) }; componentDidMount() { - const newTabIndex = this.getTabIndex(); - - if (newTabIndex === 0 && this.isSameDay(this.props.preSelection)) { - // focus the day on mount so that keyboard navigation works while cycling through months - // prevent focus for these activeElement cases so we don't pull focus from the input as the calendar opens - (!document.activeElement || document.activeElement === document.body) && - this.dayEl.current.focus(); - } + this.handleFocusDay(); } componentDidUpdate(prevProps) { - const newTabIndex = this.getTabIndex(); - - if ( - newTabIndex === 0 && - this.isSameDay(this.props.preSelection) !== - this.isSameDay(prevProps.preSelection) - ) { - // only do this while the input isn't focused - // otherwise, typing/backspacing the date manually may steal focus away from the input - !prevProps.isInputFocused && this.dayEl.current.focus(); - } + this.handleFocusDay(prevProps); } dayEl = React.createRef(); @@ -266,6 +252,35 @@ export default class Day extends React.Component { return tabIndex; }; + // various cases when we need to apply focus to the preselected day + // focus the day on mount/update so that keyboard navigation works while cycling through months with up or down keys (not for prev and next month buttons) + // prevent focus for these activeElement cases so we don't pull focus from the input as the calendar opens + handleFocusDay = (prevProps = {}) => { + let shouldFocusDay = false; + // only do this while the input isn't focused + // otherwise, typing/backspacing the date manually may steal focus away from the input + if ( + this.getTabIndex() === 0 && + !prevProps.isInputFocused && + this.isSameDay(this.props.preSelection) + ) { + // there is currently no activeElement + if (!document.activeElement || document.activeElement === document.body) { + shouldFocusDay = true; + } + // the activeElement is in the container, and it is another instance of Day + if ( + this.props.containerRef && + this.props.containerRef.current && + this.props.containerRef.current.contains(document.activeElement) && + document.activeElement.classList.contains("react-datepicker__day") + ) { + shouldFocusDay = true; + } + } + + shouldFocusDay && this.dayEl.current.focus(); + }; render = () => (
{this.props.children} diff --git a/src/month.jsx b/src/month.jsx index 407ab9279..1f5c8608b 100644 --- a/src/month.jsx +++ b/src/month.jsx @@ -48,7 +48,11 @@ export default class Month extends React.Component { showQuarterYearPicker: PropTypes.bool, handleOnKeyDown: PropTypes.func, isInputFocused: PropTypes.bool, - weekAriaLabelPrefix: PropTypes.string + weekAriaLabelPrefix: PropTypes.string, + containerRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ current: PropTypes.instanceOf(Element) }) + ]) }; handleDayClick = (day, event) => { @@ -155,6 +159,7 @@ export default class Month extends React.Component { renderDayContents={this.props.renderDayContents} handleOnKeyDown={this.props.handleOnKeyDown} isInputFocused={this.props.isInputFocused} + containerRef={this.props.containerRef} /> ); diff --git a/src/week.jsx b/src/week.jsx index d2ec7c721..6572e2878 100644 --- a/src/week.jsx +++ b/src/week.jsx @@ -45,7 +45,11 @@ export default class Week extends React.Component { shouldCloseOnSelect: PropTypes.bool, renderDayContents: PropTypes.func, handleOnKeyDown: PropTypes.func, - isInputFocused: PropTypes.bool + isInputFocused: PropTypes.bool, + containerRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ current: PropTypes.instanceOf(Element) }) + ]) }; handleDayClick = (day, event) => { @@ -124,6 +128,7 @@ export default class Week extends React.Component { disabledKeyboardNavigation={this.props.disabledKeyboardNavigation} handleOnKeyDown={this.props.handleOnKeyDown} isInputFocused={this.props.isInputFocused} + containerRef={this.props.containerRef} /> ); }) diff --git a/test/calendar_test.js b/test/calendar_test.js index ce48a5df8..234a0ce83 100644 --- a/test/calendar_test.js +++ b/test/calendar_test.js @@ -1213,7 +1213,9 @@ describe("Calendar", function() { showQuarterYearPicker /> ); - expect(shallowCalendar.html().indexOf(`aria-label="${ariaLabel}"`)).not.equal(-1); + expect( + shallowCalendar.html().indexOf(`aria-label="${ariaLabel}"`) + ).not.equal(-1); }); it("should have a previous-button with the provided aria-label for year", () => { @@ -1227,7 +1229,9 @@ describe("Calendar", function() { showQuarterYearPicker /> ); - expect(shallowCalendar.html().indexOf(`aria-label="${ariaLabel}"`)).not.equal(-1); + expect( + shallowCalendar.html().indexOf(`aria-label="${ariaLabel}"`) + ).not.equal(-1); }); it("should have a next-button with the provided aria-label for month", () => { @@ -1240,7 +1244,9 @@ describe("Calendar", function() { onClickOutside={() => {}} /> ); - expect(shallowCalendar.html().indexOf(`aria-label="${ariaLabel}"`)).not.equal(-1); + expect( + shallowCalendar.html().indexOf(`aria-label="${ariaLabel}"`) + ).not.equal(-1); }); it("should have a previous-button with the provided aria-label for month", () => { @@ -1253,6 +1259,51 @@ describe("Calendar", function() { onClickOutside={() => {}} /> ); - expect(shallowCalendar.html().indexOf(`aria-label="${ariaLabel}"`)).not.equal(-1); + expect( + shallowCalendar.html().indexOf(`aria-label="${ariaLabel}"`) + ).not.equal(-1); + }); + + describe("changing the month also changes the preselection to preserve keyboard navigation abilities", () => { + it("updates the preselection when you choose Next Month", () => { + let selected = new Date(); + selected.setDate(1); + const currentMonth = selected.getMonth(); + + const datePicker = TestUtils.renderIntoDocument( + + ); + const dateInput = datePicker.input; + TestUtils.Simulate.focus(ReactDOM.findDOMNode(dateInput)); + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass( + datePicker, + "react-datepicker__navigation--next" + ) + ); + expect(datePicker.state.preSelection.getMonth()).to.equal( + currentMonth === 11 ? 0 : currentMonth + 1 + ); + }); + it("updates the preselection when you choose Previous Month", () => { + let selected = new Date(); + selected.setDate(1); + const currentMonth = selected.getMonth(); + + const datePicker = TestUtils.renderIntoDocument( + + ); + const dateInput = datePicker.input; + TestUtils.Simulate.focus(ReactDOM.findDOMNode(dateInput)); + TestUtils.Simulate.click( + TestUtils.findRenderedDOMComponentWithClass( + datePicker, + "react-datepicker__navigation--previous" + ) + ); + expect(datePicker.state.preSelection.getMonth()).to.equal( + currentMonth === 0 ? 11 : currentMonth - 1 + ); + }); }); });