UNPKG

@syncfusion/react-calendars

Version:

A complete package of date or time components with built-in features such as date formatting, inline editing, multiple (range) selection, range restriction, month and year selection, strict mode, and globalization.

1,102 lines (1,101 loc) 49.7 kB
import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime"; import { useEffect, useRef, useState, useImperativeHandle, forwardRef, isValidElement, useCallback } from 'react'; import { L10n, formatDate, parseDate, useProviderContext } from '@syncfusion/react-base'; import { getValue, getDefaultDateObject, isNullOrUndefined, cldrData, preRender } from '@syncfusion/react-base'; import * as React from 'react'; import { Button, Color, Variant } from '@syncfusion/react-buttons'; import { addMonths, addYears, getWeekNumber } from '../../src/utils/calendar-util'; import { ChevronLeftIcon, ChevronRightIcon, CloseIcon } from '@syncfusion/react-icons'; import { CalendarCell } from './calendar-cell'; /** * Specifies the view options for the calendar. * * @enum {string} */ export var CalendarView; (function (CalendarView) { /** * Displays the calendar by month. * */ CalendarView["Month"] = "Month"; /** * Displays the calendar by year. * */ CalendarView["Year"] = "Year"; /** * Displays the calendar by decade. * */ CalendarView["Decade"] = "Decade"; })(CalendarView || (CalendarView = {})); /** * Specifies the format of the day to be displayed in the header. * * @enum {string} */ export var WeekDaysFormats; (function (WeekDaysFormats) { /** * Short format, typically a single letter. * */ WeekDaysFormats["Short"] = "Short"; /** * Narrow format, usually a minimal abbreviation. * */ WeekDaysFormats["Narrow"] = "Narrow"; /** * Abbreviated format, a shortened form of the day name. * */ WeekDaysFormats["Abbreviated"] = "Abbreviated"; /** * Wide format, the full name of the day. * */ WeekDaysFormats["Wide"] = "Wide"; })(WeekDaysFormats || (WeekDaysFormats = {})); /** * Specifies the rules used to determine which week is considered * the first week of a calendar year. * */ export var WeekRule; (function (WeekRule) { /** * The first week begins on January first, regardless of * which day of the week it falls on. * */ WeekRule["FirstDay"] = "FirstDay"; /** * The first week begins on the first occurrence of the * designated weekday on or after January first. * */ WeekRule["FirstFullWeek"] = "FirstFullWeek"; /** * The first week must contain at least four days of the new year, * following the ISO standard definition. * */ WeekRule["FirstFourDayWeek"] = "FirstFourDayWeek"; })(WeekRule || (WeekRule = {})); export const Calendar = forwardRef((props, ref) => { const { className = '', minDate = new Date(1900, 0, 1), maxDate = new Date(2099, 11, 31), depth = CalendarView.Month, start = CalendarView.Month, firstDayOfWeek: firstDayOfWeekProp = 0, value, defaultValue, readOnly = false, weekNumber = false, weekRule = WeekRule.FirstDay, showTodayButton = true, weekDaysFormat = WeekDaysFormats.Short, disabled = false, multiSelect = false, onViewChange, cellTemplate = null, onChange, fullScreenMode = false, ...otherProps } = props; const isControlled = value !== undefined; const { locale } = useProviderContext(); const localeStrings = { today: 'Today' }; const [currentView, setCurrentView] = useState(start); const [isImproperDateRange, setIsImproperDateRange] = useState(false); const [isTodayDisabled, setIsTodayDisabled] = useState(false); const [isPreviousDisabled, setIsPreviousDisabled] = useState(false); const [isNextDisabled, setIsNextDisabled] = useState(false); const [focusedActiveDecendent, setFocusedActiveDecendent] = useState(''); const [onSelection, setOnSelection] = useState(false); const [headerTitle, setheaderTitle] = useState(''); const firstDayOfWeek = firstDayOfWeekProp !== null ? (firstDayOfWeekProp > 6 ? 0 : firstDayOfWeekProp) : 0; const calendarElement = useRef(null); const headerTitleElement = useRef(null); const focusedElementRef = useRef(null); const selectedElementRef = useRef(null); const l10nInstance = L10n('calendar', localeStrings, locale || 'en-US'); const [isHeaderFocused, setIsHeaderFocused] = useState(false); let startDecadeHdYr = ''; let endDecadeHdYr = ''; const { dir } = useProviderContext(); const [internalValue, setInternalValue] = useState(() => { if (isControlled) { return null; } if (defaultValue !== undefined) { return defaultValue; } if (multiSelect) { return []; } return null; }); const currentValue = isControlled ? value : internalValue; const normalizedDates = React.useMemo(() => { if (multiSelect && Array.isArray(currentValue)) { return currentValue.filter((d) => (!minDate || d >= minDate) && (!maxDate || d <= maxDate)); } if (!multiSelect && currentValue && !Array.isArray(currentValue)) { if (minDate && currentValue < minDate) { return [minDate]; } if (maxDate && currentValue > maxDate) { return [maxDate]; } if ((!minDate || currentValue >= minDate) && (!maxDate || currentValue <= maxDate)) { return [currentValue]; } } return []; }, [currentValue, minDate, maxDate, multiSelect]); const [currentDate, setCurrentDate] = useState(() => { if (normalizedDates.length > 0) { if (multiSelect) { return normalizedDates[normalizedDates.length - 1]; } else { return normalizedDates[0]; } } if (!isControlled && defaultValue) { if (Array.isArray(defaultValue) && defaultValue.length > 0) { return defaultValue[defaultValue.length - 1]; } else if (!Array.isArray(defaultValue)) { return defaultValue; } } const today = new Date(); return today < minDate ? new Date(minDate) : today > maxDate ? new Date(maxDate) : today; }); useEffect(() => { setCurrentView(start); }, [start]); const updateValue = useCallback((newValue, event) => { if (!isControlled) { setInternalValue(newValue); } if (onChange) { onChange({ value: newValue, event }); } }, [isControlled, onChange]); const publicAPI = { value: currentValue, multiSelect, minDate, maxDate, firstDayOfWeek, start, depth, weekNumber, disabled, weekRule, showTodayButton, weekDaysFormat, cellTemplate }; useEffect(() => { preRender('calendar'); }, []); useEffect(() => { if (currentView === CalendarView.Month) { titleUpdate(currentDate, 'days'); } else if (currentView === CalendarView.Year) { titleUpdate(currentDate, 'months'); } else { setheaderTitle(`${startDecadeHdYr} - ${endDecadeHdYr}`); } setIsImproperDateRange(minDate > maxDate); const today = new Date(); today.setHours(0, 0, 0, 0); setIsTodayDisabled(today < minDate || today > maxDate); iconHandler(); }, [currentDate, currentView, minDate, maxDate, onSelection]); useEffect(() => { if (focusedElementRef.current) { setFocusedActiveDecendent(focusedElementRef.current.id); } }, [focusedElementRef.current]); const iconHandler = () => { const current = new Date(currentDate); current.setDate(1); switch (currentView) { case CalendarView.Month: setIsPreviousDisabled(compareMonth(currentDate, minDate) < 1); setIsNextDisabled(compareMonth(currentDate, maxDate) > -1); break; case CalendarView.Year: setIsPreviousDisabled(compare(currentDate, minDate, 0) < 1); setIsNextDisabled(compare(currentDate, maxDate, 0) > -1); break; case CalendarView.Decade: setIsPreviousDisabled(compare(currentDate, minDate, 10) < 1); setIsNextDisabled(compare(currentDate, maxDate, 10) > -1); } }; const previous = () => { switch (currentView) { case CalendarView.Month: setCurrentDate(addMonths(currentDate, -1)); break; case CalendarView.Year: setCurrentDate(addYears(currentDate, -1)); break; case CalendarView.Decade: setCurrentDate(addYears(currentDate, -10)); break; } }; const next = () => { switch (currentView) { case CalendarView.Month: setCurrentDate(addMonths(currentDate, 1)); break; case CalendarView.Year: setCurrentDate(addYears(currentDate, 1)); break; case CalendarView.Decade: setCurrentDate(addYears(currentDate, 10)); break; } }; const compareMonth = (start, end) => { let result; if (start.getFullYear() > end.getFullYear()) { result = 1; } else if (start.getFullYear() < end.getFullYear()) { result = -1; } else { result = start.getMonth() === end.getMonth() ? 0 : start.getMonth() > end.getMonth() ? 1 : -1; } return result; }; const compare = (startDate, endDate, modifier) => { let start = endDate.getFullYear(); let end = start; let result = 0; if (modifier) { start = start - (start % modifier); end = start - (start % modifier) + modifier - 1; } if (startDate.getFullYear() > end) { result = 1; } else if (startDate.getFullYear() < start) { result = -1; } return result; }; const addContentFocus = () => { if (selectedElementRef.current) { selectedElementRef.current.classList.add('sf-focused-cell'); } else if (focusedElementRef.current) { focusedElementRef.current.classList.add('sf-focused-cell'); } }; const removeContentFocus = () => { if (selectedElementRef.current) { selectedElementRef.current.classList.remove('sf-focused-cell'); } else if (focusedElementRef.current) { focusedElementRef.current.classList.remove('sf-focused-cell'); } }; const navigatedTo = (e, view, date) => { e.preventDefault(); if (date && +date >= +minDate && +date <= +maxDate) { setCurrentDate(date); } if (date && +date <= +minDate) { setCurrentDate(new Date(minDate)); } if (date && +date >= +maxDate) { setCurrentDate(new Date(maxDate)); } if (onViewChange) { onViewChange({ event: e, view: view, date: currentDate || date }); } setCurrentView(view); }; const minMaxDate = (localDate) => { const currentDate = new Date(new Date(+localDate).setHours(0, 0, 0, 0)); const min = new Date(new Date(+minDate).setHours(0, 0, 0, 0)); const max = new Date(new Date(+maxDate).setHours(0, 0, 0, 0)); if (+currentDate === +min || +currentDate === +max) { if (+localDate < +minDate) { return new Date(+minDate); } if (+localDate > +maxDate) { return new Date(+maxDate); } } return localDate; }; const renderMonths = (date) => { const monthPerRow = weekNumber ? 8 : 7; const tdEles = renderDays(date); return (_jsxs(_Fragment, { children: [createContentHeader(), renderTemplate(tdEles, monthPerRow, 'sf-month')] })); }; const todayButtonClick = (e) => { if (showTodayButton) { e.preventDefault(); const tempValue = new Date(new Date().setHours(0, 0, 0, 0)); const dateValue = new Date(+tempValue.getTime()); if (isControlled) { if (onChange) { onChange({ value: multiSelect ? [tempValue] : tempValue, ...e }); } } else { if (multiSelect) { const currentValues = Array.isArray(currentValue) ? currentValue : []; const newValues = [...currentValues, tempValue]; updateValue(newValues, e); } else { updateValue(tempValue, e); } } if (depth !== CalendarView.Month) { navigatedTo(e, depth, new Date(dateValue)); } else { if (getViewNumber(start) >= getViewNumber(depth)) { navigatedTo(e, depth, new Date(dateValue)); } else { navigatedTo(e, CalendarView.Month, new Date(dateValue)); } } } }; const renderDays = (currentDate, isTodayDate) => { const tdEles = []; const totalDaysInGrid = 42; const todayDate = isTodayDate ? new Date(+currentDate) : new Date(); let localDate = new Date(currentDate); const currentMonth = localDate.getMonth(); let minMaxDates; localDate = new Date(localDate.getFullYear(), localDate.getMonth(), 0, localDate.getHours(), localDate.getMinutes(), localDate.getSeconds(), localDate.getMilliseconds()); while (localDate.getDay() !== firstDayOfWeek) { setStartDate(localDate, -1 * 86400000); } for (let day = 0; day < totalDaysInGrid; ++day) { const isWeekNumber = day % 7 === 0 && weekNumber; minMaxDates = new Date(+localDate); localDate = minMaxDate(localDate); const dateFormatOptions = { locale: locale, type: 'dateTime', skeleton: 'full' }; const date = parseDate(formatDate(localDate, dateFormatOptions), dateFormatOptions); const isOtherMonth = date.getMonth() !== currentMonth; const isToday = date && todayDate && date.getFullYear() === todayDate.getFullYear() && date.getMonth() === todayDate.getMonth() && date.getDate() === todayDate.getDate(); const isSelected = multiSelect ? normalizedDates.some((selectedDate) => selectedDate && date.toDateString() === selectedDate.toDateString()) : normalizedDates.length > 0 && date.toDateString() === normalizedDates[0].toDateString(); const isDisabled = (minDate > date) || (maxDate < date); const isWeekend = date.getDay() === 0 || date.getDay() === 6; let isFocused = false; if (currentDate.getDate() === localDate.getDate() && !isOtherMonth && !isDisabled) { isFocused = true; } else { if (currentDate >= maxDate && parseInt(`${date.valueOf()}`, 10) === +maxDate && !isOtherMonth && !isDisabled) { isFocused = true; } if (currentDate <= minDate && parseInt(`${date.valueOf()}`, 10) === +minDate && !isOtherMonth && !isDisabled) { isFocused = true; } } const baseClasses = `${isOtherMonth ? 'sf-other-month' : ''} ${isWeekend ? 'sf-weekend' : ''} ${isDisabled ? 'sf-disabled sf-overlay' : ''}`; const disabledClass = disabled ? 'sf-disabled sf-overlay' : ''; const className = `sf-cell ${baseClasses} ${disabledClass} ${isToday ? 'sf-today' : ''} ${isToday && isFocused && !disabled ? 'sf-focused-date' : ''} ${isFocused && !onSelection && !disabled ? 'sf-focused-date' : ''} ${isSelected ? 'sf-selected' : ''}`.trim(); if (isWeekNumber) { tdEles.push(_jsx("td", { className: 'sf-cell sf-week-number', style: { cursor: 'default' }, children: _jsx("span", { children: (() => { const numberOfDays = weekRule === 'FirstDay' ? 6 : (weekRule === 'FirstFourDayWeek' ? 3 : 0); const finalDate = new Date(localDate.getFullYear(), localDate.getMonth(), (localDate.getDate() + numberOfDays)); return getWeekNumber(finalDate); })() }) }, `week-${day}`)); } const cellProps = { id: `${localDate.valueOf()}`, className: className, isDisabled: isDisabled || (disabled ? true : false), isOutOfRange: isOtherMonth, isToday: isToday, isSelected: isSelected, isFocused: isFocused && !onSelection, isWeekend: isWeekend, date: new Date(date), onClick: (e) => { if (disabled) { return; } clickHandler(e, date); }, title: formatDate(date, { locale: locale, type: 'date', skeleton: 'full' }) }; if (typeof cellTemplate === 'function') { try { const wrappedCellProps = { ...cellProps, ref: (el) => { if (isFocused && el) { focusedElementRef.current = el; } if (isSelected && el) { selectedElementRef.current = el; } } }; const CustomCell = cellTemplate; tdEles.push(CustomCell(wrappedCellProps)); } catch (error) { tdEles.push(_jsx(CalendarCell, { ...cellProps, ref: (el) => { if (isFocused && el) { focusedElementRef.current = el; } if (isSelected && el) { selectedElementRef.current = el; } }, children: renderDayCells(localDate, isDisabled, isOtherMonth) })); } } else { tdEles.push(_jsx(CalendarCell, { ...cellProps, ref: (el) => { if (isFocused && el) { focusedElementRef.current = el; } if (isSelected && el) { selectedElementRef.current = el; } }, children: renderDayCells(localDate, isDisabled, isOtherMonth) })); } localDate = new Date(+minMaxDates); localDate.setDate(localDate.getDate() + 1); } return tdEles; }; const setStartDate = (date, time) => { const tzOffset = date.getTimezoneOffset(); const d = new Date(date.getTime() + time); const tzOffsetDiff = d.getTimezoneOffset() - tzOffset; const minutesMilliSeconds = 60000; date.setTime(d.getTime() + tzOffsetDiff * minutesMilliSeconds); }; const renderDayCells = (date, isDisabled, isOtherMonth) => { const title = formatDate(date, { locale: locale, type: 'date', skeleton: 'full' }); const dayText = formatDate(date, { locale: locale, format: 'd', type: 'date', skeleton: 'yMd' }); const isWeekend = date.getDay() === 0 || date.getDay() === 6; if (isValidElement(cellTemplate)) { return cellTemplate; } else if (typeof cellTemplate === 'function') { const isCalendarCellComponent = (cellTemplate).length === 1; if (isCalendarCellComponent) { return null; } else { const customContent = cellTemplate(date, isWeekend, currentView, currentDate); if (customContent) { return customContent; } } } return (_jsx("span", { className: "sf-day", title: title, "aria-disabled": isDisabled || isOtherMonth, children: dayText })); }; const createContentHeader = () => { const effectiveFirstDayOfWeek = firstDayOfWeek; const shortNames = !isNullOrUndefined(weekDaysFormat) ? getCultureValues().length > 0 ? shiftArray(getCultureValues(), effectiveFirstDayOfWeek) : ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] : []; return (_jsx("thead", { className: 'sf-week-header', children: _jsxs("tr", { children: [weekNumber && _jsx("th", { className: 'sf-week-number', "aria-hidden": 'true' }), shortNames.slice(0, 7).map((day, index) => (_jsx("th", { className: '', children: toCapitalize(day) }, index)))] }) })); }; const getCultureValues = () => { const culShortNames = []; let cldrObj; const dayFormat = 'days.stand-alone.' + weekDaysFormat?.toLowerCase(); if ((locale === 'en' || locale === 'en-US') && !isNullOrUndefined(dayFormat)) { cldrObj = getValue(dayFormat, getDefaultDateObject()); } else { cldrObj = getCultureObjects(cldrData, (locale || 'en-US')); } if (!isNullOrUndefined(cldrObj)) { for (const obj of Object.keys(cldrObj)) { culShortNames.push(getValue(obj, cldrObj)); } } return culShortNames; }; const getCultureObjects = (ld, c) => { const gregorianFormat = weekDaysFormat ? `.dates.calendars.gregorian.days.format.${weekDaysFormat.toLowerCase()}` : ''; const mainVal = 'main.'; return getValue(`${mainVal}${c}${gregorianFormat}`, ld); }; const shiftArray = (array, places) => { return array.slice(places).concat(array.slice(0, places)); }; const renderTemplate = (elements, count, classNm) => { const rows = []; for (let i = 0; i < elements.length; i += count) { const trElements = elements.slice(i, i + count); const hasOtherMonth = trElements.some((element) => isValidElement(element) && 'className' in element.props && typeof element.props.className === 'string' && element.props.className.includes('sf-other-month')); const isOtherMonthRow = hasOtherMonth && trElements.every((element) => isValidElement(element) && 'className' in element.props && typeof element.props.className === 'string' && (element.props.className.includes('sf-other-month') || element.props.className.includes('sf-week-number'))); rows.push(_jsx("tr", { className: isOtherMonthRow && i === 0 ? 'sf-month-hide' : '', children: trElements }, i)); } return (_jsx("tbody", { className: classNm, children: rows })); }; const renderYears = () => { const monthPerRow = 3; const tdEles = []; const curDate = new Date(currentDate); const curYear = curDate.getFullYear(); const curMonth = curDate.getMonth(); const min = new Date(minDate); const max = new Date(maxDate); const minYear = min.getFullYear(); const minMonth = min.getMonth(); const maxYear = max.getFullYear(); const maxMonth = max.getMonth(); const selectedDate = multiSelect ? (Array.isArray(currentValue) && currentValue.length > 0 ? currentValue[currentValue.length - 1] : null) : (currentValue && !Array.isArray(currentValue) ? currentValue : null); const selectedMonth = selectedDate?.getMonth(); const selectedYear = selectedDate?.getFullYear(); for (let month = 0; month < 12; ++month) { const monthDate = new Date(curYear, month, 1); const isFocusedDate = month === curMonth; const isSelected = selectedMonth === month && selectedYear === curYear; const isDisabled = (minDate && (curYear < minYear || (curYear === minYear && month < minMonth))) || (maxDate && (curYear > maxYear || (curYear === maxYear && month > maxMonth))); const className = 'sf-cell' + (isSelected ? ' sf-selected' : isFocusedDate ? ' sf-focused-date' : '') + (isDisabled ? ' sf-disabled' : ''); tdEles.push(_jsx("td", { className: className, id: `${monthDate.valueOf()}`, onClick: (e) => handleCellClick(e, monthDate, currentView), ref: (el) => { if (isFocusedDate && el) { focusedElementRef.current = el; } if (isSelected && el) { selectedElementRef.current = el; } }, "aria-selected": isSelected || false, children: renderYearCell(monthDate) }, month)); } return _jsx(_Fragment, { children: renderTemplate(tdEles, monthPerRow, 'sf-zoomin') }); }; const renderYearCell = (localDate) => { const title = formatDate(localDate, { locale: locale, type: 'date', format: 'MMM y' }); const content = toCapitalize(formatDate(localDate, { locale: locale, format: undefined, type: 'dateTime', skeleton: 'MMM' })); return (_jsx("span", { className: 'sf-day', title: title, children: content })); }; const renderDecades = () => { const yearPerRow = 3; const yearCell = 12; const tdEles = []; const curDate = new Date(currentDate); const curYear = curDate.getFullYear(); const localDate = new Date(curDate); localDate.setMonth(0); const baseDecadeStartYear = curYear - (curYear % 10); const startYr = new Date(localDate.setFullYear(baseDecadeStartYear)); const endYr = new Date(localDate.setFullYear(baseDecadeStartYear + 9)); const startFullYr = startYr.getFullYear(); const endFullYr = endYr.getFullYear(); startDecadeHdYr = formatDate(startYr, { locale: locale, format: undefined, type: 'dateTime', skeleton: 'y' }); endDecadeHdYr = formatDate(endYr, { locale: locale, format: undefined, type: 'dateTime', skeleton: 'y' }); const minYear = new Date(minDate).getFullYear(); const maxYear = new Date(maxDate).getFullYear(); const selectedDate = multiSelect ? (Array.isArray(currentValue) && currentValue.length > 0 ? currentValue[currentValue.length - 1] : null) : (currentValue && !Array.isArray(currentValue) ? currentValue : null); const startYear = baseDecadeStartYear - 1; const selectedYear = selectedDate?.getFullYear() || null; for (let i = 0; i < yearCell; ++i) { const year = startYear + i; localDate.setFullYear(year); const fullYear = localDate.getFullYear(); const isOutOfRange = fullYear < minYear || fullYear > maxYear; const isOtherDecade = fullYear < startFullYr || fullYear > endFullYr; let className = 'sf-cell'; let isSelected = false; let isFocused = false; if (isOutOfRange) { className += ' sf-disabled'; } if (isOtherDecade) { className += ' sf-other-year'; } if (selectedYear === fullYear) { className += ' sf-selected'; isSelected = true; } else if (fullYear === curYear) { className += ' sf-focused-date'; isFocused = true; } const decadeDate = new Date(localDate); tdEles.push(_jsx("td", { className: className, id: `${decadeDate.valueOf()}`, onClick: (e) => handleCellClick(e, decadeDate, currentView), ref: (el) => { if (isFocused && el) { focusedElementRef.current = el; } if (isSelected && el) { selectedElementRef.current = el; } }, "aria-selected": isSelected || false, children: renderDecadeCell(decadeDate) }, year)); } return _jsx(_Fragment, { children: renderTemplate(tdEles, yearPerRow, 'sf-zoomin') }); }; const renderDecadeCell = (localDate) => { const content = formatDate(localDate, { locale: locale, format: undefined, type: 'dateTime', skeleton: 'y' }); return (_jsx("span", { className: 'sf-day', children: content })); }; const handleCellClick = useCallback((e, date, view) => { setCurrentDate(date); if (view === CalendarView.Year) { const year = date.getFullYear(); const month = date.getMonth(); const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const firstValidDay = new Date(firstDay); while (firstValidDay <= lastDay) { if (firstValidDay >= minDate && firstValidDay <= maxDate) { break; } firstValidDay.setDate(firstValidDay.getDate() + 1); } if (firstValidDay > lastDay) { return; } if (depth === CalendarView.Year) { updateValue(firstValidDay, e); } else { navigatedTo(e, CalendarView.Month, firstValidDay); } return; } if (depth === CalendarView.Decade) { updateValue(date, e); } else { navigatedTo(e, CalendarView.Year, date); } }, [minDate, maxDate, depth, updateValue, navigatedTo]); const titleUpdate = (date, view) => { const dayFormatOptions = formatDate(date, { locale: locale, type: 'dateTime', skeleton: 'yMMMM', calendar: 'gregorian' }); const monthFormatOptions = formatDate(date, { locale: locale, format: undefined, type: 'dateTime', skeleton: 'y', calendar: 'gregorian' }); switch (view) { case 'days': if (headerTitleElement.current) { setheaderTitle(toCapitalize(dayFormatOptions)); } break; case 'months': if (headerTitleElement.current) { setheaderTitle(monthFormatOptions); } break; } }; const clickHandler = (e, value) => { if (readOnly || disabled || isImproperDateRange) { return; } const isOtherMonth = currentView === CalendarView.Month && value.getMonth() !== currentDate.getMonth(); if (isOtherMonth) { const newDate = new Date(value); setCurrentDate(newDate); return; } const storeView = currentView; selectDate(e, getIdValue(e, null)); if (multiSelect && currentDate !== value && (storeView === CalendarView.Year || storeView === CalendarView.Month)) { if (focusedElementRef.current) { focusedElementRef.current.classList.remove('sf-focused-date'); } } if (calendarElement.current) { calendarElement.current.focus(); } }; const selectDate = (e, date) => { if (isControlled) { if (onChange) { let newValue; if (multiSelect) { const currentValues = Array.isArray(currentValue) ? currentValue : []; const dateExists = currentValues.some((v) => v.toDateString() === date.toDateString()); if (dateExists) { newValue = currentValues.filter((v) => v.toDateString() !== date.toDateString()); } else { newValue = [...currentValues, date]; } } else { newValue = date; } onChange({ value: newValue, ...e }); } } else { if (multiSelect) { const currentValues = Array.isArray(currentValue) ? currentValue : []; const dateExists = currentValues.some((v) => v.toDateString() === date.toDateString()); let newValues; if (dateExists) { newValues = currentValues.filter((v) => v.toDateString() !== date.toDateString()); } else { newValues = [...currentValues, date]; } updateValue(newValues, e); } else { updateValue(date, e); } } setOnSelection(true); setCurrentDate(date); }; const getIdValue = (e, element) => { let eve; if (e) { eve = e.currentTarget; } else { eve = element; } const dateFormatOptions = { locale: locale, type: 'dateTime', skeleton: 'full', calendar: 'gregorian' }; const dateString = formatDate(new Date(parseInt('' + eve.getAttribute('id'), 10)), dateFormatOptions); const date = parseDate(dateString, dateFormatOptions); const value = date.valueOf() - date.valueOf() % 1000; return new Date(value); }; const handleHeaderTitleClick = (e, view) => { switch (view) { case CalendarView.Month: navigatedTo(e, CalendarView.Year); break; case CalendarView.Year: navigatedTo(e, CalendarView.Decade); break; default: break; } }; const toCapitalize = (text) => { return text.charAt(0).toUpperCase() + text.slice(1); }; const getViewNumber = (view) => { switch (view) { case CalendarView.Month: return 0; case CalendarView.Year: return 1; case CalendarView.Decade: return 2; default: return 0; } }; const keyActionHandle = (e) => { if (calendarElement.current === null || e.key === 'Escape' || readOnly || disabled || isImproperDateRange || isHeaderFocused) { return; } e.stopPropagation(); setOnSelection(false); const selectedDate = multiSelect ? selectedElementRef.current : focusedElementRef.current; const view = getViewNumber(currentView); const depthValue = getViewNumber(depth); const levelRestrict = (view === depthValue && getViewNumber(start) >= depthValue); const element = focusedElementRef.current || selectedDate; switch (e.key) { case 'ArrowLeft': keyboardNavigate(-1, view); e.preventDefault(); break; case 'ArrowRight': keyboardNavigate(1, view); e.preventDefault(); break; case 'ArrowUp': keyboardNavigate(view === 0 ? -7 : -4, view); e.preventDefault(); break; case 'ArrowDown': keyboardNavigate(view === 0 ? 7 : 4, view); e.preventDefault(); break; case 'Enter': if (element) { if (levelRestrict) { const d = new Date(parseInt(element.id, 10)); selectDate(e, d); } else if (!e.currentTarget.className.includes('sf-disabled')) { handleCellClick(e, currentDate, currentView); } } break; case 'Home': setCurrentDate(firstDay(currentDate)); e.preventDefault(); break; case 'End': setCurrentDate(lastDay(currentDate, view)); e.preventDefault(); break; case 'PageUp': if (e.shiftKey) { setCurrentDate(addYears(currentDate, -1)); } else { setCurrentDate(addMonths(currentDate, -1)); } e.preventDefault(); break; case 'PageDown': if (e.shiftKey) { setCurrentDate(addYears(currentDate, 1)); } else { setCurrentDate(addMonths(currentDate, 1)); } e.preventDefault(); break; } if (e.ctrlKey) { switch (e.key) { case 'ArrowUp': if (view < 2 && view >= getViewNumber(depth)) { const nextView = view === 0 ? CalendarView.Year : CalendarView.Decade; navigatedTo(e, nextView); } e.preventDefault(); break; case 'ArrowDown': if (view > 0 && view > depthValue) { const nextView = view === 2 ? CalendarView.Year : CalendarView.Month; navigatedTo(e, nextView, currentDate); } e.preventDefault(); break; case 'Home': navigatedTo(e, CalendarView.Month, new Date(currentDate.getFullYear(), 0, 1)); e.preventDefault(); break; case 'End': navigatedTo(e, CalendarView.Month, new Date(currentDate.getFullYear(), 11, 31)); e.preventDefault(); break; } } }; const firstDay = (date) => { const view = getViewNumber(currentView); const visibleDates = getVisibleDates(date, view); for (const d of visibleDates) { if (!isDateDisabled(d)) { return d; } } return date; }; const lastDay = (date, view) => { const lastDate = new Date(date.getFullYear(), date.getMonth() + 1, 0); if (view !== 2) { const timeOffset = Math.abs(lastDate.getTimezoneOffset() - firstDay(date).getTimezoneOffset()); if (timeOffset) { lastDate.setHours(firstDay(date).getHours() + (timeOffset / 60)); } return findLastDay(lastDate, view); } else { return findLastDay(firstDay(lastDate), view); } }; const findLastDay = (date, view) => { const visibleDates = getVisibleDates(date, view).reverse(); for (const d of visibleDates) { if (!isDateDisabled(d)) { return d; } } return date; }; const isDateDisabled = (date) => { return (minDate && date < minDate) || (maxDate && date > maxDate); }; const getVisibleDates = (baseDate, view) => { const result = []; if (view === 0) { const start = new Date(baseDate.getFullYear(), baseDate.getMonth(), 1); const end = new Date(baseDate.getFullYear(), baseDate.getMonth() + 1, 0); for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { result.push(new Date(d)); } } else if (view === 1) { for (let m = 0; m < 12; m++) { result.push(new Date(baseDate.getFullYear(), m, 1)); } } else if (view === 2) { const decadeStart = baseDate.getFullYear() - (baseDate.getFullYear() % 10) - 1; for (let y = decadeStart; y < decadeStart + 12; y++) { result.push(new Date(y, 0, 1)); } } return result; }; const keyboardNavigate = (num, view) => { const date = new Date(currentDate); switch (view) { case 2: if (isMonthYearRange(date)) { setCurrentDate(addYears(currentDate, num)); } else { setCurrentDate(currentDate); } break; case 1: if (isMonthYearRange(date)) { setCurrentDate(addMonths(currentDate, num)); } else { setCurrentDate(currentDate); } break; case 0: date.setDate(date.getDate() + num); if (+date >= +minDate && +date <= +maxDate) { setCurrentDate(date); } else { setCurrentDate(currentDate); } break; } }; const isMonthYearRange = (date) => { return date.getMonth() >= minDate.getMonth() && date.getFullYear() >= minDate.getFullYear() && date.getMonth() <= maxDate.getMonth() && date.getFullYear() <= maxDate.getFullYear(); }; const renderModelHeader = () => { if (!fullScreenMode) { return null; } return (_jsxs("div", { className: "sf-model-header", children: [_jsx("div", { className: "sf-popup-close", role: "button", "aria-label": "close", children: _jsx(CloseIcon, { width: 14, height: 14 }) }), showTodayButton && (_jsx(Button, { tabIndex: disabled || isImproperDateRange ? -1 : 0, className: `sf-today sf-primary ${isTodayDisabled || disabled || isImproperDateRange ? 'sf-disabled' : ''}`, variant: Variant.Standard, disabled: isTodayDisabled || disabled || isImproperDateRange, onClick: (e) => todayButtonClick(e), "aria-label": 'Today', children: l10nInstance.getConstant('today') })), _jsxs("div", { className: "sf-day-wrapper", children: [_jsx("span", { className: "sf-model-day", children: formatDate(currentValue && !Array.isArray(currentValue) ? currentValue : new Date(), { locale: locale || 'en-US', format: 'E', type: 'date' }) }), _jsx("span", { className: "sf-model-month", children: formatDate(currentValue && !Array.isArray(currentValue) ? currentValue : new Date(), { locale: locale || 'en-US', format: 'MMM d', type: 'date' }) })] })] })); }; useImperativeHandle(ref, () => ({ ...publicAPI, element: calendarElement.current }), [publicAPI]); const classNames = [ 'sf-calendar', className, 'sf-control', dir === 'rtl' ? 'sf-rtl' : '', (isImproperDateRange || disabled) ? 'sf-overlay' : '', weekDaysFormat === WeekDaysFormats.Wide ? 'sf-calendar-day-header-lg' : '', weekNumber ? 'sf-week-number' : '', readOnly ? 'sf-readonly' : '' ].filter(Boolean).join(' '); return (_jsxs("div", { ref: calendarElement, className: classNames, tabIndex: 0, "data-role": 'calendar', onKeyDown: keyActionHandle, onClick: () => { if (calendarElement.current) { calendarElement.current.focus(); } }, ...otherProps, children: [fullScreenMode && renderModelHeader(), _jsxs("div", { className: `sf-header ${currentView === CalendarView.Decade ? 'sf-decade' : currentView === CalendarView.Year ? 'sf-year' : 'sf-month'}`, children: [_jsx("div", { ref: headerTitleElement, className: "sf-day sf-title", onClick: (e) => { if (!disabled && !isImproperDateRange) { handleHeaderTitleClick(e, currentView); } }, onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); if (!disabled && !isImproperDateRange) { handleHeaderTitleClick(e, currentView); } } }, "aria-atomic": "true", "aria-live": "assertive", "aria-label": "title", tabIndex: disabled || isImproperDateRange ? -1 : 0, onFocus: () => setIsHeaderFocused(true), onMouseUp: (e) => { e.currentTarget.blur(); }, onBlur: () => { setIsHeaderFocused(false); if (focusedElementRef.current) { focusedElementRef.current.focus(); } }, children: (() => { if (typeof cellTemplate === 'function') { const customTitle = cellTemplate(currentView, true); if (customTitle) { return customTitle; } } return headerTitle; })() }), _jsxs("div", { className: 'sf-icon-container', children: [_jsx(Button, { className: `sf-prev sf-round ${isPreviousDisabled || disabled || isImproperDateRange ? 'sf-overlay' : ''}`, disabled: isPreviousDisabled || disabled || isImproperDateRange, variant: Variant.Standard, color: Color.Secondary, onClick: () => { setOnSelection(false); previous(); }, onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); setOnSelection(false); previous(); } }, "aria-label": 'previous month', title: currentView === CalendarView.Month ? 'Previous Month' : currentView === CalendarView.Year ? 'Previous Year' : 'Previous Decade', "aria-disabled": isPreviousDisabled, icon: _jsx(ChevronLeftIcon, { viewBox: '0 0 26 24' }), onMouseUp: (e) => { e.currentTarget.blur(); }, onFocus: () => setIsHeaderFocused(true), onBlur: () => { setIsHeaderFocused(false); } }), _jsx(Button, { className: `sf-next sf-round ${isNextDisabled || disabled || isImproperDateRange ? 'sf-overlay' : ''}`, disabled: isNextDisabled || disabled || isImproperDateRange, variant: Variant.Standard, color: Color.Secondary, onClick: () => { setOnSelection(false); next(); }, "aria-label": 'next month', title: currentView === CalendarView.Month ? 'Next Month' : currentView === CalendarView.Year ? 'Next Year' : 'Next Decade', "aria-disabled": isNextDisabled, icon: _jsx(ChevronRightIcon, { viewBox: '0 0 20 26' }), onMouseUp: (e) => { e.currentTarget.blur(); }, onFocus: () => setIsHeaderFocused(true), onBlur: () => { setIsHeaderFocused(false); }, onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); setOnSelection(false); next(); } } })] })] }), _jsx("div", { className: `sf-content ${currentView === CalendarView.Decade ? 'sf-decade' : currentView === CalendarView.Year ? 'sf-year' : 'sf-month'}`, children: _jsx("table", { className: 'sf-calendar-content-table', onFocus: addContentFocus, onBlur: removeContentFocus, tabIndex: disabled || isImproperDateRange ? -1 : 0, "aria-activedescendant": focusedActiveDecendent, "aria-labelledby": 'element', children: (() => { switch (getViewNumber(currentView)) { case 0: return renderMonths(currentDate); case 1: return renderYears(); case 2: return renderDecades(); default: return null; } })() || null }) }), showTodayButton && (_jsx("div", { className: 'sf-footer-container', children: _jsx(Button, { tabIndex: disabled || isImproperDateRange ? -1 : 0, className: `sf-today sf-primary sf-css ${isTodayDisabled || disabled || isImproperDateRange ? 'sf-disabled' : ''}`, variant: Variant.Standard, disabled: isTodayDisabled || disabled || isImproperDateRange, onClick: (e) => todayButtonClick(e), "aria-label": 'Today', children: l10nInstance.getConstant('today') }) }))] })); }); export default React.memo(Calendar);