/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ import YearDropdown from "./year_dropdown"; import MonthDropdown from "./month_dropdown"; import MonthYearDropdown from "./month_year_dropdown"; import Month from "./month"; import Time from "./time"; import React from "react"; import PropTypes from "prop-types"; import classnames from "classnames"; import FocusTrap from "focus-trap-react"; import CalendarContainer from "./calendar_container"; import { now, setMonth, getMonth, addMonths, subtractMonths, getStartOfWeek, getStartOfDate, getStartOfMonth, addDays, cloneDate, formatDate, localizeDate, setYear, getYear, isBefore, isAfter, getLocaleData, getFormattedWeekdayInLocale, getWeekdayShortInLocale, getWeekdayMinInLocale, isSameDay, allDaysDisabledBefore, allDaysDisabledAfter, getEffectiveMinDate, getEffectiveMaxDate } from "./date_utils"; const FocusTrapContainer = React.forwardRef((props, ref) =>
); const DROPDOWN_FOCUS_CLASSNAMES = [ "react-datepicker__year-select", "react-datepicker__month-select", "react-datepicker__month-year-select" ]; const isDropdownSelect = (element = {}) => { const classNames = (element.className || "").split(/\s+/); return DROPDOWN_FOCUS_CLASSNAMES.some( testClassname => classNames.indexOf(testClassname) >= 0 ); }; export default class Calendar extends React.Component { static propTypes = { adjustDateOnChange: PropTypes.bool, className: PropTypes.string, children: PropTypes.node, container: PropTypes.func, dateFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.array]) .isRequired, dayClassName: PropTypes.func, disabledKeyboardNavigation: PropTypes.bool, dropdownMode: PropTypes.oneOf(["scroll", "select"]), endDate: PropTypes.object, excludeDates: PropTypes.array, filterDate: PropTypes.func, fixedHeight: PropTypes.bool, formatWeekNumber: PropTypes.func, highlightDates: PropTypes.instanceOf(Map), includeDates: PropTypes.array, includeTimes: PropTypes.array, injectTimes: PropTypes.array, inline: PropTypes.bool, locale: PropTypes.string, maxDate: PropTypes.object, minDate: PropTypes.object, monthsShown: PropTypes.number, onClickOutside: PropTypes.func.isRequired, onMonthChange: PropTypes.func, onYearChange: PropTypes.func, forceShowMonthNavigation: PropTypes.bool, onDropdownFocus: PropTypes.func, onSelect: PropTypes.func.isRequired, onWeekSelect: PropTypes.func, showTimeSelect: PropTypes.bool, showTimeSelectOnly: PropTypes.bool, timeFormat: PropTypes.string, timeIntervals: PropTypes.number, onTimeChange: PropTypes.func, minTime: PropTypes.object, maxTime: PropTypes.object, excludeTimes: PropTypes.array, timeCaption: PropTypes.string, openToDate: PropTypes.object, peekNextMonth: PropTypes.bool, scrollableYearDropdown: PropTypes.bool, scrollableMonthYearDropdown: PropTypes.bool, preSelection: PropTypes.object, selected: PropTypes.object, selectsEnd: PropTypes.bool, selectsStart: PropTypes.bool, showMonthDropdown: PropTypes.bool, showMonthYearDropdown: PropTypes.bool, showWeekNumbers: PropTypes.bool, showYearDropdown: PropTypes.bool, startDate: PropTypes.object, todayButton: PropTypes.node, useWeekdaysShort: PropTypes.bool, formatWeekDay: PropTypes.func, withPortal: PropTypes.bool, utcOffset: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), weekLabel: PropTypes.string, yearDropdownItemNumber: PropTypes.number, setOpen: PropTypes.func, shouldCloseOnSelect: PropTypes.bool, useShortMonthInDropdown: PropTypes.bool, showDisabledMonthNavigation: PropTypes.bool, previousMonthButtonLabel: PropTypes.string, nextMonthButtonLabel: PropTypes.string, renderCustomHeader: PropTypes.func, renderDayContents: PropTypes.func, updateSelection: PropTypes.func.isRequired, accessibleMode: PropTypes.bool, enableFocusTrap: PropTypes.bool }; static get defaultProps() { return { onDropdownFocus: () => {}, monthsShown: 1, forceShowMonthNavigation: false, timeCaption: "Time", previousMonthButtonLabel: "Previous Month", nextMonthButtonLabel: "Next Month", enableFocusTrap: true }; } constructor(props) { super(props); this.state = { date: this.localizeDate(this.getDateInView()), selectingDate: null, monthContainer: null, pauseFocusTrap: false }; this.monthRef = React.createRef(); this.yearRef = React.createRef(); } componentDidMount() { // monthContainer height is needed in time component // to determine the height for the ul in the time component // setState here so height is given after final component // layout is rendered if (this.props.showTimeSelect) { this.assignMonthContainer = (() => { this.setState({ monthContainer: this.monthContainer }); })(); } } 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) }); } } setMonthRef = (node) => { this.monthRef = node; } setYearRef = (node) => { this.yearRef = node; } handleOnDropdownToggle = (isOpen, dropdown) => { this.setState({pauseFocusTrap: isOpen}); if (!isOpen) { const element = dropdown === 'month' ? this.monthRef : this.yearRef; if (element) { // The focus trap has been unpaused and will reinitialize focus // but does so on the wrong element (calendar) // This refocuses the previous element (dropdown button). // Duration arrived at by trial-and-error. setTimeout(() => element.focus(), 25); } } } handleClickOutside = event => { this.props.onClickOutside(event); }; handleDropdownFocus = event => { if (isDropdownSelect(event.target)) { this.props.onDropdownFocus(); } }; getDateInView = () => { const { preSelection, selected, openToDate, utcOffset } = this.props; const minDate = getEffectiveMinDate(this.props); const maxDate = getEffectiveMaxDate(this.props); const current = now(utcOffset); 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 = () => { this.setState( { date: addMonths(cloneDate(this.state.date), 1) }, () => this.handleMonthChange(this.state.date) ); }; decreaseMonth = () => { this.setState( { date: subtractMonths(cloneDate(this.state.date), 1) }, () => this.handleMonthChange(this.state.date) ); }; handleDayClick = (day, event) => this.props.onSelect(day, event); handleDayMouseEnter = day => this.setState({ selectingDate: day }); handleMonthMouseLeave = () => this.setState({ selectingDate: null }); handleYearChange = date => { if (this.props.onYearChange) { this.props.onYearChange(date); } if (this.props.accessibleMode) { this.handleSelectionChange(date); } }; handleMonthChange = date => { if (this.props.onMonthChange) { this.props.onMonthChange(date); } if (this.props.accessibleMode) { this.handleSelectionChange(date); } }; handleSelectionChange = date => { if (this.props.adjustDateOnChange) { this.props.updateSelection(date); } else { this.props.updateSelection(getStartOfMonth(cloneDate(date))); } } handleMonthYearChange = date => { this.handleYearChange(date); this.handleMonthChange(date); }; changeYear = year => { this.setState( { date: setYear(cloneDate(this.state.date), year) }, () => this.handleYearChange(this.state.date) ); }; changeMonth = month => { this.setState( { date: setMonth(cloneDate(this.state.date), month) }, () => this.handleMonthChange(this.state.date) ); }; changeMonthYear = monthYear => { this.setState( { date: setYear( setMonth(cloneDate(this.state.date), getMonth(monthYear)), getYear(monthYear) ) }, () => this.handleMonthYearChange(this.state.date) ); }; header = (date = this.state.date) => { const startOfWeek = getStartOfWeek(cloneDate(date)); const dayNames = []; if (this.props.showWeekNumbers) { dayNames.push(
{this.props.weekLabel || "#"}
); } 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.formatWeekday(localeData, day); return (
{weekDayName}
); }) ); }; formatWeekday = (localeData, day) => { if (this.props.formatWeekDay) { return getFormattedWeekdayInLocale( localeData, day, this.props.formatWeekDay ); } return this.props.useWeekdaysShort ? getWeekdayShortInLocale(localeData, day) : getWeekdayMinInLocale(localeData, day); }; renderPreviousMonthButton = () => { if (this.props.renderCustomHeader) { return; } const allPrevDaysDisabled = allDaysDisabledBefore( this.state.date, "month", this.props ); if ( (!this.props.forceShowMonthNavigation && !this.props.showDisabledMonthNavigation && allPrevDaysDisabled) || this.props.showTimeSelectOnly ) { return; } const classes = [ "react-datepicker__navigation", "react-datepicker__navigation--previous" ]; let clickHandler = this.decreaseMonth; if (allPrevDaysDisabled && this.props.showDisabledMonthNavigation) { classes.push("react-datepicker__navigation--previous--disabled"); clickHandler = null; } return ( ); }; renderNextMonthButton = () => { if (this.props.renderCustomHeader) { return; } const allNextDaysDisabled = allDaysDisabledAfter( this.state.date, "month", this.props ); if ( (!this.props.forceShowMonthNavigation && !this.props.showDisabledMonthNavigation && allNextDaysDisabled) || this.props.showTimeSelectOnly ) { return; } const classes = [ "react-datepicker__navigation", "react-datepicker__navigation--next" ]; if (this.props.showTimeSelect) { classes.push("react-datepicker__navigation--next--with-time"); } if (this.props.todayButton) { classes.push("react-datepicker__navigation--next--with-today-button"); } let clickHandler = this.increaseMonth; if (allNextDaysDisabled && this.props.showDisabledMonthNavigation) { classes.push("react-datepicker__navigation--next--disabled"); clickHandler = null; } return ( ); }; 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"); } if (this.props.showMonthYearDropdown) { classes.push("react-datepicker__current-month--hasMonthYearDropdown"); } return (
{formatDate(date, this.props.dateFormat)}
); }; renderYearDropdown = (overrideHide = false) => { if (!this.props.showYearDropdown || overrideHide) { return; } return ( ); }; renderMonthDropdown = (overrideHide = false) => { if (!this.props.showMonthDropdown || overrideHide) { return; } return ( ); }; renderMonthYearDropdown = (overrideHide = false) => { if (!this.props.showMonthYearDropdown || overrideHide) { return; } return ( ); }; renderTodayButton = () => { if (!this.props.todayButton || this.props.showTimeSelectOnly) { return; } return (
this.props.onSelect(getStartOfDate(now(this.props.utcOffset)), e) } > {this.props.todayButton}
); }; renderDefaultHeader = ({ monthDate, i }) => (
{this.renderCurrentMonth(monthDate)}
{this.renderMonthDropdown(i !== 0)} {this.renderMonthYearDropdown(i !== 0)} {this.renderYearDropdown(i !== 0)}
{this.header(monthDate)}
); renderCustomHeader = ({ monthDate, i }) => { if (i !== 0) { return null; } const prevMonthButtonDisabled = allDaysDisabledBefore( this.state.date, "month", this.props ); const nextMonthButtonDisabled = allDaysDisabledAfter( this.state.date, "month", this.props ); return (
{this.props.renderCustomHeader({ ...this.state, changeMonth: this.changeMonth, changeYear: this.changeYear, decreaseMonth: this.decreaseMonth, increaseMonth: this.increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled })}
{this.header(monthDate)}
); }; renderMonths = () => { if (this.props.showTimeSelectOnly) { return; } 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(
{ this.monthContainer = div; }} className="react-datepicker__month-container" > {this.props.renderCustomHeader ? this.renderCustomHeader({ monthDate, i }) : this.renderDefaultHeader({ monthDate, i })}
); } return monthList; }; renderTimeSection = () => { if ( this.props.showTimeSelect && (this.state.monthContainer || this.props.showTimeSelectOnly) ) { return (