/* * 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(