From 0efa13aef1fdbeb50d2ac23bd4c0c1c854adbf9f Mon Sep 17 00:00:00 2001 From: Dave Thompson and Mackenzie Fernandez Date: Thu, 18 Feb 2016 21:47:01 -0700 Subject: [PATCH 1/5] refactor: extracted duplicate code into functions --- src/DayPicker.js | 64 +++++++++++++++++++++-------------------------- test/DayPicker.js | 12 ++++----- 2 files changed, 35 insertions(+), 41 deletions(-) diff --git a/src/DayPicker.js b/src/DayPicker.js index 43b22e30bd..e43033327d 100644 --- a/src/DayPicker.js +++ b/src/DayPicker.js @@ -25,7 +25,7 @@ class Caption extends Component { export default class DayPicker extends Component { static propTypes = { - + tabIndex: PropTypes.number, initialMonth: PropTypes.instanceOf(Date), numberOfMonths: PropTypes.number, @@ -54,7 +54,6 @@ export default class DayPicker extends Component { renderDay: PropTypes.func, captionElement: PropTypes.element - }; static defaultProps = { @@ -156,26 +155,35 @@ export default class DayPicker extends Component { }); } - focusPreviousDay(dayNode) { - const body = dayNode.parentNode.parentNode.parentNode.parentNode; - let dayNodes = body.querySelectorAll(".DayPicker-Day:not(.DayPicker-Day--outside)"); - let nodeIndex; + getDayNodes() { + return this.refs.dayPicker.querySelectorAll(".DayPicker-Day:not(.DayPicker-Day--outside)"); + } + + getDayNodeIndex(dayNode, dayNodes) { for (let i = 0; i < dayNodes.length; i++) { if (dayNodes[i] === dayNode) { - nodeIndex = i; - break; + return i; } } + + return -1; + } + + focusFirstDayOfMonth() { + this.getDayNodes()[0].focus(); + } + + focusLastDayOfMonth() { + const dayNodes = this.getDayNodes(); + dayNodes[dayNodes.length - 1].focus(); + } + + focusPreviousDay(dayNode) { + const dayNodes = this.getDayNodes(); + const nodeIndex = this.getDayNodeIndex(dayNode, dayNodes); + if (nodeIndex === 0) { - const { currentMonth } = this.state; - const { numberOfMonths } = this.props; - const prevMonth = DateUtils.addMonths(currentMonth, -numberOfMonths); - this.setState({ - currentMonth: prevMonth - }, () => { - dayNodes = body.querySelectorAll(".DayPicker-Day:not(.DayPicker-Day--outside)"); - dayNodes[dayNodes.length - 1].focus(); - }); + this.showPreviousMonth(() => { this.focusLastDayOfMonth() }) } else { dayNodes[nodeIndex - 1].focus(); @@ -183,26 +191,11 @@ export default class DayPicker extends Component { } focusNextDay(dayNode) { - const body = dayNode.parentNode.parentNode.parentNode.parentNode; - let dayNodes = body.querySelectorAll(".DayPicker-Day:not(.DayPicker-Day--outside)"); - let nodeIndex; - for (let i = 0; i < dayNodes.length; i++) { - if (dayNodes[i] === dayNode) { - nodeIndex = i; - break; - } - } + const dayNodes = this.getDayNodes(); + const nodeIndex = this.getDayNodeIndex(dayNode, dayNodes); if (nodeIndex === dayNodes.length - 1) { - const { currentMonth } = this.state; - const { numberOfMonths } = this.props; - const nextMonth = DateUtils.addMonths(currentMonth, numberOfMonths); - this.setState({ - currentMonth: nextMonth - }, () => { - dayNodes = body.querySelectorAll(".DayPicker-Day:not(.DayPicker-Day--outside)"); - dayNodes[0].focus(); - }); + this.showNextMonth(() => { this.focusFirstDayOfMonth() }); } else { dayNodes[nodeIndex + 1].focus(); @@ -470,6 +463,7 @@ export default class DayPicker extends Component { return (
this.handleKeyDown(e) } diff --git a/test/DayPicker.js b/test/DayPicker.js index 36c22fd2ed..c9d14fadde 100644 --- a/test/DayPicker.js +++ b/test/DayPicker.js @@ -856,7 +856,7 @@ describe("DayPicker", () => { const dayPickerEl = TestUtils.renderIntoDocument( ); - const node = ReactDOM.findDOMNode(dayPickerEl); + const node = dayPickerEl.refs.dayPicker; const dayNode = node.querySelector(".DayPicker-Day:not(.DayPicker-Day--outside)"); TestUtils.Simulate.keyDown(dayNode, { keyCode: keys.ENTER @@ -869,7 +869,7 @@ describe("DayPicker", () => { const dayPickerEl = TestUtils.renderIntoDocument( ); - const node = ReactDOM.findDOMNode(dayPickerEl); + const node = dayPickerEl.refs.dayPicker; const dayNode = node.querySelector(".DayPicker-Day:not(.DayPicker-Day--outside)"); TestUtils.Simulate.keyDown(dayNode, { keyCode: keys.SPACE @@ -882,7 +882,7 @@ describe("DayPicker", () => { const dayPickerEl = TestUtils.renderIntoDocument( ); - const node = ReactDOM.findDOMNode(dayPickerEl); + const node = dayPickerEl.refs.dayPicker; const dayNode = node.querySelector(".DayPicker-Day:not(.DayPicker-Day--outside)"); TestUtils.Simulate.keyDown(dayNode, { keyCode: keys.ENTER @@ -895,8 +895,8 @@ describe("DayPicker", () => { const dayPickerEl = TestUtils.renderIntoDocument( ); - const node = ReactDOM.findDOMNode(dayPickerEl); - const dayNode = node.querySelectorAll(".DayPicker-Day:not(.DayPicker-Day--outside)")[0]; + const node = dayPickerEl.refs.dayPicker; + const dayNode = node.querySelector(".DayPicker-Day:not(.DayPicker-Day--outside)"); const focusPreviousDay = sinon.spy(dayPickerEl, "focusPreviousDay"); TestUtils.Simulate.keyDown(dayNode, { keyCode: keys.LEFT @@ -908,7 +908,7 @@ describe("DayPicker", () => { const dayPickerEl = TestUtils.renderIntoDocument( ); - const node = ReactDOM.findDOMNode(dayPickerEl); + const node = dayPickerEl.refs.dayPicker; const dayNode = node.querySelector(".DayPicker-Day:not(.DayPicker-Day--outside)"); const focusNextDay = sinon.spy(dayPickerEl, "focusNextDay"); TestUtils.Simulate.keyDown(dayNode, { From fb9e2415adf74b459e265afd007977e2c98fbdf1 Mon Sep 17 00:00:00 2001 From: Dave Thompson and Mackenzie Fernandez Date: Thu, 18 Feb 2016 21:47:01 -0700 Subject: [PATCH 2/5] refactor: simplified showNext/PreviousMonth --- src/DayPicker.js | 56 ++++++++++++++++-------------------------------- 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/src/DayPicker.js b/src/DayPicker.js index e43033327d..9804210acb 100644 --- a/src/DayPicker.js +++ b/src/DayPicker.js @@ -84,21 +84,13 @@ export default class DayPicker extends Component { } allowPreviousMonth() { - const { fromMonth } = this.props; - if (!fromMonth) { - return true; - } - const { currentMonth } = this.state; - return Helpers.getMonthsDiff(currentMonth, fromMonth) < 0; + const previousMonth = DateUtils.addMonths(this.state.currentMonth, -1) + return this.allowMonth(previousMonth); } allowNextMonth() { - const { toMonth, numberOfMonths } = this.props; - if (!toMonth) { - return true; - } - const { currentMonth } = this.state; - return Helpers.getMonthsDiff(currentMonth, toMonth) >= numberOfMonths; + const nextMonth = DateUtils.addMonths(this.state.currentMonth, this.props.numberOfMonths); + return this.allowMonth(nextMonth); } allowMonth(d) { @@ -110,24 +102,18 @@ export default class DayPicker extends Component { return true; } - showMonth(d) { + showMonth(d, callback) { if (!this.allowMonth(d)) { return; } + this.setState({ currentMonth: Helpers.startOfMonth(d) - }); + }, callback); } - showNextMonth(callback) { - if (!this.allowNextMonth()) { - return; - } - const { currentMonth } = this.state; - const nextMonth = DateUtils.addMonths(currentMonth, 1); - this.setState({ - currentMonth: nextMonth - }, () => { + showMonthAndCallHandler(d, callback) { + this.showMonth(d, () => { if (callback) { callback(); } @@ -137,22 +123,18 @@ export default class DayPicker extends Component { }); } + showNextMonth(callback) { + if (this.allowNextMonth()) { + const nextMonth = DateUtils.addMonths(this.state.currentMonth, 1); + this.showMonthAndCallHandler(nextMonth, callback); + } + } + showPreviousMonth(callback) { - if (!this.allowPreviousMonth()) { - return; + if (this.allowPreviousMonth()) { + const previousMonth = DateUtils.addMonths(this.state.currentMonth, -1); + this.showMonthAndCallHandler(previousMonth, callback); } - const { currentMonth } = this.state; - const prevMonth = DateUtils.addMonths(currentMonth, -1); - this.setState({ - currentMonth: prevMonth - }, () => { - if (callback) { - callback(); - } - if (this.props.onMonthChange) { - this.props.onMonthChange(this.state.currentMonth); - } - }); } getDayNodes() { From 70f48c84cab49c1c2769a7f41a1051129f90ce2f Mon Sep 17 00:00:00 2001 From: Dave Thompson and Mackenzie Fernandez Date: Thu, 18 Feb 2016 21:47:01 -0700 Subject: [PATCH 3/5] feature: navigate between weeks with up/down arrows --- src/DayPicker.js | 71 ++++++++++++++++++++++++++++++------ test/DayPicker.js | 93 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 151 insertions(+), 13 deletions(-) diff --git a/src/DayPicker.js b/src/DayPicker.js index 9804210acb..17def6a222 100644 --- a/src/DayPicker.js +++ b/src/DayPicker.js @@ -5,7 +5,9 @@ import * as LocaleUtils from "./LocaleUtils"; const keys = { LEFT: 37, + UP: 38, RIGHT: 39, + DOWN: 40, ENTER: 13, SPACE: 32 }; @@ -162,30 +164,70 @@ export default class DayPicker extends Component { focusPreviousDay(dayNode) { const dayNodes = this.getDayNodes(); - const nodeIndex = this.getDayNodeIndex(dayNode, dayNodes); + const dayNodeIndex = this.getDayNodeIndex(dayNode, dayNodes); - if (nodeIndex === 0) { + if (dayNodeIndex === 0) { this.showPreviousMonth(() => { this.focusLastDayOfMonth() }) } else { - dayNodes[nodeIndex - 1].focus(); + dayNodes[dayNodeIndex - 1].focus(); } } focusNextDay(dayNode) { const dayNodes = this.getDayNodes(); - const nodeIndex = this.getDayNodeIndex(dayNode, dayNodes); + const dayNodeIndex = this.getDayNodeIndex(dayNode, dayNodes); - if (nodeIndex === dayNodes.length - 1) { + if (dayNodeIndex === dayNodes.length - 1) { this.showNextMonth(() => { this.focusFirstDayOfMonth() }); } else { - dayNodes[nodeIndex + 1].focus(); + dayNodes[dayNodeIndex + 1].focus(); + } + } + + focusNextWeek(dayNode) { + const dayNodes = this.getDayNodes(); + const dayNodeIndex = this.getDayNodeIndex(dayNode, dayNodes); + const isInLastWeekOfMonth = dayNodeIndex > dayNodes.length - 8; + + if (isInLastWeekOfMonth) { + this.showNextMonth(() => { + const daysAfterIndex = dayNodes.length - dayNodeIndex; + const nextMonthDayNodeIndex = 7 - daysAfterIndex; + this.getDayNodes()[nextMonthDayNodeIndex].focus(); + }); + } + else { + dayNodes[dayNodeIndex + 7].focus(); + } + } + + focusPreviousWeek(dayNode) { + const dayNodes = this.getDayNodes(); + const dayNodeIndex = this.getDayNodeIndex(dayNode, dayNodes); + const isInFirstWeekOfMonth = dayNodeIndex <= 6; + + if (isInFirstWeekOfMonth) { + this.showPreviousMonth(() => { + const previousMonthDayNodes = this.getDayNodes(); + const startOfLastWeekOfMonth = previousMonthDayNodes.length - 7; + const previousMonthDayNodeIndex = startOfLastWeekOfMonth + dayNodeIndex; + previousMonthDayNodes[previousMonthDayNodeIndex].focus(); + }); + } + else { + dayNodes[dayNodeIndex - 7].focus(); } } // Event handlers + cancelEvent(e) { + e.preventDefault(); + e.stopPropagation(); + } + handleKeyDown(e) { e.persist(); @@ -212,19 +254,24 @@ export default class DayPicker extends Component { e.persist(); switch (e.keyCode) { case keys.LEFT: - e.preventDefault(); - e.stopPropagation(); + this.cancelEvent(e); this.focusPreviousDay(e.target); break; case keys.RIGHT: - e.preventDefault(); - e.stopPropagation(); + this.cancelEvent(e); this.focusNextDay(e.target); break; + case keys.UP: + this.cancelEvent(e); + this.focusPreviousWeek(e.target); + break; + case keys.DOWN: + this.cancelEvent(e); + this.focusNextWeek(e.target); + break; case keys.ENTER: case keys.SPACE: - e.preventDefault(); - e.stopPropagation(); + this.cancelEvent(e); if (this.props.onDayClick) { this.handleDayClick(e, day, modifiers); } diff --git a/test/DayPicker.js b/test/DayPicker.js index c9d14fadde..401cea199b 100644 --- a/test/DayPicker.js +++ b/test/DayPicker.js @@ -20,6 +20,8 @@ const DayPicker = require("../src/DayPicker").default; const keys = { LEFT: 37, RIGHT: 39, + UP: 38, + DOWN: 40, ENTER: 13, SPACE: 32 }; @@ -917,6 +919,32 @@ describe("DayPicker", () => { expect(focusNextDay).to.be.called; }); + it("calls focusNextWeek when down key is pressed", () => { + const dayPickerEl = TestUtils.renderIntoDocument( + + ); + const node = dayPickerEl.refs.dayPicker; + const dayNode = node.querySelector(".DayPicker-Day:not(.DayPicker-Day--outside)"); + const focusNextWeek = sinon.spy(dayPickerEl, "focusNextWeek"); + TestUtils.Simulate.keyDown(dayNode, { + keyCode: keys.DOWN + }); + expect(focusNextWeek).to.be.called; + }); + + it("calls focusPreviousWeek when up key is pressed", () => { + const dayPickerEl = TestUtils.renderIntoDocument( + + ); + const node = dayPickerEl.refs.dayPicker; + const dayNode = node.querySelector(".DayPicker-Day:not(.DayPicker-Day--outside)"); + const focusPreviousWeek = sinon.spy(dayPickerEl, "focusPreviousWeek"); + TestUtils.Simulate.keyDown(dayNode, { + keyCode: keys.UP + }); + expect(focusPreviousWeek).to.be.called; + }); + describe("handleKeyDown", () => { it("should handle keydown event", () => { @@ -1019,9 +1047,72 @@ describe("DayPicker", () => { expect(document.activeElement.innerHTML).to.equal("1"); expect(dayPickerEl.state.currentMonth.getMonth()).to.equal(6); - }); + describe("change week", () => { + it("focuses the same day of the next week", () => { + const focusedNode = getDayNode(body, 2, 1); + expect(focusedNode.innerHTML).to.equal("15"); + + dayPickerEl.focusNextWeek(focusedNode); + expect(document.activeElement.innerHTML).to.equal("22"); + expect(dayPickerEl.state.currentMonth.getMonth()).to.equal(5); + }); + + it("focuses the same day of the next week in the next month", () => { + const juneThirtieth = getDayNode(body, 4, 2); + expect(juneThirtieth.innerHTML).to.equal("30"); + + dayPickerEl.focusNextWeek(juneThirtieth); + expect(document.activeElement.innerHTML).to.equal("7"); + expect(dayPickerEl.state.currentMonth.getMonth()).to.equal(6); + + const julyThirtyFirst = getDayNode(body, 4, 5); + expect(julyThirtyFirst.innerHTML).to.equal("31"); + + dayPickerEl.focusNextWeek(julyThirtyFirst); + expect(document.activeElement.innerHTML).to.equal("7"); + expect(dayPickerEl.state.currentMonth.getMonth()).to.equal(7); + }); + + it("focuses the first day of the next month after leapday", () => { + dayPickerEl = TestUtils.renderIntoDocument( + + ); + body = ReactDOM.findDOMNode(TestUtils.findRenderedDOMComponentWithClass(dayPickerEl, "DayPicker-Body")); + const focusedNode = getDayNode(body, 4, 1); + expect(focusedNode.innerHTML).to.equal("29"); + + dayPickerEl.focusNextDay(focusedNode); + expect(document.activeElement.innerHTML).to.equal("1"); + expect(dayPickerEl.state.currentMonth.getMonth()).to.equal(2); + }); + + it("focuses the same day of the previous week", () => { + const focusedNode = getDayNode(body, 2, 1); + expect(focusedNode.innerHTML).to.equal("15"); + + dayPickerEl.focusPreviousWeek(focusedNode); + expect(document.activeElement.innerHTML).to.equal("8"); + expect(dayPickerEl.state.currentMonth.getMonth()).to.equal(5); + }); + + it("focuses the same day of the previous week in the previous month", () => { + const juneFirst = getDayNode(body, 0, 1); + expect(juneFirst.innerHTML).to.equal("1"); + + dayPickerEl.focusPreviousWeek(juneFirst); + expect(document.activeElement.innerHTML).to.equal("25"); + expect(dayPickerEl.state.currentMonth.getMonth()).to.equal(4); + + const maySecond = getDayNode(body, 1, 0); + expect(maySecond.innerHTML).to.equal("3"); + + dayPickerEl.focusPreviousWeek(maySecond); + expect(document.activeElement.innerHTML).to.equal("26"); + expect(dayPickerEl.state.currentMonth.getMonth()).to.equal(3); + }); + }); }); }); From 5bc70e358de7816ed677dab90132946c642570e4 Mon Sep 17 00:00:00 2001 From: Dave Thompson and Mackenzie Fernandez Date: Thu, 18 Feb 2016 21:47:01 -0700 Subject: [PATCH 4/5] feature: navigate year using up/down keys --- src/DayPicker.js | 16 +++++++++ test/DayPicker.js | 84 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/src/DayPicker.js b/src/DayPicker.js index 17def6a222..8e2444dfd7 100644 --- a/src/DayPicker.js +++ b/src/DayPicker.js @@ -139,6 +139,16 @@ export default class DayPicker extends Component { } } + showNextYear(callback) { + const nextMonth = DateUtils.addMonths(this.state.currentMonth, 12); + this.showMonthAndCallHandler(nextMonth, callback); + } + + showPreviousYear(callback) { + const nextMonth = DateUtils.addMonths(this.state.currentMonth, -12); + this.showMonthAndCallHandler(nextMonth, callback); + } + getDayNodes() { return this.refs.dayPicker.querySelectorAll(".DayPicker-Day:not(.DayPicker-Day--outside)"); } @@ -246,6 +256,12 @@ export default class DayPicker extends Component { case keys.RIGHT: this.showNextMonth(callback); break; + case keys.UP: + this.showPreviousYear(callback); + break; + case keys.DOWN: + this.showNextYear(callback); + break; } } } diff --git a/test/DayPicker.js b/test/DayPicker.js index 401cea199b..250ce48392 100644 --- a/test/DayPicker.js +++ b/test/DayPicker.js @@ -426,6 +426,90 @@ describe("DayPicker", () => { }); }); + describe("showPreviousYear", () => { + it("shows the previous year", () => { + const callback = sinon.spy(); + const handleMonthChange = sinon.spy(); + const dayPickerEl = TestUtils.renderIntoDocument( + + ); + dayPickerEl.showPreviousYear(callback); + + expect(dayPickerEl.state.currentMonth.getMonth()).to.equal(7); + expect(dayPickerEl.state.currentMonth.getDate()).to.equal(1); + expect(dayPickerEl.state.currentMonth.getFullYear()).to.equal(2014); + expect(callback).to.have.been.called; + expect(handleMonthChange).to.have.been.called; + }); + + it("does not show a month before `fromMonth`", () => { + const dayPickerEl = TestUtils.renderIntoDocument( + + ); + dayPickerEl.showPreviousYear(); + expect(dayPickerEl.state.currentMonth.getMonth()).to.equal(10); + expect(dayPickerEl.state.currentMonth.getFullYear()).to.equal(2015); + }); + + it("is called when up key is pressed over the root node", () => { + const dayPickerEl = TestUtils.renderIntoDocument( + + ); + const showPreviousYear = sinon.spy(dayPickerEl, "showPreviousYear"); + TestUtils.Simulate.keyDown(ReactDOM.findDOMNode(dayPickerEl), { + keyCode: keys.UP + }); + expect(showPreviousYear).to.be.called; + }); + }); + + describe("showNextYear", () => { + it("shows the next year", () => { + const callback = sinon.spy(); + const handleMonthChange = sinon.spy(); + const dayPickerEl = TestUtils.renderIntoDocument( + + ); + dayPickerEl.showNextYear(callback); + + expect(dayPickerEl.state.currentMonth.getMonth()).to.equal(7); + expect(dayPickerEl.state.currentMonth.getDate()).to.equal(1); + expect(dayPickerEl.state.currentMonth.getFullYear()).to.equal(2016); + expect(callback).to.have.been.called; + expect(handleMonthChange).to.have.been.called; + }); + + it("does not show a month after `toMonth`", () => { + const dayPickerEl = TestUtils.renderIntoDocument( + + ); + dayPickerEl.showNextYear(); + expect(dayPickerEl.state.currentMonth.getMonth()).to.equal(10); + expect(dayPickerEl.state.currentMonth.getFullYear()).to.equal(2015); + }); + + it("is called when down key is pressed over the root node", () => { + const dayPickerEl = TestUtils.renderIntoDocument( + + ); + const showNextYear = sinon.spy(dayPickerEl, "showNextYear"); + TestUtils.Simulate.keyDown(ReactDOM.findDOMNode(dayPickerEl), { + keyCode: keys.DOWN + }); + expect(showNextYear).to.be.called; + }); + }); + describe("showMonth", () => { it("shows the specified month", () => { const dayPickerEl = TestUtils.renderIntoDocument( From 8cb34b96b1dbf426176074c372638201bd4bff0c Mon Sep 17 00:00:00 2001 From: Dave Thompson and Mackenzie Fernandez Date: Fri, 19 Feb 2016 10:24:03 -0700 Subject: [PATCH 5/5] feature: improved aria labels --- src/DayPicker.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/DayPicker.js b/src/DayPicker.js index 8e2444dfd7..f48d9c2932 100644 --- a/src/DayPicker.js +++ b/src/DayPicker.js @@ -356,6 +356,7 @@ export default class DayPicker extends Component { const leftButton = isRTL ? this.allowNextMonth() : this.allowPreviousMonth(); const rightButton = isRTL ? this.allowPreviousMonth() : this.allowNextMonth(); + return (
{ leftButton && @@ -391,12 +392,12 @@ export default class DayPicker extends Component { { caption } -
-
+
+
{ this.renderWeekDays() }
-
+
{ this.renderWeeksInMonth(date) }
@@ -467,10 +468,17 @@ export default class DayPicker extends Component { tabIndex = this.props.tabIndex; } } + + const ariaLabel = this.props.localeUtils.formatDate ? + this.props.localeUtils.formatDate(day) : day.toDateString(); + const ariaDisabled = isOutside ? "true" : "false"; + return (
this.handleDayKeyDown(e, day, modifiers) } onMouseEnter= { onDayMouseEnter ?