diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cad6598ea..6d0a9c2bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,11 +28,16 @@ Start reading our code, and you'll get the hang of it. We optimize for readabili Local development configuration is pretty snappy. Here's how to get set up: 1. Install/use node >=16.0.0 +1. Install/use yarn <=1.x.x 1. Run `yarn link` from project root 1. Run `cd docs-site && yarn link react-datepicker` 1. Run `yarn install` from project root 1. 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) -1. Run `yarn start` from project root +1. Run `yarn start` from project root. (This command launches a documentation app and runs it as a simple webserver at http://localhost:3000.) 1. Open new terminal window +1. Run `yarn build-dev` from project root. (This command sets up a development environment that keeps an eye on any file changes. When a file is updated, it auto-builds using the latest code.) + +You can run `yarn test` to execute the test suite and linters. To help you develop the component we’ve set up some tests that cover the basic functionality (can be found in `/tests`). Even though we’re big fans of testing, this only covers a small piece of the component. We highly recommend you add tests when you’re adding new functionality. + 1. After each JS change run `yarn build:js` in project root 1. After each SCSS change run `yarn run css:dev && yarn run css:modules:dev` in project root diff --git a/docs-site/src/components/Examples/index.js b/docs-site/src/components/Examples/index.js index e4f98d673..0f3606fd6 100644 --- a/docs-site/src/components/Examples/index.js +++ b/docs-site/src/components/Examples/index.js @@ -79,6 +79,7 @@ import RenderCustomYear from "../../examples/renderCustomYear"; import TimeInput from "../../examples/timeInput"; import StrictParsing from "../../examples/strictParsing"; import MonthPicker from "../../examples/monthPicker"; +import WeekPicker from "../../examples/weekPicker"; import monthPickerFullName from "../../examples/monthPickerFullName"; import monthPickerTwoColumns from "../../examples/monthPickerTwoColumns"; import monthPickerFourColumns from "../../examples/monthPickerFourColumns"; @@ -512,6 +513,10 @@ export default class exampleComponents extends React.Component { title: "Calendar Start day", component: CalendarStartDay, }, + { + title: "Week Picker", + component: WeekPicker, + }, { title: "External Form", component: ExternalForm, diff --git a/docs-site/src/examples/weekPicker.js b/docs-site/src/examples/weekPicker.js new file mode 100644 index 000000000..d4fc21c64 --- /dev/null +++ b/docs-site/src/examples/weekPicker.js @@ -0,0 +1,13 @@ +() => { + const [startDate, setStartDate] = useState(new Date("2021/02/22")); + return ( + setStartDate(date)} + dateFormat="I/R" + locale="en-GB" + showWeekNumbers + showWeekPicker + /> + ); +}; diff --git a/docs/index.md b/docs/index.md index 21e8fba42..ee320e015 100644 --- a/docs/index.md +++ b/docs/index.md @@ -133,6 +133,7 @@ | `showTimeSelectOnly` | `bool` | | | | `showTwoColumnMonthYearPicker` | `bool` | `false` | | | `showWeekNumbers` | `bool` | | | +| `showWeekPicker` | `bool` | `false` | | | `showYearDropdown` | `bool` | | | | `showYearPicker` | `bool` | `false` | | | `startDate` | `instanceOfDate` | | | diff --git a/src/calendar.jsx b/src/calendar.jsx index 7a6cea6e8..9b666c623 100644 --- a/src/calendar.jsx +++ b/src/calendar.jsx @@ -140,6 +140,7 @@ export default class Calendar extends React.Component { showFourColumnMonthYearPicker: PropTypes.bool, showYearPicker: PropTypes.bool, showQuarterYearPicker: PropTypes.bool, + showWeekPicker: PropTypes.bool, showTimeSelectOnly: PropTypes.bool, timeFormat: PropTypes.string, timeIntervals: PropTypes.number, @@ -941,6 +942,7 @@ export default class Calendar extends React.Component { } showYearPicker={this.props.showYearPicker} showQuarterYearPicker={this.props.showQuarterYearPicker} + showWeekPicker={this.props.showWeekPicker} isInputFocused={this.props.isInputFocused} containerRef={this.containerRef} monthShowsDuplicateDaysEnd={monthShowsDuplicateDaysEnd} diff --git a/src/day.jsx b/src/day.jsx index 7efb85faf..a89e2a68e 100644 --- a/src/day.jsx +++ b/src/day.jsx @@ -14,6 +14,7 @@ import { isBefore, isAfter, getDayOfWeekCode, + getStartOfWeek, formatDate, } from "./date_utils"; @@ -38,6 +39,8 @@ export default class Day extends React.Component { selectsEnd: PropTypes.bool, selectsStart: PropTypes.bool, selectsRange: PropTypes.bool, + showWeekPicker: PropTypes.bool, + showWeekNumber: PropTypes.bool, selectsDisabledDaysInRange: PropTypes.bool, startDate: PropTypes.instanceOf(Date), renderDayContents: PropTypes.func, @@ -52,6 +55,7 @@ export default class Day extends React.Component { PropTypes.string, PropTypes.shape({ locale: PropTypes.object }), ]), + calendarStartDay: PropTypes.number, }; componentDidMount() { @@ -90,13 +94,38 @@ export default class Day extends React.Component { isKeyboardSelected = () => !this.props.disabledKeyboardNavigation && - !this.isSameDay(this.props.selected) && - this.isSameDay(this.props.preSelection); + !( + this.isSameDay(this.props.selected) || + this.isSameWeek(this.props.selected) + ) && + (this.isSameDay(this.props.preSelection) || + this.isSameWeek(this.props.preSelection)); isDisabled = () => isDayDisabled(this.props.day, this.props); isExcluded = () => isDayExcluded(this.props.day, this.props); + isStartOfWeek = () => + isSameDay( + this.props.day, + getStartOfWeek( + this.props.day, + this.props.locale, + this.props.calendarStartDay, + ), + ); + + isSameWeek = (other) => + this.props.showWeekPicker && + isSameDay( + other, + getStartOfWeek( + this.props.day, + this.props.locale, + this.props.calendarStartDay, + ), + ); + getHighLightedClass = () => { const { day, highlightDates } = this.props; @@ -246,7 +275,8 @@ export default class Day extends React.Component { isCurrentDay = () => this.isSameDay(newDate()); - isSelected = () => this.isSameDay(this.props.selected); + isSelected = () => + this.isSameDay(this.props.selected) || this.isSameWeek(this.props.selected); getClassNames = (date) => { const dayClassName = this.props.dayClassName @@ -309,10 +339,14 @@ export default class Day extends React.Component { getTabIndex = (selected, preSelection) => { const selectedDay = selected || this.props.selected; const preSelectionDay = preSelection || this.props.preSelection; - const tabIndex = - this.isKeyboardSelected() || - (this.isSameDay(selectedDay) && isSameDay(preSelectionDay, selectedDay)) + !( + this.props.showWeekPicker && + (this.props.showWeekNumber || !this.isStartOfWeek()) + ) && + (this.isKeyboardSelected() || + (this.isSameDay(selectedDay) && + isSameDay(preSelectionDay, selectedDay))) ? 0 : -1; diff --git a/src/index.jsx b/src/index.jsx index 89885ec7a..d8ad254fb 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -37,6 +37,7 @@ import { getHightLightDaysMap, getYear, getMonth, + getStartOfWeek, registerLocale, setDefaultLocale, getDefaultLocale, @@ -108,6 +109,7 @@ export default class DatePicker extends React.Component { showFourColumnMonthYearPicker: false, showYearPicker: false, showQuarterYearPicker: false, + showWeekPicker: false, strictParsing: false, timeIntervals: 30, timeCaption: "Time", @@ -256,6 +258,7 @@ export default class DatePicker extends React.Component { showFourColumnMonthYearPicker: PropTypes.bool, showYearPicker: PropTypes.bool, showQuarterYearPicker: PropTypes.bool, + showWeekPicker: PropTypes.bool, showDateSelect: PropTypes.bool, showTimeSelect: PropTypes.bool, showTimeSelectOnly: PropTypes.bool, @@ -552,6 +555,13 @@ export default class DatePicker extends React.Component { }); } if (date || !event.target.value) { + if (this.props.showWeekPicker) { + date = getStartOfWeek( + date, + this.props.locale, + this.props.calendarStartDay, + ); + } this.setSelected(date, event, true); } }; @@ -565,6 +575,13 @@ export default class DatePicker extends React.Component { if (this.props.onChangeRaw) { this.props.onChangeRaw(event); } + if (this.props.showWeekPicker) { + date = getStartOfWeek( + date, + this.props.locale, + this.props.calendarStartDay, + ); + } this.setSelected(date, event, false, monthSelectedIn); if (this.props.showDateSelect) { this.setState({ isRenderAriaLiveMessage: true }); @@ -665,6 +682,13 @@ export default class DatePicker extends React.Component { const hasMaxDate = typeof this.props.maxDate !== "undefined"; let isValidDateSelection = true; if (date) { + if (this.props.showWeekPicker) { + date = getStartOfWeek( + date, + this.props.locale, + this.props.calendarStartDay, + ); + } const dateStartOfDay = startOfDay(date); if (hasMinDate && hasMaxDate) { // isDayinRange uses startOfDay internally, so not necessary to manipulate times here @@ -748,16 +772,18 @@ export default class DatePicker extends React.Component { return; } - // if calendar is open, these keys will focus the selected day + // if calendar is open, these keys will focus the selected item if (this.state.open) { if (eventKey === "ArrowDown" || eventKey === "ArrowUp") { event.preventDefault(); - const selectedDay = + const selectorString = + this.props.showWeekPicker && this.props.showWeekNumbers + ? '.react-datepicker__week-number[tabindex="0"]' + : '.react-datepicker__day[tabindex="0"]'; + const selectedItem = this.calendar.componentNode && - this.calendar.componentNode.querySelector( - '.react-datepicker__day[tabindex="0"]', - ); - selectedDay && selectedDay.focus({ preventScroll: true }); + this.calendar.componentNode.querySelector(selectorString); + selectedItem && selectedItem.focus({ preventScroll: true }); return; } @@ -828,10 +854,18 @@ export default class DatePicker extends React.Component { let newSelection; switch (eventKey) { case "ArrowLeft": - newSelection = subDays(copy, 1); + if (this.props.showWeekPicker) { + newSelection = subWeeks(copy, 1); + } else { + newSelection = subDays(copy, 1); + } break; case "ArrowRight": - newSelection = addDays(copy, 1); + if (this.props.showWeekPicker) { + newSelection = addWeeks(copy, 1); + } else { + newSelection = addDays(copy, 1); + } break; case "ArrowUp": newSelection = subWeeks(copy, 1); @@ -851,6 +885,9 @@ export default class DatePicker extends React.Component { case "End": newSelection = addYears(copy, 1); break; + default: + newSelection = null; + break; } if (!newSelection) { if (this.props.onInputError) { @@ -1047,6 +1084,7 @@ export default class DatePicker extends React.Component { showFourColumnMonthYearPicker={this.props.showFourColumnMonthYearPicker} showYearPicker={this.props.showYearPicker} showQuarterYearPicker={this.props.showQuarterYearPicker} + showWeekPicker={this.props.showWeekPicker} showPopperArrow={this.props.showPopperArrow} excludeScrollbar={this.props.excludeScrollbar} handleOnKeyDown={this.props.onKeyDown} diff --git a/src/month.jsx b/src/month.jsx index 488825da4..c374b4743 100644 --- a/src/month.jsx +++ b/src/month.jsx @@ -111,6 +111,7 @@ export default class Month extends React.Component { showTwoColumnMonthYearPicker: PropTypes.bool, showFourColumnMonthYearPicker: PropTypes.bool, showQuarterYearPicker: PropTypes.bool, + showWeekPicker: PropTypes.bool, handleOnKeyDown: PropTypes.func, isInputFocused: PropTypes.bool, weekAriaLabelPrefix: PropTypes.string, @@ -330,6 +331,7 @@ export default class Month extends React.Component { selectsRange={this.props.selectsRange} selectsDisabledDaysInRange={this.props.selectsDisabledDaysInRange} showWeekNumber={this.props.showWeekNumbers} + showWeekPicker={this.props.showWeekPicker} startDate={this.props.startDate} endDate={this.props.endDate} dayClassName={this.props.dayClassName} @@ -718,6 +720,7 @@ export default class Month extends React.Component { selectsEnd, showMonthYearPicker, showQuarterYearPicker, + showWeekPicker, } = this.props; return classnames( @@ -728,6 +731,7 @@ export default class Month extends React.Component { }, { "react-datepicker__monthPicker": showMonthYearPicker }, { "react-datepicker__quarterPicker": showQuarterYearPicker }, + { "react-datepicker__weekPicker": showWeekPicker }, ); }; diff --git a/src/stylesheets/datepicker.scss b/src/stylesheets/datepicker.scss index 137cf970a..e02152cc5 100644 --- a/src/stylesheets/datepicker.scss +++ b/src/stylesheets/datepicker.scss @@ -368,22 +368,45 @@ &.react-datepicker__week-number--clickable { cursor: pointer; - &:hover { + &:not( + .react-datepicker__week-number--selected, + .react-datepicker__week-number--keyboard-selected + ):hover { border-radius: $datepicker__border-radius; background-color: $datepicker__background-color; } } -} -.react-datepicker__day-names, -.react-datepicker__week { - white-space: nowrap; + &--selected { + border-radius: $datepicker__border-radius; + background-color: $datepicker__selected-color; + color: #fff; + + &:hover { + background-color: darken($datepicker__selected-color, 5%); + } + } + + &--keyboard-selected { + border-radius: $datepicker__border-radius; + background-color: lighten($datepicker__selected-color, 10%); + color: #fff; + + &:hover { + background-color: darken($datepicker__selected-color, 5%); + } + } } .react-datepicker__day-names { + white-space: nowrap; margin-bottom: -8px; } +.react-datepicker__week { + white-space: nowrap; +} + .react-datepicker__day-name, .react-datepicker__day, .react-datepicker__time-name { diff --git a/src/week.jsx b/src/week.jsx index 08987c77d..22e7e8e9b 100644 --- a/src/week.jsx +++ b/src/week.jsx @@ -2,7 +2,9 @@ import React from "react"; import PropTypes from "prop-types"; import Day from "./day"; import WeekNumber from "./week_number"; -import * as utils from "./date_utils"; +import classnames from "classnames"; + +import { addDays, getWeek, getStartOfWeek, isSameDay } from "./date_utils"; export default class Week extends React.Component { static get defaultProps() { @@ -52,6 +54,7 @@ export default class Week extends React.Component { selectsRange: PropTypes.bool, selectsDisabledDaysInRange: PropTypes.bool, showWeekNumber: PropTypes.bool, + showWeekPicker: PropTypes.bool, startDate: PropTypes.instanceOf(Date), setOpen: PropTypes.func, shouldCloseOnSelect: PropTypes.bool, @@ -82,6 +85,14 @@ export default class Week extends React.Component { if (typeof this.props.onWeekSelect === "function") { this.props.onWeekSelect(day, weekNumber, event); } + if (this.props.showWeekPicker) { + const startOfWeek = getStartOfWeek( + day, + this.props.locale, + this.props.calendarStartDay, + ); + this.handleDayClick(startOfWeek, event); + } if (this.props.shouldCloseOnSelect) { this.props.setOpen(false); } @@ -91,11 +102,11 @@ export default class Week extends React.Component { if (this.props.formatWeekNumber) { return this.props.formatWeekNumber(date); } - return utils.getWeek(date); + return getWeek(date); }; renderDays = () => { - const startOfWeek = utils.getStartOfWeek( + const startOfWeek = getStartOfWeek( this.props.day, this.props.locale, this.props.calendarStartDay, @@ -103,21 +114,31 @@ export default class Week extends React.Component { const days = []; const weekNumber = this.formatWeekNumber(startOfWeek); if (this.props.showWeekNumber) { - const onClickAction = this.props.onWeekSelect - ? this.handleWeekClick.bind(this, startOfWeek, weekNumber) - : undefined; + const onClickAction = + this.props.onWeekSelect || this.props.showWeekPicker + ? this.handleWeekClick.bind(this, startOfWeek, weekNumber) + : undefined; days.push( , ); } return days.concat( [0, 1, 2, 3, 4, 5, 6].map((offset) => { - const day = utils.addDays(startOfWeek, offset); + const day = addDays(startOfWeek, offset); return ( + getStartOfWeek( + this.props.day, + this.props.locale, + this.props.calendarStartDay, + ); + + isKeyboardSelected = () => + !this.props.disabledKeyboardNavigation && + !isSameDay(this.startOfWeek(), this.props.selected) && + isSameDay(this.startOfWeek(), this.props.preSelection); + render() { - return
{this.renderDays()}
; + const weekNumberClasses = { + "react-datepicker__week": true, + "react-datepicker__week--selected": isSameDay( + this.startOfWeek(), + this.props.selected, + ), + "react-datepicker__week--keyboard-selected": this.isKeyboardSelected(), + }; + return ( +
{this.renderDays()}
+ ); } } diff --git a/src/week_number.jsx b/src/week_number.jsx index f69489eec..b5a057897 100644 --- a/src/week_number.jsx +++ b/src/week_number.jsx @@ -1,6 +1,7 @@ import React from "react"; import PropTypes from "prop-types"; import classnames from "classnames"; +import { isSameDay } from "./date_utils"; export default class WeekNumber extends React.Component { static get defaultProps() { @@ -11,27 +12,125 @@ export default class WeekNumber extends React.Component { static propTypes = { weekNumber: PropTypes.number.isRequired, + date: PropTypes.instanceOf(Date).isRequired, onClick: PropTypes.func, ariaLabelPrefix: PropTypes.string, + selected: PropTypes.instanceOf(Date), + preSelection: PropTypes.instanceOf(Date), + showWeekPicker: PropTypes.bool, + showWeekNumber: PropTypes.bool, + disabledKeyboardNavigation: PropTypes.bool, + inline: PropTypes.bool, + shouldFocusDayInline: PropTypes.bool, + handleOnKeyDown: PropTypes.func, + containerRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + ]), }; + componentDidMount() { + this.handleFocusWeekNumber(); + } + + componentDidUpdate(prevProps) { + this.handleFocusWeekNumber(prevProps); + } + + weekNumberEl = React.createRef(); + handleClick = (event) => { if (this.props.onClick) { this.props.onClick(event); } }; + handleOnKeyDown = (event) => { + const eventKey = event.key; + if (eventKey === " ") { + event.preventDefault(); + event.key = "Enter"; + } + + this.props.handleOnKeyDown(event); + }; + + isKeyboardSelected = () => + !this.props.disabledKeyboardNavigation && + !isSameDay(this.props.date, this.props.selected) && + isSameDay(this.props.date, this.props.preSelection); + + getTabIndex = () => + this.props.showWeekPicker && + this.props.showWeekNumber && + (this.isKeyboardSelected() || + (isSameDay(this.props.date, this.props.selected) && + isSameDay(this.props.preSelection, this.props.selected))) + ? 0 + : -1; + + // various cases when we need to apply focus to the preselected week-number + // focus the week-number 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 + handleFocusWeekNumber = (prevProps = {}) => { + let shouldFocusWeekNumber = 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 && + isSameDay(this.props.date, this.props.preSelection) + ) { + // there is currently no activeElement and not inline + if (!document.activeElement || document.activeElement === document.body) { + shouldFocusWeekNumber = true; + } + // inline version: + // do not focus on initial render to prevent autoFocus issue + // focus after month has changed via keyboard + if (this.props.inline && !this.props.shouldFocusDayInline) { + shouldFocusWeekNumber = false; + } + // the activeElement is in the container, and it is another instance of WeekNumber + if ( + this.props.containerRef && + this.props.containerRef.current && + this.props.containerRef.current.contains(document.activeElement) && + document.activeElement && + document.activeElement.classList.contains( + "react-datepicker__week-number", + ) + ) { + shouldFocusWeekNumber = true; + } + } + + shouldFocusWeekNumber && + this.weekNumberEl.current && + this.weekNumberEl.current.focus({ preventScroll: true }); + }; + render() { const { weekNumber, ariaLabelPrefix = "week ", onClick } = this.props; + const weekNumberClasses = { "react-datepicker__week-number": true, "react-datepicker__week-number--clickable": !!onClick, + "react-datepicker__week-number--selected": isSameDay( + this.props.date, + this.props.selected, + ), + "react-datepicker__week-number--keyboard-selected": + this.isKeyboardSelected(), }; return (
{weekNumber}
diff --git a/test/calendar_test.test.js b/test/calendar_test.test.js index ee6960771..183bde46f 100644 --- a/test/calendar_test.test.js +++ b/test/calendar_test.test.js @@ -6,7 +6,7 @@ import React from "react"; import Calendar from "../src/calendar"; import Month from "../src/month"; import Day from "../src/day"; -import { findDOMNode } from "react-dom"; +import ReactDOM from "react-dom"; import TestUtils from "react-dom/test-utils"; import YearDropdown from "../src/year_dropdown"; import MonthDropdown from "../src/month_dropdown"; @@ -933,7 +933,7 @@ describe("Calendar", () => { }} />, ); - TestUtils.Simulate.focus(findDOMNode(datePicker.input)); + TestUtils.Simulate.focus(ReactDOM.findDOMNode(datePicker.input)); const calendar = TestUtils.scryRenderedComponentsWithType( datePicker.calendar, Calendar, @@ -958,7 +958,7 @@ describe("Calendar", () => { }} />, ); - TestUtils.Simulate.focus(findDOMNode(datePicker.input)); + TestUtils.Simulate.focus(ReactDOM.findDOMNode(datePicker.input)); const calendar = TestUtils.scryRenderedComponentsWithType( datePicker.calendar, Calendar, @@ -983,7 +983,7 @@ describe("Calendar", () => { }} />, ); - TestUtils.Simulate.focus(findDOMNode(datePicker.input)); + TestUtils.Simulate.focus(ReactDOM.findDOMNode(datePicker.input)); const calendar = TestUtils.scryRenderedComponentsWithType( datePicker.calendar, Calendar, @@ -1007,11 +1007,11 @@ describe("Calendar", () => { />, ); - TestUtils.Simulate.focus(findDOMNode(datePicker.input)); + TestUtils.Simulate.focus(ReactDOM.findDOMNode(datePicker.input)); expect(onCalendarOpen).toHaveBeenCalled(); - TestUtils.Simulate.blur(findDOMNode(datePicker.input)); + TestUtils.Simulate.blur(ReactDOM.findDOMNode(datePicker.input)); expect(onCalendarOpen).toHaveBeenCalled(); }); @@ -1186,12 +1186,16 @@ describe("Calendar", () => { }); describe("localization", () => { - function testLocale(calendar, selected, locale) { + function testLocale(calendar, selected, locale, calendarStartDay) { const calendarText = calendar.find(".react-datepicker__current-month"); expect(calendarText.text()).toBe( utils.formatDate(selected, dateFormat, locale), ); - const firstDateOfWeek = utils.getStartOfWeek(selected, locale); + const firstDateOfWeek = utils.getStartOfWeek( + selected, + locale, + calendarStartDay, + ); const firstWeekDayMin = utils.getWeekdayMinInLocale( firstDateOfWeek, locale, @@ -1585,7 +1589,7 @@ describe("Calendar", () => { , ); const dateInput = datePicker.input; - TestUtils.Simulate.focus(findDOMNode(dateInput)); + TestUtils.Simulate.focus(ReactDOM.findDOMNode(dateInput)); TestUtils.Simulate.click( TestUtils.findRenderedDOMComponentWithClass( datePicker, @@ -1605,7 +1609,7 @@ describe("Calendar", () => { , ); const dateInput = datePicker.input; - TestUtils.Simulate.focus(findDOMNode(dateInput)); + TestUtils.Simulate.focus(ReactDOM.findDOMNode(dateInput)); TestUtils.Simulate.click( TestUtils.findRenderedDOMComponentWithClass( datePicker, @@ -1662,7 +1666,7 @@ describe("Calendar", () => { onKeyDown={onKeyDownSpy} />, ); - TestUtils.Simulate.focus(findDOMNode(datePicker.input)); + TestUtils.Simulate.focus(ReactDOM.findDOMNode(datePicker.input)); const prevMonthButton = TestUtils.findRenderedDOMComponentWithClass( datePicker, "react-datepicker__navigation--previous", @@ -1683,7 +1687,7 @@ describe("Calendar", () => { onKeyDown={onKeyDownSpy} />, ); - TestUtils.Simulate.focus(findDOMNode(datePicker.input)); + TestUtils.Simulate.focus(ReactDOM.findDOMNode(datePicker.input)); const nextMonthButton = TestUtils.findRenderedDOMComponentWithClass( datePicker, "react-datepicker__navigation--next", @@ -1733,7 +1737,7 @@ describe("Calendar", () => { ); const dateInput = datePicker.input; - TestUtils.Simulate.focus(findDOMNode(dateInput)); + TestUtils.Simulate.focus(ReactDOM.findDOMNode(dateInput)); const calendar = TestUtils.scryRenderedComponentsWithType( datePicker.calendar, @@ -1764,7 +1768,7 @@ describe("Calendar", () => { ); const dateInput = datePicker.input; - TestUtils.Simulate.focus(findDOMNode(dateInput)); + TestUtils.Simulate.focus(ReactDOM.findDOMNode(dateInput)); const calendar = TestUtils.scryRenderedComponentsWithType( datePicker.calendar, diff --git a/test/week_number_test.test.js b/test/week_number_test.test.js index 1f2a0037d..1cc943340 100644 --- a/test/week_number_test.test.js +++ b/test/week_number_test.test.js @@ -1,14 +1,16 @@ import React from "react"; import WeekNumber from "../src/week_number"; import { shallow } from "enzyme"; +import * as utils from "../src/date_utils"; function renderWeekNumber(weekNumber, props = {}) { return shallow(); } describe("WeekNumber", () => { - let shallowWeekNumber; - describe("rendering", () => { + let shallowWeekNumber, instance; + + describe("Rendering", () => { it("should render the specified Week Number", () => { const weekNumber = 1; shallowWeekNumber = renderWeekNumber(weekNumber); @@ -18,18 +20,18 @@ describe("WeekNumber", () => { expect(shallowWeekNumber.text()).toBe(weekNumber + ""); }); - it("should call the onClick function if it is defined", () => { - const onClick = jest.fn(); - shallowWeekNumber = shallow( - , + it("should handle onClick function", () => { + const onClickMock = jest.fn(); + const shallowWeekNumber = shallow( + , ); shallowWeekNumber.instance().handleClick({}); - expect(onClick).toHaveBeenCalledTimes(1); + expect(onClickMock).toHaveBeenCalledTimes(1); }); it("should have an aria-label containing the provided prefix", () => { const ariaLabelPrefix = "A prefix in my native language"; - const shallowWeekNumber = shallow( + shallowWeekNumber = shallow( , ); expect( @@ -37,4 +39,274 @@ describe("WeekNumber", () => { ).not.toBe(-1); }); }); + + describe("Component Lifecycle", () => { + const handleFocusWeekNumberMock = jest.fn(); + + beforeEach(() => { + shallowWeekNumber = shallow(); + instance = shallowWeekNumber.instance(); + instance.handleFocusWeekNumber = handleFocusWeekNumberMock; + }); + + afterEach(() => { + handleFocusWeekNumberMock.mockClear(); + }); + + it("should call handleFocusWeeknumber on mount", () => { + instance.componentDidMount(); + expect(handleFocusWeekNumberMock).toHaveBeenCalledTimes(1); + }); + + it("should call handleFocusWeekNumber with prevProps on update", () => { + const prevProps = { someProp: "someValue" }; + instance.componentDidUpdate(prevProps); + expect(handleFocusWeekNumberMock).toHaveBeenCalledWith(prevProps); + }); + }); + + describe("Event Handling", () => { + it("should call onClick prop when handleClick is triggered", () => { + const onClickMock = jest.fn(); + const eventMock = { target: {} }; + const shallowWeekNumber = shallow(); + shallowWeekNumber.simulate("click", eventMock); + expect(onClickMock).toHaveBeenCalledWith(eventMock); + }); + + describe("handleOnKeyDown", () => { + const handleOnKeyDownMock = jest.fn((event) => { + if (event.key === " ") { + event.preventDefault(); + event.key = "Enter"; + } + }); + + it("should change space key to Enter", () => { + const eventSpace = { + key: " ", + preventDefault: jest.fn(), + }; + handleOnKeyDownMock(eventSpace); + expect(eventSpace.preventDefault).toHaveBeenCalled(); + expect(eventSpace.key).toBe("Enter"); + }); + + it("should not change any other key", () => { + const eventA = { + key: "a", + }; + handleOnKeyDownMock(eventA); + expect(eventA.key).toBe("a"); + }); + }); + }); + + describe("Utility Functions", () => { + beforeEach(() => { + shallowWeekNumber = shallow(); + instance = shallowWeekNumber.instance(); + }); + + describe("getTabIndex", () => { + it("should return 0 if showWeekPicker and showWeekNumber are true and the day is selected", () => { + const shallowWeekNumber = shallow( + , + ); + const instance = shallowWeekNumber.instance(); + instance.isKeyboardSelected = jest.fn(() => true); + instance.isSameDay = jest.fn(() => true); + const props = { ...instance.props, preSelection: new Date() }; + instance.props = props; + expect(instance.getTabIndex()).toBe(0), + expect(instance.isSameDay()).toBe(true); + }); + + it("should return 0 if showWeekPicker and showWeekNumber are true and the day is the preSelection", () => { + const shallowWeekNumber = shallow( + , + ); + const instance = shallowWeekNumber.instance(); + instance.isKeyboardSelected = jest.fn(() => true); + instance.isSameDay = jest.fn(() => true); + const props = { ...instance.props, preSelection: new Date() }; + instance.props = props; + expect(instance.getTabIndex()).toBe(0); + }); + + it("should return -1 if showWeekPicker is false", () => { + const shallowWeekNumber = shallow( + , + ); + const instance = shallowWeekNumber.instance(); + expect(instance.getTabIndex()).toBe(-1); + }); + + it("should return -1 if showWeekNumber is false", () => { + const shallowWeekNumber = shallow( + , + ); + const instance = shallowWeekNumber.instance(); + expect(instance.getTabIndex()).toBe(-1); + }); + + it("should return -1 if the day is not selected or the preSelection", () => { + const shallowWeekNumber = shallow( + , + ); + const instance = shallowWeekNumber.instance(); + instance.isKeyboardSelected = jest.fn(() => false); + instance.isSameDay = jest.fn(() => false); + const props = { ...instance.props, preSelection: new Date() }; + instance.props = props; + expect(instance.getTabIndex()).toBe(-1); + }); + }); + + describe("weekNumberClasses should return the correct classes", () => { + it("should have the class 'react-datepicker__week-number'", () => { + const weekNumber = 1; + const shallowWeekNumber = shallow( + , + ); + expect( + shallowWeekNumber.hasClass("react-datepicker__week-number"), + ).toBe(true); + }); + + it("should have the class 'react-datepicker__week-number--clickable' if onClick is defined", () => { + const shallowWeekNumber = shallow( {}} />); + expect( + shallowWeekNumber.hasClass( + "react-datepicker__week-number--clickable", + ), + ).toBe(true); + }); + + it("should have the class 'react-datepicker__week-number--selected' if selected is current week and preselected is also current week", () => { + const currentWeekNumber = utils.newDate("2023-10-22T13:09:53+02:00"); + const shallowWeekNumber = shallow( + , + ); + expect( + shallowWeekNumber.hasClass("react-datepicker__week-number--selected"), + ).toBe(true); + }); + + it("should have the class 'react-datepicker__week-number--selected' if selected is current week and preselected is not current week", () => { + const currentWeekNumber = utils.newDate("2023-10-22T13:09:53+02:00"); + const preSelection = utils.addWeeks(currentWeekNumber, 1); + const shallowWeekNumber = shallow( + , + ); + expect( + shallowWeekNumber.hasClass("react-datepicker__week-number--selected"), + ).toBe(true); + }); + + it("should have the class 'react-datepicker__week-number--selected' if selected is not current week and preselected is current week", () => { + const currentWeekNumber = utils.newDate("2023-10-22T13:09:53+02:00"); + const selected = utils.addWeeks(currentWeekNumber, 1); + const shallowWeekNumber = shallow( + , + ); + expect( + shallowWeekNumber.hasClass("react-datepicker__week-number--selected"), + ).toBe(false); + expect( + shallowWeekNumber.hasClass( + "react-datepicker__week-number--keyboard-selected", + ), + ).toBe(true); + }); + + it("should have the class 'react-datepicker__week-number--selected' if selected is not current week and preselected is not current week", () => { + const currentWeekNumber = utils.newDate("2023-10-22T13:09:53+02:00"); + const selected = utils.addWeeks(currentWeekNumber, 1); + const preSelection = utils.addWeeks(currentWeekNumber, 2); + const shallowWeekNumber = shallow( + , + ); + expect( + shallowWeekNumber.hasClass("react-datepicker__week-number--selected"), + ).toBe(false); + expect( + shallowWeekNumber.hasClass( + "react-datepicker__week-number--keyboard-selected", + ), + ).toBe(false); + }); + }); + }); + + describe("handleFocusWeekNumber", () => { + let weekNumberEl, instance, shallowWeekNumber; + + const createComponentWithProps = (props = {}) => { + shallowWeekNumber = shallow(); + instance = shallowWeekNumber.instance(); + instance.weekNumberEl = weekNumberEl; + instance.getTabIndex = jest.fn(() => 0); + instance.isSameDay = jest.fn(() => true); + }; + + beforeEach(() => { + weekNumberEl = { current: { focus: jest.fn() } }; + createComponentWithProps(); + }); + + const setActiveElement = (element) => { + Object.defineProperty(document, "activeElement", { + value: element, + writable: false, + configurable: true, + }); + }; + + it("should focus if conditions are met", () => { + setActiveElement(document.body); + instance.handleFocusWeekNumber(); + expect(weekNumberEl.current.focus).toHaveBeenCalled(); + }); + + it("should not focus if input is focused", () => { + const inputElement = document.createElement("input"); + setActiveElement(inputElement); + instance.handleFocusWeekNumber({ isInputFocused: true }); + expect(weekNumberEl.current.focus).not.toHaveBeenCalled(); + }); + + it("should not focus if inline prop set and shouldFocusDayInline is false", () => { + createComponentWithProps({ inline: true, shouldFocusDayInline: false }); + instance.handleFocusWeekNumber(); + expect(weekNumberEl.current.focus).not.toHaveBeenCalled(); + }); + + it("should focus if active element is another instance of WeekNumber", () => { + const activeElement = document.createElement("div"); + activeElement.classList.add("react-datepicker__week-number"); + setActiveElement(activeElement); + const containerRef = { current: document.createElement("div") }; + createComponentWithProps({ containerRef }); + containerRef.current.appendChild(activeElement); + instance.handleFocusWeekNumber(); + expect(weekNumberEl.current.focus).toHaveBeenCalled(); + }); + }); }); diff --git a/test/week_picker_test.js b/test/week_picker_test.js new file mode 100644 index 000000000..937c6bd25 --- /dev/null +++ b/test/week_picker_test.js @@ -0,0 +1,46 @@ +import React from "react"; +import DatePicker from "../src/index.jsx"; +import Day from "../src/day"; +import WeekNumber from "../src/week_number"; + +describe("WeekPicker", () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should change the week when clicked on any option in the picker", () => { + const onChangeSpy = sinon.spy(); + const weekPicker = TestUtils.renderIntoDocument( + , + ); + expect(onChangeSpy.called).to.be.false; + TestUtils.Simulate.focus(weekPicker.inputRef.current); + const day = TestUtils.scryRenderedComponentsWithType( + weekPicker.calendar, + Day, + )[0]; + TestUtils.Simulate.click(day.ref); + expect(onChangeSpy.calledOnce).to.be.true; + }); + + it("should change the week when clicked on any week number in the picker", () => { + const onChangeSpy = sinon.spy(); + const weekPicker = TestUtils.renderIntoDocument( + , + ); + expect(onChangeSpy.called).to.be.false; + TestUtils.Simulate.focus(weekPicker.inputRef.current); + const weekNumber = TestUtils.scryRenderedComponentsWithType( + weekPicker.calendar, + WeekNumber, + )[0]; + TestUtils.Simulate.click(weekNumber.ref); + expect(onChangeSpy.calledOnce).to.be.true; + }); +}); diff --git a/test/week_test.test.js b/test/week_test.test.js index a071c7b4b..cde8f4e7b 100644 --- a/test/week_test.test.js +++ b/test/week_test.test.js @@ -176,4 +176,122 @@ describe("Week", () => { day.simulate("mouseenter"); expect(day.prop("day")).toEqual(dayMouseEntered); }); + + describe("handleWeekClick", () => { + it("should call onWeekSelect prop with correct arguments", () => { + const onWeekSelect = jest.fn(); + const day = new Date("2022-02-01"); + const weekNumber = 5; + const event = { target: {} }; + const wrapper = shallow( + {}} + />, + ); + wrapper.instance().handleWeekClick(day, weekNumber, event); + expect(onWeekSelect).toHaveBeenCalledWith(day, weekNumber, event); + }); + + it("should call handleDayClick with start of week if showWeekPicker prop is true", () => { + const handleDayClick = jest.fn(); + const day = new Date("2022-02-01"); + const weekNumber = 5; + const event = { target: {} }; + const wrapper = shallow( + {}} + showWeekPicker + shouldCloseOnSelect={false} + setOpen={() => {}} + />, + ); + wrapper.instance().handleDayClick = handleDayClick; + wrapper.instance().handleWeekClick(day, weekNumber, event); + const startOfWeek = utils.getStartOfWeek(day); + expect(handleDayClick).toHaveBeenCalledWith(startOfWeek, event); + }); + + it("should call setOpen prop with false if shouldCloseOnSelect prop is true", () => { + const setOpen = jest.fn(); + const day = new Date("2022-02-01"); + const weekNumber = 5; + const event = { target: {} }; + const wrapper = shallow( + {}} + showWeekPicker={false} + shouldCloseOnSelect + setOpen={setOpen} + />, + ); + wrapper.instance().handleWeekClick(day, weekNumber, event); + expect(setOpen).toHaveBeenCalledWith(false); + }); + }); + + describe("selected and keyboard-selected", () => { + it("selected is current week and preselected is also current week", () => { + const currentWeek = utils.newDate("2023-10-22T13:09:53+02:00"); + const shallowWeek = shallow( + , + ); + expect(shallowWeek.hasClass("react-datepicker__week--selected")).toBe( + true, + ); + }); + + it("selected is current week and preselected is not current week", () => { + const currentWeek = utils.newDate("2023-10-22T13:09:53+02:00"); + const preSelection = utils.addWeeks(currentWeek, 1); + const shallowWeek = shallow( + , + ); + expect(shallowWeek.hasClass("react-datepicker__week--selected")).toBe( + true, + ); + }); + + it("selected is not current week and preselect is current week", () => { + const currentWeek = utils.newDate("2023-10-22T13:09:53+02:00"); + const selected = utils.addWeeks(currentWeek, 1); + const shallowWeek = shallow( + , + ); + expect(shallowWeek.hasClass("react-datepicker__week--selected")).toBe( + false, + ); + expect( + shallowWeek.hasClass("react-datepicker__week--keyboard-selected"), + ).toBe(true); + }); + + it("select is not current week and preselect is not current week", () => { + const currentWeek = utils.newDate("2023-10-22T13:09:53+02:00"); + const selected = utils.addWeeks(currentWeek, 1); + const shallowWeek = shallow( + , + ); + expect(shallowWeek.hasClass("react-datepicker__week--selected")).toBe( + false, + ); + expect( + shallowWeek.hasClass("react-datepicker__week--keyboard-selected"), + ).toBe(false); + }); + }); }); diff --git a/test/year_picker_test.test.js b/test/year_picker_test.test.js index 16b1e2f25..c29bfb05e 100644 --- a/test/year_picker_test.test.js +++ b/test/year_picker_test.test.js @@ -3,9 +3,9 @@ import { mount } from "enzyme"; import DatePicker from "../src/index.jsx"; import Year from "../src/year.jsx"; import TestUtils from "react-dom/test-utils"; +import ReactDOM from "react-dom"; import * as utils from "../src/date_utils.js"; import Calendar from "../src/calendar.jsx"; -import { findDOMNode } from "react-dom"; describe("YearPicker", () => { it("should show year picker component when showYearPicker prop is present", () => { @@ -441,7 +441,7 @@ describe("YearPicker", () => { }} />, ); - TestUtils.Simulate.focus(findDOMNode(datePicker.input)); + TestUtils.Simulate.focus(ReactDOM.findDOMNode(datePicker.input)); const calendar = TestUtils.scryRenderedComponentsWithType( datePicker.calendar, Calendar, @@ -480,7 +480,7 @@ describe("YearPicker", () => { }} />, ); - TestUtils.Simulate.focus(findDOMNode(datePicker.input)); + TestUtils.Simulate.focus(ReactDOM.findDOMNode(datePicker.input)); const calendar = TestUtils.scryRenderedComponentsWithType( datePicker.calendar, Calendar,