Skip to content

Commit

Permalink
Feature/2094 month navigation day selection (#2097)
Browse files Browse the repository at this point in the history
* set preselection when Month changes.

consolidated focus() logic on Day component.
passed down containerRef to Day component.

* added tests for next and prev months

* updated dev instructions on contributing document

* Travis CI failing on this "unused" proptype

Co-authored-by: DDiaz <daniel.diaz@isobar.com>
  • Loading branch information
dan-diaz and DDiaz committed Mar 25, 2020
1 parent c1fdd32 commit efffb47
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 33 deletions.
13 changes: 7 additions & 6 deletions CONTRIBUTING.md
Expand Up @@ -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
6 changes: 5 additions & 1 deletion src/calendar.jsx
Expand Up @@ -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) {
Expand Down Expand Up @@ -284,6 +285,8 @@ export default class Calendar extends React.Component {
this.props.setOpen(true);
}
}

this.props.setPreSelection && this.props.setPreSelection(date);
};

handleMonthYearChange = date => {
Expand Down Expand Up @@ -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}
/>
</div>
);
Expand Down
55 changes: 35 additions & 20 deletions src/day.jsx
Expand Up @@ -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();
Expand Down Expand Up @@ -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 = () => (
<div
ref={this.dayEl}
Expand Down
1 change: 1 addition & 0 deletions src/index.jsx
Expand Up @@ -795,6 +795,7 @@ export default class DatePicker extends React.Component {
handleOnKeyDown={this.onDayKeyDown}
isInputFocused={this.state.focused}
customTimeInput={this.props.customTimeInput}
setPreSelection={this.setPreSelection}
>
{this.props.children}
</WrappedCalendar>
Expand Down
7 changes: 6 additions & 1 deletion src/month.jsx
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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}
/>
);

Expand Down
7 changes: 6 additions & 1 deletion src/week.jsx
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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}
/>
);
})
Expand Down
59 changes: 55 additions & 4 deletions test/calendar_test.js
Expand Up @@ -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", () => {
Expand All @@ -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", () => {
Expand All @@ -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", () => {
Expand All @@ -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(
<DatePicker selected={selected} />
);
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(
<DatePicker selected={selected} />
);
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
);
});
});
});

0 comments on commit efffb47

Please sign in to comment.