UNPKG

terra-date-picker

Version:

The terra-date-picker component provides users a way to enter or select a date from the date picker.

748 lines (694 loc) 22.8 kB
import { FormattedMessage } from 'react-intl'; import * as KeyCode from 'keycode-js'; import YearDropdown from './year_dropdown' import MonthDropdown from './month_dropdown' import Month from './month' import React from 'react' import PropTypes from 'prop-types' import classNames from 'classnames/bind' import styles from './stylesheets/react_datepicker.module.scss' import { now, setMonth, getMonth, addMonths, subtractMonths, getStartOfWeek, getStartOfDate, addDays, cloneDate, formatDate, localizeDate, setYear, getYear, isBefore, isAfter, getLocaleData, getWeekdayShortInLocale, getWeekdayMinInLocale, getStartOfMonth, isSameDay, allDaysDisabledBefore, allDaysDisabledAfter, getEffectiveMinDate, getEffectiveMaxDate, isDayDisabled, dateValues } from './date_utils' const cx = classNames.bind(styles); const DROPDOWN_FOCUS_CLASSNAMES = [ 'react-datepicker-year-select', 'react-datepicker-month-select' ] const isDropdownSelect = (element = {}) => { const classNamesList = (element.className || '').split(/\s+/) return DROPDOWN_FOCUS_CLASSNAMES.some(testClassname => classNamesList.indexOf(testClassname) >= 0) } export default class Calendar extends React.Component { static propTypes = { /** * Prop to change date when a valid date is selected. */ adjustDateOnChange: PropTypes.bool, /** * Class name to style the date picker. */ className: PropTypes.string, /** * Components to render within date picker. */ children: PropTypes.node, /** * Format of the date selected. */ dateFormat: PropTypes.oneOfType([ PropTypes.string, PropTypes.array ]).isRequired, /** * Prop to style individual days on calendar. */ dayClassName: PropTypes.func, /** * Whether the year and month dropdowns should be in the scroll or select mode. */ dropdownMode: PropTypes.oneOf(['scroll', 'select']).isRequired, /** * Maximum Date for a given range. */ endDate: PropTypes.object, /** * Array to exclude certain dates. */ excludeDates: PropTypes.array, /** * A callback function to be executed to determine if a given date should be filtered. */ filterDate: PropTypes.func, /** * Specifies whether the height of calendar dom fixed or variable. */ fixedHeight: PropTypes.bool, /** * A callback function to be executed to format week number . */ formatWeekNumber: PropTypes.func, /** * Highlight range of dates with custom classes. */ highlightDates: PropTypes.instanceOf(WeakMap), /** * Show dates only in the given array. */ includeDates: PropTypes.array, /** * Timezone value to indicate in which timezone the date-time component is rendered. * The value provided should be a valid [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) string, else will default to browser/local timezone. */ initialTimeZone: PropTypes.string, /** * Prop to show inline version of date picker component. */ inline: PropTypes.bool, /** * @private * Internationalization object with translation APIs. Provided by `injectIntl`. */ intl: PropTypes.shape({ formatMessage: PropTypes.func }), /** * Name of locale data for different international formatting. */ locale: PropTypes.string, /** * Maximum value of date that can be selected by user. */ maxDate: PropTypes.object, /** * Minimum value of date that can be selected by user. */ minDate: PropTypes.object, /** * Prop to show multiple months on date picker. */ monthsShown: PropTypes.number, /** * A callback function that is executed when user clicks outside the datepicker. */ onClickOutside: PropTypes.func, /** * A callback function that is executed when month is selected. */ onMonthChange: PropTypes.func, /** * A callback function to execute when month component loses focus. * requires no parameter. */ onMonthBlur: PropTypes.func, /** * Prop to show month navigation. */ forceShowMonthNavigation: PropTypes.bool, /** * A callback function that is executed when date picker is clicked for dropdown. */ onDropdownFocus: PropTypes.func, /** * A callback function to execute on mouse down on day. * requires no parameter. */ onDayMouseDown: PropTypes.func, /** * A callback function that is executed when a valid date is selected. */ onSelect: PropTypes.func.isRequired, /** * A callback function that is executed when a Week number is selected. */ onWeekSelect: PropTypes.func, /** * Prop to open calendar on the given date. */ openToDate: PropTypes.object, /** * Prop to show dates of next month also. */ peekNextMonth: PropTypes.bool, /** * Prop to show a scrollable dropdown to choose year on the date picker. */ scrollableYearDropdown: PropTypes.bool, /** * Prop to store previous selection. */ preSelection: PropTypes.object, /** * A callback function used to set preSelection date when the calendar month or year is updated */ setPreSelection: PropTypes.func, /** * Selected Date Value. */ selected: PropTypes.object, /** * Mark date picker to select end of range . */ selectsEnd: PropTypes.bool, /** * Mark date picker to select start of range . */ selectsStart: PropTypes.bool, /** * Prop to show a dropdown to select month in date picker . */ showMonthDropdown: PropTypes.bool, /** * Prop to show week numbers . */ showWeekNumbers: PropTypes.bool, /** * Prop to show a dropdown to select year in date picker . */ showYearDropdown: PropTypes.bool, /** * Minimum date for a given range . */ startDate: PropTypes.object, /** * Name of button to select current date . */ todayButton: PropTypes.string, /** * Prop to show short names of weekdays . */ useWeekdaysShort: PropTypes.bool, /** * Label value for weeks on date picker. */ weekLabel: PropTypes.string, /** * Year Values to show on dropdown +/- the given value. */ yearDropdownItemNumber: PropTypes.number, /** * A callback function to execute when a date picker is open. */ setOpen: PropTypes.func, /** * Whether or not calendar is navigated by keyboard */ isCalendarKeyboardFocused: PropTypes.bool, /** * Whether or not calendar is opened by keyboard */ isCalendarOpenedViaKeyboard: PropTypes.bool, } static get defaultProps () { return { onDropdownFocus: () => {}, monthsShown: 1, forceShowMonthNavigation: false, isCalendarKeyboardFocused: false } } constructor (props) { super(props) this.state = { isMonthChanged: false, date: this.localizeDate(this.getDateInView()), selectingDate: null, calendarIsKeyboardFocused: this.props.isCalendarKeyboardFocused, } this.todayBtnRef = React.createRef(); this.closeBtnRef = React.createRef(); this.previousMonthBtnRef = React.createRef(); this.nextMonthBtnRef = React.createRef(); this.monthRef; this.monthDropdownRef; this.yearDropdownRef; this.handleDropdownClick = this.handleDropdownClick.bind(this); } componentDidUpdate (prevProps) { if (this.props.preSelection && !isSameDay(this.props.preSelection, prevProps.preSelection)) { this.setState({ date: this.localizeDate(this.props.preSelection) }) } else if (this.props.openToDate && !isSameDay(this.props.openToDate, prevProps.openToDate)) { this.setState({ date: this.localizeDate(this.props.openToDate) }) } } handleDropdownClick(event){ if(event.keyCode === KeyCode.KEY_UP || event.keyCode === KeyCode.KEY_DOWN ) { this.setState({ calendarIsKeyboardFocused : true}); } else { this.setState({ calendarIsKeyboardFocused : false}); } } handleOnClick = (event) => { const calendarControls = [ this.todayBtnRef.current, this.closeBtnRef.current, this.previousMonthBtnRef.current, this.nextMonthBtnRef.current, this.monthDropdownRef, this.yearDropdownRef, ]; const isEventTargetMatchingCalendarControl = (target) => { return calendarControls.indexOf(target) >= 0; } const isEventTargetContainedWithinCalendarControl = (target) => { const containsEventTarget = (this.previousMonthBtnRef.current && this.previousMonthBtnRef.current.contains(target) || this.nextMonthBtnRef.current && this.nextMonthBtnRef.current.contains(target) || this.monthDropdownRef && this.monthDropdownRef.contains(target) || this.yearDropdownRef && this.yearDropdownRef.contains(target)); return containsEventTarget; } if (isEventTargetMatchingCalendarControl(event.target) || isEventTargetContainedWithinCalendarControl(event.target)) { return; } // If the user is not clicking on a calendar control, shift focus to the calendar if (this.monthRef) { this.monthRef.focus(); } } handleClickOutside = (event) => { this.props.onClickOutside(event) } handleDropdownFocus = (event) => { if (isDropdownSelect(event.target)) { this.props.onDropdownFocus() } } handleCloseButtonClick = (event) => { if (this.props.onRequestClose) { this.props.onRequestClose(event) } } handlePreviousMonthBtnKeyDown = (event) => { if (event.shiftKey && event.keyCode === KeyCode.KEY_TAB) { this.setState({ calendarIsKeyboardFocused: true }) } } handleNextMonthBtnKeyDown = (event) => { if (event.keyCode === KeyCode.KEY_RETURN) { this.setState({ calendarIsKeyboardFocused: true }) } } handleTodayBtnKeyDown = (event) => { if (event.keyCode === KeyCode.KEY_TAB) { this.setState({ calendarIsKeyboardFocused: true }) } } handleMonthBlur = () => { this.setState({ calendarIsKeyboardFocused: false }) if (this.props.onMonthBlur) { this.props.onMonthBlur(); } } handleMonthFocus = () => { if (this.props.inline) { this.setState({ calendarIsKeyboardFocused: true }) } } setMonthRef = (node) => { this.monthRef = node; } setMonthDropdownRef = (node) => { this.monthDropdownRef = node; } setYearDropdownRef = (node) => { this.yearDropdownRef = node; } getDateInView = () => { const { preSelection, selected, openToDate, initialTimeZone } = this.props const minDate = getEffectiveMinDate(this.props) const maxDate = getEffectiveMaxDate(this.props) const current = now(initialTimeZone) const initialDate = openToDate || selected || preSelection if (initialDate) { return initialDate } else { if (minDate && isBefore(current, minDate)) { return minDate } else if (maxDate && isAfter(current, maxDate)) { return maxDate } } return current } localizeDate = date => localizeDate(date, this.props.locale) increaseMonth = (event) => { this.nextMonthBtnRef.current.focus(); // To apply focus style in firefox this.setState({ isMonthChanged: true, date: getStartOfMonth(addMonths(cloneDate(this.state.date), 1)), }, () => this.handleMonthChange(this.state.date)) this.props.setPreSelection(getStartOfMonth(addMonths(cloneDate(this.state.date), 1)),dateValues.MONTH,addMonths(cloneDate(this.state.date), 1)); // To check if button is pressed using mouse or keyboard if(event.target.type === undefined) { this.setState({ calendarIsKeyboardFocused : false}); } } decreaseMonth = (event) => { this.previousMonthBtnRef.current.focus(); // To apply focus style in firefox this.setState({ isMonthChanged: true, date: getStartOfMonth(subtractMonths(cloneDate(this.state.date), 1)) }, () => this.handleMonthChange(this.state.date)) this.props.setPreSelection(getStartOfMonth(subtractMonths(cloneDate(this.state.date), 1)),dateValues.MONTH,subtractMonths(cloneDate(this.state.date), 1)); // To check if button is pressed using mouse or keyboard if(event.target.type === undefined) { this.setState({ calendarIsKeyboardFocused : false}); } } handleDayClick = (day, event) => this.props.onSelect(day, event) handleDayMouseEnter = day => this.setState({ selectingDate: day }) handleDayMouseDown = () => { if (this.props.onDayMouseDown) { this.props.onDayMouseDown(); } } handleMonthMouseLeave = () => this.setState({ selectingDate: null }) handleMonthChange = (date) => { if (this.props.onMonthChange) { this.props.onMonthChange(date) } if (this.props.adjustDateOnChange) { if (this.props.onSelect) { this.props.onSelect(date) } if (this.props.setOpen) { this.props.setOpen(true) } } } changeYear = (year) => { this.setState({ isMonthChanged: true, date: getStartOfMonth(setYear(cloneDate(this.state.date), year)) }) } changeMonth = (month) => { this.setState({ isMonthChanged: true, date: getStartOfMonth(setMonth(cloneDate(this.state.date), month)) }, () => this.handleMonthChange(this.state.date)) } header = (date = this.state.date) => { const startOfWeek = getStartOfWeek(cloneDate(date)) const dayNames = [] if (this.props.showWeekNumbers) { dayNames.push( <div key="W" className={cx('react-datepicker-day-name')}> {this.props.weekLabel || '#'} </div> ) } return dayNames.concat([0, 1, 2, 3, 4, 5, 6].map(offset => { const day = addDays(cloneDate(startOfWeek), offset) const localeData = getLocaleData(day) const weekDayName = this.props.useWeekdaysShort ? getWeekdayShortInLocale(localeData, day) : getWeekdayMinInLocale(localeData, day) return ( <div key={offset} className={cx('react-datepicker-day-name')}> {weekDayName} </div> ) })) } renderPreviousMonthButton = () => { if (!this.props.forceShowMonthNavigation && allDaysDisabledBefore(this.state.date, 'month', this.props)) { return (<div></div>) } return ( <FormattedMessage id="Terra.datePicker.previousMonth"> {text => ( <button type="button" className={cx('react-datepicker-navigation--previous')} aria-label={text} onClick={this.decreaseMonth} onKeyDown={this.handlePreviousMonthBtnKeyDown} ref={this.previousMonthBtnRef} > <span data-navigation-previous className={cx('prev-month-icon')} /> </button> )} </FormattedMessage> ) } renderNextMonthButton = () => { if (!this.props.forceShowMonthNavigation && allDaysDisabledAfter(this.state.date, 'month', this.props)) { return (<div></div>) } return ( <FormattedMessage id="Terra.datePicker.nextMonth"> {text => ( <button type="button" className={cx('react-datepicker-navigation--next')} aria-label={text} onClick={this.increaseMonth} onKeyDown={this.handleNextMonthBtnKeyDown} ref={this.nextMonthBtnRef} > <span data-navigation-next className={cx('next-month-icon')} /> </button> )} </FormattedMessage> ) } renderCurrentMonth = (date = this.state.date) => { const classes = ['react-datepicker-current-month'] if (this.props.showYearDropdown) { classes.push('react-datepicker-current-month--hasYearDropdown') } if (this.props.showMonthDropdown) { classes.push('react-datepicker-current-month--hasMonthDropdown') } return ( <div className={cx(classes)}> {formatDate(date, this.props.dateFormat)} </div> ) } renderYearDropdown = (overrideHide = false) => { if (!this.props.showYearDropdown || overrideHide) { return } return ( <YearDropdown adjustDateOnChange={this.props.adjustDateOnChange} date={this.state.date} onSelect={this.props.onSelect} setOpen={this.props.setOpen} dropdownMode={this.props.dropdownMode} onChange={this.changeYear} minDate={this.props.minDate} maxDate={this.props.maxDate} refCallback={this.setYearDropdownRef} year={getYear(this.state.date)} scrollableYearDropdown={this.props.scrollableYearDropdown} yearDropdownItemNumber={this.props.yearDropdownItemNumber} onClick={this.handleDropdownClick} onKeyDown={this.handleDropdownClick} /> ) } renderMonthDropdown = (overrideHide = false) => { if (!this.props.showMonthDropdown) { return } return ( <MonthDropdown dropdownMode={this.props.dropdownMode} locale={this.props.locale} dateFormat={this.props.dateFormat} onChange={this.changeMonth} month={getMonth(this.state.date)} refCallback={this.setMonthDropdownRef} onClick={this.handleDropdownClick} onKeyDown={this.handleDropdownClick} /> ) } renderTodayButton = () => { if (!this.props.todayButton) { return } const today = getStartOfDate(now(this.props.initialTimeZone)); return ( <button className={cx('react-datepicker-today-button')} onClick={e => this.props.onSelect(today, e)} onKeyDown={this.handleTodayBtnKeyDown} ref={this.todayBtnRef} disabled={isDayDisabled(today, this.props)} > {this.props.todayButton} </button> ) } renderCloseButton = () => { return ( <FormattedMessage id="Terra.datePicker.closeCalendar"> {text => ( <button className={cx('react-datepicker-close-button')} type="button" onClick={this.handleCloseButtonClick} ref={this.closeBtnRef} > {text} </button> )} </FormattedMessage> ) } renderMonths = () => { let keyboardFocus= false; if(this.props.isCalendarOpenedViaKeyboard || this.props.isCalendarKeyboardFocused) { keyboardFocus = true; } if(this.state.isMonthChanged) { keyboardFocus= this.state.calendarIsKeyboardFocused; } if(!keyboardFocus && this.state.calendarIsKeyboardFocused) { keyboardFocus = true; } var monthList = [] for (var i = 0; i < this.props.monthsShown; ++i) { var monthDate = addMonths(cloneDate(this.state.date), i) var monthKey = `month-${i}` monthList.push( <div key={monthKey} onClick={this.handleOnClick} className={cx('react-datepicker-month-container')}> <Month day={monthDate} isCalendarKeyboardFocused={keyboardFocus} dayClassName={this.props.dayClassName} onMonthFocus={this.handleMonthFocus} onMonthBlur={this.handleMonthBlur} onDayClick={this.handleDayClick} onDayMouseEnter={this.handleDayMouseEnter} onDayMouseDown={this.handleDayMouseDown} onMouseLeave={this.handleMonthMouseLeave} onWeekSelect={this.props.onWeekSelect} formatWeekNumber={this.props.formatWeekNumber} minDate={this.props.minDate} maxDate={this.props.maxDate} excludeDates={this.props.excludeDates} highlightDates={this.props.highlightDates} selectingDate={this.state.selectingDate} includeDates={this.props.includeDates} inline={this.props.inline} fixedHeight={this.props.fixedHeight} filterDate={this.props.filterDate} preSelection={this.props.preSelection} refCallback={this.setMonthRef} selected={this.props.selected} selectsStart={this.props.selectsStart} selectsEnd={this.props.selectsEnd} showWeekNumbers={this.props.showWeekNumbers} startDate={this.props.startDate} endDate={this.props.endDate} peekNextMonth={this.props.peekNextMonth} handleCalendarKeyDown={this.props.handleCalendarKeyDown} locale={this.props.locale} intl={this.props.intl} initialTimeZone={this.props.initialTimeZone} /> <div className={cx('react-datepicker-header')}> {this.renderCurrentMonth(monthDate)} <div className={cx('react-datepicker-header-controls')}> {this.renderPreviousMonthButton()} <div className={cx(['react-datepicker-header-dropdown', `react-datepicker-header-dropdown--${this.props.dropdownMode}`])} onFocus={this.handleDropdownFocus} > {this.renderMonthDropdown(i !== 0)} {this.renderYearDropdown(i !== 0)} </div> {this.renderNextMonthButton()} </div> <div className={cx('react-datepicker-day-names')} aria-hidden="true"> {this.header(monthDate)} </div> </div> </div> ) } return monthList } render () { const supportsOnTouchStart = 'ontouchstart' in window; /** * Ensures focus moves into datepicker popup correctly when it is opened on touch devices * by making focusable element (today button) first in the DOM order */ if (supportsOnTouchStart) { return ( <div className={cx(['react-datepicker', 'supports-on-touch-start', this.props.className])} data-terra-date-picker-calendar> <div className={cx('react-datepicker-footer')}> {this.renderTodayButton()} {this.renderCloseButton()} </div> {this.renderMonths()} {this.props.children} </div> ); } /** * Ensures users can start interacting with the calendar via up/down/left/right arrow keys * when it first opens by making the month component render first in the DOM order */ return ( <div className={cx(['react-datepicker', this.props.className])} data-terra-date-picker-calendar> {this.renderMonths()} <div className={cx('react-datepicker-footer')}> {this.renderTodayButton()} </div> {this.props.children} </div> ) } }