diff --git a/src/components/date-picker/calendar.tsx b/src/components/date-picker/calendar.tsx index 5998f6494..35cddd306 100644 --- a/src/components/date-picker/calendar.tsx +++ b/src/components/date-picker/calendar.tsx @@ -1,4 +1,4 @@ -import {computed} from 'mobx'; +import {computed, observable} from 'mobx'; import {observer} from 'mobx-react'; import * as React from 'react'; import {stylable} from 'wix-react-tools'; @@ -29,15 +29,19 @@ export interface CalendarProps { } export interface CalendarState { - showMonthView: boolean; + view: 'year' | 'month' | 'default'; + viewDate: Date; } const monthNames = getMonthNames(); @stylable(styles) @observer -export class Calendar extends React.Component { - public state: CalendarState = {showMonthView: false}; +export class Calendar extends React.Component { + @observable private calendarState: CalendarState = { + view: 'default', + viewDate: this.props.value + }; public render() { return ( @@ -55,7 +59,7 @@ export class Calendar extends React.Component { {this.getHeader()} @@ -67,28 +71,24 @@ export class Calendar extends React.Component { - {this.state.showMonthView ? -
- {this.monthArray} -
- : -
- {this.dayNames} - {this.previousDays} - {this.days} - {this.followingDays} -
- } + {this.getCalendarView()} ); } private getHeader = () => { - if (this.state.showMonthView) { + if (this.calendarState.view === 'year') { + const decade = `${this.calendarState.viewDate.getFullYear() - 5}` + + `-${this.calendarState.viewDate.getFullYear() + 4}`; return ( - {this.year} + {decade} + ); + } else if (this.calendarState.view === 'month') { + return ( + + {this.calendarState.viewDate.getFullYear()} ); } else { return ( @@ -98,6 +98,31 @@ export class Calendar extends React.Component { } } + private getCalendarView = () => { + if (this.calendarState.view === 'year') { + return ( +
+ {this.yearArray} +
+ ); + } else if (this.calendarState.view === 'month') { + return ( +
+ {this.monthArray} +
+ ); + } else { + return ( +
+ {this.dayNames} + {this.previousDays} + {this.days} + {this.followingDays} +
+ ); + } + } + @computed private get monthName(): string { return monthNames[this.props.value.getMonth()]; @@ -201,19 +226,40 @@ export class Calendar extends React.Component { monthNames.forEach(month => { monthArray.push( {month} - ); + ); }); return monthArray; } + @computed + private get yearArray(): JSX.Element[] { + const yearArray: JSX.Element[] = []; + + for (let year = this.calendarState.viewDate.getFullYear() - 5; + year <= this.calendarState.viewDate.getFullYear() + 4; year ++) { + yearArray.push( + + {year} + + ); + } + + return yearArray; + } + private isCurrentDay(day: number): boolean { const currentDate = new Date(); return (this.props.value.getFullYear() === currentDate.getFullYear() @@ -240,41 +286,79 @@ export class Calendar extends React.Component { } } - private toggleMonthView = () => { - this.setState({showMonthView: !this.state.showMonthView}); + private toggleDateSelectView = () => { + if (this.calendarState.view === 'year') { + this.calendarState.view = 'default'; + this.resetViewDate(); + } else if (this.calendarState.view === 'default') { + this.calendarState.view = 'month'; + } else if (this.calendarState.view === 'month') { + this.calendarState.view = 'year'; + } } - private onSelectMonth: React.EventHandler> = event => { - event.preventDefault(); - this.toggleMonthView(); + private closeDateSelectView = () => { + if (this.calendarState.view === 'year') { + this.calendarState.view = 'month'; + } else if (this.calendarState.view === 'month') { + this.calendarState.view = 'default'; + } + } - const date = new Date(this.props.value.getFullYear(), + private resetViewDate = () => { + this.calendarState.viewDate = this.props.value; + } + + private onSelectMonth: React.EventHandler> = event => { + const date = new Date(this.calendarState.viewDate.getFullYear(), monthNames.indexOf((event.target as HTMLSpanElement).textContent!), - this.props.value.getDate()); + this.calendarState.viewDate.getDate()); + + this.props.updateDropdownDate(date); + this.calendarState.viewDate = date; + this.closeDateSelectView(); + } + + private onSelectYear: React.EventHandler> = event => { + const date = new Date(parseInt((event.target as HTMLSpanElement).textContent!, 10), + this.calendarState.viewDate.getMonth(), + this.calendarState.viewDate.getDate()); this.props.updateDropdownDate(date); + this.calendarState.viewDate = date; + this.closeDateSelectView(); } private goToNextMonth: React.EventHandler> = event => { event.preventDefault(); - const nextDate: Date = this.state.showMonthView - ? new Date(this.props.value.getFullYear() + 1, this.props.value.getMonth(), 1) - : getMonthFromOffset(new Date(this.props.value.getFullYear(), this.props.value.getMonth(), 1), 1); - this.props.updateDropdownDate(nextDate); + if (this.calendarState.view === 'default') { + const nextDate: Date = getMonthFromOffset( + new Date(this.props.value.getFullYear(), this.props.value.getMonth(), 1), 1); + this.props.updateDropdownDate(nextDate); + } else { + this.calendarState.viewDate = this.calendarState.view === 'year' + ? new Date(this.calendarState.viewDate.getFullYear() + 10, this.calendarState.viewDate.getMonth(), 1) + : new Date(this.calendarState.viewDate.getFullYear() + 1, this.calendarState.viewDate.getMonth(), 1); + } } private goToPrevMonth: React.EventHandler> = event => { event.preventDefault(); - const nextDate: Date = this.state.showMonthView - ? new Date(this.props.value.getFullYear() - 1, this.props.value.getMonth(), 1) - : getMonthFromOffset(new Date(this.props.value.getFullYear(), this.props.value.getMonth(), 1), -1); - this.props.updateDropdownDate(nextDate); + if (this.calendarState.view === 'default') { + const nextDate: Date = getMonthFromOffset( + new Date(this.props.value.getFullYear(), this.props.value.getMonth(), 1), -1); + this.props.updateDropdownDate(nextDate); + } else { + this.calendarState.viewDate = this.calendarState.view === 'year' + ? new Date(this.calendarState.viewDate.getFullYear() - 10, this.calendarState.viewDate.getMonth(), 1) + : new Date(this.calendarState.viewDate.getFullYear() - 1, this.calendarState.viewDate.getMonth(), 1); + } } - private headerClicked: React.EventHandler> = event => { + private onHeaderClick: React.EventHandler> = event => { event.preventDefault(); - this.toggleMonthView(); + this.toggleDateSelectView(); } } diff --git a/src/components/date-picker/date-picker.st.css b/src/components/date-picker/date-picker.st.css index cdda2a891..da54df45c 100644 --- a/src/components/date-picker/date-picker.st.css +++ b/src/components/date-picker/date-picker.st.css @@ -165,6 +165,10 @@ -st-extends: calendar; } +.year-view { + -st-extends: calendar; +} + /* Styling for the days of the week */ .dayName { font-family: value(fontFamily); @@ -196,6 +200,24 @@ background-color: value(color_Keyboard_Focused); } +.year { + /* 100/2 = 50... show 2 years per row */ + flex: 0 0 50%; + line-height: 54px; + font-family: value(fontFamily); + font-size: 14px; + font-weight: bold; + text-align: center; + color: value(color_MainText); + cursor: pointer; +} + +/* Hover styles for year selection */ +.year:hover { + color: value(color_MainText); + background-color: value(color_Keyboard_Focused); +} + /* Styling for the days of the month */ .day { -st-states: focused, selected, current, inactive, disabled; diff --git a/test-kit/components/date-picker-driver.ts b/test-kit/components/date-picker-driver.ts index 9063fe2c9..024cc8570 100644 --- a/test-kit/components/date-picker-driver.ts +++ b/test-kit/components/date-picker-driver.ts @@ -51,6 +51,10 @@ export class DatePickerTestDriver extends DriverBase { simulate.mouseDown(this.getMonth(month)); } + public clickOnYear(year: string): void { + simulate.mouseDown(this.getYear(year)); + } + public openCalender(): void { simulate.click(this.select('CALENDAR_ICON')); } @@ -87,6 +91,10 @@ export class DatePickerTestDriver extends DriverBase { return bodySelect('MONTH_VIEW'); } + public get yearView(): HTMLDivElement | null { + return bodySelect('YEAR_VIEW'); + } + public getDay(day: number | string): HTMLSpanElement | null { return bodySelect(datePickerDropdown, `DAY_${day}`); } @@ -115,7 +123,11 @@ export class DatePickerTestDriver extends DriverBase { return bodySelect(datePickerDropdown, `MONTH_${month.toUpperCase()}`); } - public elementHasStylableState(element: Element, stateName: string): boolean { + public getYear(year: string): HTMLSpanElement | null { + return bodySelect(datePickerDropdown, `YEAR_${year}`); + } + + public hasStylableState(element: Element, stateName: string): boolean { return elementHasStylableState(element, baseStyle, stateName); } } diff --git a/test/components/date-picker.spec.tsx b/test/components/date-picker.spec.tsx index 860889bad..2a2cabe8c 100644 --- a/test/components/date-picker.spec.tsx +++ b/test/components/date-picker.spec.tsx @@ -83,7 +83,7 @@ describe('The DatePicker Component', () => { datePickerDemo.datePicker.clickOnMonth(monthToClick); await waitForDom(() => { - expect(datePickerDemo.datePicker.monthView).to.be.absent(); + expect(datePickerDemo.datePicker.monthView, 'expected month view to be absent').to.be.absent(); expect(datePickerDemo.datePicker.isOpen()).to.equal(true); expect(datePickerDemo.datePicker.headerDate).to.have.text(`${monthToClick} 2017`); }); @@ -231,7 +231,7 @@ describe('The DatePicker Component', () => { datePicker.changeDate('2sgsdfsdfw223'); - await waitForDom(() => expect(datePicker.elementHasStylableState(datePicker.root, 'error')).to.equal(true)); + await waitForDom(() => expect(datePicker.hasStylableState(datePicker.root, 'error')).to.equal(true)); }); it('should remove error state when a date is chosen from the calendar', async () => { @@ -240,13 +240,13 @@ describe('The DatePicker Component', () => { datePicker.changeDate('2sgsdfsdfw223'); - await waitForDom(() => expect(datePicker.elementHasStylableState(datePicker.root, 'error')).to.equal(true)); + await waitForDom(() => expect(datePicker.hasStylableState(datePicker.root, 'error')).to.equal(true)); datePicker.openCalender(); await waitForDom(() => datePicker.clickOnDay(2)); - await waitForDom(() => expect(datePicker.elementHasStylableState(datePicker.root, 'error')) + await waitForDom(() => expect(datePicker.hasStylableState(datePicker.root, 'error')) .to.equal(false)); }); }); @@ -302,7 +302,7 @@ describe('The DatePicker Component', () => { const {driver: datePicker, waitForDom} = clientRenderer.render() .withDriver(DatePickerTestDriver); - await waitForDom(() => expect(datePicker.elementHasStylableState(datePicker.root, 'disabled')) + await waitForDom(() => expect(datePicker.hasStylableState(datePicker.root, 'disabled')) .to.equal(true)); }); }); @@ -358,7 +358,7 @@ describe('The DatePicker Component', () => { const {driver: datePicker, waitForDom} = clientRenderer.render() .withDriver(DatePickerTestDriver); - await waitForDom(() => expect(datePicker.elementHasStylableState(datePicker.root, 'readOnly')) + await waitForDom(() => expect(datePicker.hasStylableState(datePicker.root, 'readOnly')) .to.equal(true)); }); }); @@ -613,6 +613,106 @@ describe('The DatePicker Component', () => { }); }); + it('when in the month-view, clicking on the year should bring up the year-view' + + ' and the header date should show a decade', async () => { + const {driver: datePicker, waitForDom} = clientRenderer.render( + + ).withDriver(DatePickerTestDriver); + + datePicker.clickOnHeader(); + + await waitForDom(() => { + expect(datePicker.monthView).to.be.present(); + expect(datePicker.headerDate).to.have.text('2017'); + }); + + datePicker.clickOnHeader(); + + await waitForDom(() => { + expect(datePicker.monthView).to.be.absent(); + expect(datePicker.yearView).to.be.present(); + expect(datePicker.headerDate).to.have.text('2012-2021'); + }); + }); + + it('when in the month-view, clicking on the arrows should not change the actual date', async () => { + const {driver: datePicker, waitForDom} = clientRenderer.render( + + ).withDriver(DatePickerTestDriver); + + datePicker.clickOnHeader(); + + await waitForDom(() => { + expect(datePicker.monthView).to.be.present(); + expect(datePicker.headerDate).to.have.text('2017'); + }); + + datePicker.clickOnPrevMonth(); + datePicker.clickOnPrevMonth(); + datePicker.clickOnHeader(); + datePicker.clickOnHeader(); + + await waitForDom(() => { + expect(datePicker.monthView).to.be.absent(); + expect(datePicker.yearView).to.be.absent(); + expect(datePicker.headerDate).to.have.text('January 2017'); + }); + }); + + it('when in the year-view, clicking on the arrows should change the decade', async () => { + const {driver: datePicker, waitForDom} = clientRenderer.render( + + ).withDriver(DatePickerTestDriver); + + datePicker.clickOnHeader(); + datePicker.clickOnHeader(); + + await waitForDom(() => { + expect(datePicker.yearView).to.be.present(); + expect(datePicker.headerDate).to.have.text('2012-2021'); + }); + + datePicker.clickOnNextMonth(); + + await waitForDom(() => { + expect(datePicker.yearView).to.be.present(); + expect(datePicker.headerDate).to.have.text('2022-2031'); + }); + + datePicker.clickOnPrevMonth(); + datePicker.clickOnPrevMonth(); + + await waitForDom(() => { + expect(datePicker.yearView).to.be.present(); + expect(datePicker.headerDate).to.have.text('2002-2011'); + }); + }); + + it('when in the year-view, clicking on a year should change the current year' + + ' to the selected year, and then hide the year-view, showing the month-view', async () => { + const yearToClick = '2019'; + const {driver: datePicker, waitForDom} = clientRenderer.render( + + ).withDriver(DatePickerTestDriver); + + datePicker.clickOnHeader(); + datePicker.clickOnHeader(); + + await waitForDom(() => { + expect(datePicker.yearView).to.be.present(); + expect(datePicker.headerDate).to.have.text('2012-2021'); + }); + + datePicker.clickOnYear(yearToClick); + + await waitForDom(() => { + expect(datePicker.yearView).to.be.absent(); + expect(datePicker.monthView).to.be.present(); + expect(datePicker.isOpen()).to.equal(true); + expect(datePicker.headerDate).to.have.text(`${yearToClick}`); + }); + }); + it('should allow disabling weekends', async () => { const {driver: datePicker, waitForDom} = clientRenderer.render( { await waitForDom(() => { // Check a weekend to ensure the days are disabled - expect(datePicker.elementHasStylableState(datePicker.getDay(4) as Element, 'disabled')).to.be.true; + expect(datePicker.hasStylableState(datePicker.getDay(4) as Element, 'disabled')).to.be.true; }); datePicker.clickOnDay(4);