From c49fb8e4b5c33426754c9d8c7eeb1599f0abbb96 Mon Sep 17 00:00:00 2001 From: Rory Stokes Date: Tue, 16 Aug 2022 11:42:49 +0930 Subject: [PATCH] Enable keyboard navigation of quarters (fixes #3464) --- src/date_utils.js | 9 +++++-- src/month.jsx | 64 +++++++++++++++++++++++++++++++++++++++++++- test/month_test.js | 66 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 3 deletions(-) diff --git a/src/date_utils.js b/src/date_utils.js index af2f91fe4..8c305c00b 100644 --- a/src/date_utils.js +++ b/src/date_utils.js @@ -6,12 +6,14 @@ import addHours from "date-fns/addHours"; import addDays from "date-fns/addDays"; import addWeeks from "date-fns/addWeeks"; import addMonths from "date-fns/addMonths"; +import addQuarters from "date-fns/addQuarters"; import addYears from "date-fns/addYears"; import subMinutes from "date-fns/subMinutes"; import subHours from "date-fns/subHours"; import subDays from "date-fns/subDays"; import subWeeks from "date-fns/subWeeks"; import subMonths from "date-fns/subMonths"; +import subQuarters from "date-fns/subQuarters"; import subYears from "date-fns/subYears"; import getSeconds from "date-fns/getSeconds"; import getMinutes from "date-fns/getMinutes"; @@ -265,7 +267,7 @@ export function getEndOfMonth(date) { // *** Addition *** -export { addMinutes, addDays, addWeeks, addMonths, addYears }; +export { addMinutes, addDays, addWeeks, addMonths, addQuarters, addYears }; // *** Subtraction *** @@ -276,6 +278,7 @@ export { subDays, subWeeks, subMonths, + subQuarters, subYears, }; @@ -377,7 +380,9 @@ export function getLocaleObject(localeSpec) { } export function getFormattedWeekdayInLocale(date, formatFunc, locale) { - return typeof formatFunc === "function" ? formatFunc(date, locale) : formatDate(date, "EEEE", locale); + return typeof formatFunc === "function" + ? formatFunc(date, locale) + : formatDate(date, "EEEE", locale); } export function getWeekdayMinInLocale(date, locale) { diff --git a/src/month.jsx b/src/month.jsx index 2ded5e45f..62b37746a 100644 --- a/src/month.jsx +++ b/src/month.jsx @@ -74,6 +74,7 @@ export default class Month extends React.Component { }; MONTH_REFS = [...Array(12)].map(() => React.createRef()); + QUARTER_REFS = [...Array(4)].map(() => React.createRef()); isDisabled = (date) => utils.isDayDisabled(date, this.props); @@ -141,6 +142,10 @@ export default class Month extends React.Component { utils.getYear(day) === utils.getYear(utils.newDate()) && m === utils.getMonth(utils.newDate()); + isCurrentQuarter = (day, q) => + utils.getYear(day) === utils.getYear(utils.newDate()) && + q === utils.getQuarter(utils.newDate()); + isSelectedMonth = (day, m, selected) => utils.getMonth(day) === m && utils.getYear(day) === utils.getYear(selected); @@ -277,6 +282,37 @@ export default class Month extends React.Component { ); }; + handleQuarterNavigation = (newQuarter, newDate) => { + if (this.isDisabled(newDate) || this.isExcluded(newDate)) return; + this.props.setPreSelection(newDate); + this.QUARTER_REFS[newQuarter - 1].current && + this.QUARTER_REFS[newQuarter - 1].current.focus(); + }; + + onQuarterKeyDown = (event, quarter) => { + const eventKey = event.key; + if (!this.props.disabledKeyboardNavigation) { + switch (eventKey) { + case "Enter": + this.onQuarterClick(event, quarter); + this.props.setPreSelection(this.props.selected); + break; + case "ArrowRight": + this.handleQuarterNavigation( + quarter === 4 ? 1 : quarter + 1, + utils.addQuarters(this.props.preSelection, 1) + ); + break; + case "ArrowLeft": + this.handleQuarterNavigation( + quarter === 1 ? 4 : quarter - 1, + utils.subQuarters(this.props.preSelection, 1) + ); + break; + } + } + }; + getMonthClassNames = (m) => { const { day, @@ -327,6 +363,16 @@ export default class Month extends React.Component { return tabIndex; }; + getQuarterTabIndex = (q) => { + const preSelectedQuarter = utils.getQuarter(this.props.preSelection); + const tabIndex = + !this.props.disabledKeyboardNavigation && q === preSelectedQuarter + ? "0" + : "-1"; + + return tabIndex; + }; + getAriaLabel = (month) => { const { chooseDayAriaLabelPrefix = "Choose", @@ -344,7 +390,15 @@ export default class Month extends React.Component { }; getQuarterClassNames = (q) => { - const { day, startDate, endDate, selected, minDate, maxDate } = this.props; + const { + day, + startDate, + endDate, + selected, + minDate, + maxDate, + preSelection, + } = this.props; return classnames( "react-datepicker__quarter-text", `react-datepicker__quarter-${q}`, @@ -357,6 +411,8 @@ export default class Month extends React.Component { q, selected ), + "react-datepicker__quarter-text--keyboard-selected": + utils.getQuarter(preSelection) === q, "react-datepicker__quarter--in-range": utils.isQuarterInRange( startDate, endDate, @@ -438,12 +494,18 @@ export default class Month extends React.Component { {quarters.map((q, j) => (
{ this.onQuarterClick(ev, q); }} + onKeyDown={(ev) => { + this.onQuarterKeyDown(ev, q); + }} className={this.getQuarterClassNames(q)} aria-selected={this.isSelectedQuarter(day, q, selected)} + tabIndex={this.getQuarterTabIndex(q)} + aria-current={this.isCurrentQuarter(day, q) ? "date" : undefined} > {utils.getQuarterShortInLocale(q, this.props.locale)}
diff --git a/test/month_test.js b/test/month_test.js index 16a834bac..43e7d5b7a 100644 --- a/test/month_test.js +++ b/test/month_test.js @@ -464,6 +464,20 @@ describe("Month", () => { ); }); + it("should enable keyboard focus on the preselected component", () => { + const monthComponent = mount( + + ); + const quarter = monthComponent.find(".react-datepicker__quarter-1"); + expect(quarter.prop("tabIndex")).to.equal("0"); + }); + it("should render full month name", () => { const monthComponent = mount( { expect(month.text()).to.equal("Feb"); }); + describe("Keyboard navigation", () => { + const renderQuarters = (props) => + shallow(); + + it("should trigger setPreSelection and set Q3 as pre-selected on arrowRight", () => { + let preSelected = false; + const setPreSelection = (param) => { + preSelected = param; + }; + + const quartersComponent = renderQuarters({ + selected: utils.newDate("2015-04-01"), + day: utils.newDate("2015-04-01"), + setPreSelection: setPreSelection, + preSelection: utils.newDate("2015-04-01"), + }); + quartersComponent + .find(".react-datepicker__quarter-2") + .simulate("keydown", getKey("Tab")); + quartersComponent + .find(".react-datepicker__quarter-2") + .simulate("keydown", getKey("ArrowRight")); + + expect(preSelected.toString()).to.equal( + utils.newDate("2015-07-01").toString() + ); + }); + + it("should trigger setPreSelection and set Q1 as pre-selected on arrowLeft", () => { + let preSelected = false; + const setPreSelection = (param) => { + preSelected = param; + }; + const quartersComponent = renderQuarters({ + selected: utils.newDate("2015-04-01"), + day: utils.newDate("2015-04-01"), + setPreSelection: setPreSelection, + preSelection: utils.newDate("2015-04-01"), + }); + quartersComponent + .find(".react-datepicker__quarter-2") + .simulate("keydown", getKey("Tab")); + quartersComponent + .find(".react-datepicker__quarter-2") + .simulate("keydown", getKey("ArrowLeft")); + + expect(preSelected.toString()).to.equal( + utils.newDate("2015-01-01").toString() + ); + }); + }); + describe("Keyboard navigation", () => { const renderMonth = (props) => shallow();