UNPKG

@kadconsulting/dry

Version:
337 lines 20.3 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { useState, useEffect, memo, useRef, useMemo, useCallback, forwardRef, } from 'react'; import Button from '../Button/Button'; import DropdownSelect from '../DropdownSelect/DropdownSelect'; // TODO-DRY: icon needs to be easer to use import Icon from '../Icons/Icon/Icon'; import { ChevronRight, ChevronLeft } from '../Icons/paths'; import { IconSizes } from '../Icons/Icon/IconTypes'; // import * as Utils from "./DatePicker.utils.js"; import './DatePicker.scss'; import classnames from 'classnames'; // Utility Functions const generateUpdatedDates = (count, daysUntilBookingAllowed) => { if (!count) { return null; } const currentDate = new Date(); const updatedDates = []; for (let i = daysUntilBookingAllowed || 0; i < count + (daysUntilBookingAllowed || 0); i++) { const updatedDate = new Date(currentDate); updatedDate.setDate(currentDate.getDate() + i); updatedDates.push(updatedDate); } return updatedDates; }; const formatDate = (date, format) => { const year = date.getFullYear(); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, '0'); const hours = date.getHours().toString().padStart(2, '0'); const minutes = date.getMinutes().toString().padStart(2, '0'); switch (format) { case 'MM/dd/yyyy': return `${month}/${day}/${year}`; case 'dd/MM/yyyy': return `${day}/${month}/${year}`; case 'yyyy/MM/dd': return `${year}/${month}/${day}`; case 'HH:mm': return `${hours}:${minutes}`; default: return date.toLocaleDateString(); } }; const DateNavigation = memo(({ nextMonth, prevMonth, changeMonth, changeYear, minDate, maxDate, currentDate, years, hasYearAndMonthSelector, }) => { const nextMonthDisabled = maxDate ? new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1) > maxDate : false; const prevMonthDisabled = minDate ? new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1) < minDate : false; const monthOptions = useMemo(() => { if (currentDate && !isNaN(currentDate.getTime())) { return Array.from({ length: 12 }, (_, i) => ({ label: new Date(currentDate.getFullYear(), i, 1).toLocaleString('default', { month: 'long' }), value: i.toString(), })); } return []; }, [currentDate]); const yearOptions = useMemo(() => { return years.map(year => ({ label: year.toString(), value: year.toString() })); }, [years]); return (_jsxs("div", { className: 'dry-date-picker__nav', children: [!hasYearAndMonthSelector && (_jsxs("span", { children: [_jsx("span", { children: new Date(currentDate).toLocaleString('default', { month: 'long', }) }), _jsx("span", { children: currentDate.getFullYear().toString() })] })), _jsxs("div", { className: 'dry-date-picker__nav-input-wrapper', children: [_jsx(Button, { width: 'max-content', disabled: prevMonthDisabled, icon: _jsx(Icon, { color: '#667085', Path: ChevronLeft, size: IconSizes.SMALL }), buttonType: 'icon', size: 'small', onClick: prevMonth }), hasYearAndMonthSelector && (_jsxs(_Fragment, { children: [_jsx(DropdownSelect, { name: 'date picker month select', value: currentDate.getMonth().toString(), options: monthOptions, onChange: (e) => changeMonth(e.target.value) }), _jsx(DropdownSelect, { name: 'date picker year select', value: currentDate.getFullYear().toString(), options: yearOptions, onChange: (e) => changeYear(e.target.value) })] })), _jsx(Button, { width: 'max-content', disabled: nextMonthDisabled, icon: _jsx(Icon, { Path: ChevronRight, size: IconSizes.SMALL }), buttonType: 'icon', size: 'small', onClick: nextMonth })] })] })); }); const DateGrid = memo(({ selectedDate, daysInMonth, firstDayOfMonth, selectDate, disabledDates, rangeLimit, daysUntilBookingAllowed, holidays, weekStartDay, minDate, maxDate, currentDate, customDateRenderer, selectedDates, onDateHover, hoveredDate, dateRangeSelection, startDate, endDate, }) => { const dayLabels = weekStartDay === 'Sun' ? ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] : ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; const offset = weekStartDay === 'Sun' ? firstDayOfMonth : (firstDayOfMonth || 7) - 1; const isHoliday = (date) => { return holidays.some((h) => h.toDateString() === date.toDateString()); }; const isDateInRange = (date) => { if (startDate && endDate) { return date >= startDate && date <= endDate; } return false; }; const isDateHovered = (date) => { return hoveredDate?.toDateString() === date.toDateString(); }; const isDateSelected = (date) => { return selectedDates?.some((d) => d.toDateString() === date.toDateString()); }; // TODO-p1: DRY this up with the isDateDisabled function in the DatePicker component below const isDateDisabled = (date, disabledDates = [], rangeLimit, daysUntilBookingAllowed, minDate, maxDate, disableWeekends) => { const isInRange = rangeLimit ? generateUpdatedDates(rangeLimit, daysUntilBookingAllowed)?.some((d) => d.toDateString() === date.toDateString()) : true; const currentDate = new Date(); currentDate.setHours(0, 0, 0, 0); // Set to start of the day let allowedBookingDate = null; if (daysUntilBookingAllowed) { allowedBookingDate = new Date(currentDate); allowedBookingDate.setDate(currentDate.getDate() + daysUntilBookingAllowed); } const isBeforeBookingAllowed = daysUntilBookingAllowed ? date.getTime() < allowedBookingDate.getTime() : false; return (disabledDates.some((d) => d.toDateString() === date.toDateString()) || (minDate ? date < minDate : false) || (maxDate ? date > maxDate : false) || (disableWeekends ? date.getDay() === 0 || date.getDay() === 6 : false) || (rangeLimit && startDate && endDate ? Math.abs(startDate.getTime() - endDate.getTime()) / (1000 * 60 * 60 * 24) >= rangeLimit : false) || isBeforeBookingAllowed || !isInRange); }; return (_jsxs("div", { className: 'dry-date-picker__date-grid', children: [dayLabels.map((label, index) => (_jsx("div", { className: 'dry-date-picker__day dry-date-picker__day--label', children: _jsx("span", { className: 'dry-date-picker__day-text', children: label }) }, index))), Array.from({ length: offset }, (_, i) => i).map((_, index) => (_jsx("div", { className: 'dry-date-picker__day dry-date-picker__day--empty' }, index))), Array.from({ length: daysInMonth }, (_, i) => i + 1).map((day) => { const thisDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), day); const isDisabled = isDateDisabled(thisDate, disabledDates, rangeLimit, daysUntilBookingAllowed, minDate, maxDate); const isToday = day === new Date().getDate() && currentDate.getMonth() === new Date().getMonth() && currentDate.getFullYear() === new Date().getFullYear(); const cellContent = customDateRenderer ? customDateRenderer(thisDate) : day; const isInRange = dateRangeSelection && startDate && endDate ? thisDate >= startDate && thisDate <= endDate : false; const cellClassNames = [ 'dry-date-picker__day', isDisabled ? 'dry-date-picker__day--disabled' : '', isInRange ? 'dry-date-picker__day--in-range' : '', isHoliday(thisDate) ? 'dry-date-picker__day--holiday' : '', isToday ? 'dry-date-picker__day--today' : '', isDateSelected(thisDate) ? 'dry-date-picker__day--selected' : '', isDateInRange(thisDate) ? 'dry-date-picker__day--in-range' : '', isDateHovered(thisDate) ? 'dry-date-picker__day--hovered' : '', isDateHovered(thisDate) && isDisabled ? 'dry-date-picker__day--disabledHovered' : '', selectedDate && thisDate.toDateString() === selectedDate.toDateString() ? 'dry-date-picker__day--current' : '', ].join(' '); return (_jsx("div", { className: cellClassNames, onClick: () => !isDisabled && selectDate(day), onMouseOver: () => onDateHover?.(thisDate), children: _jsxs("span", { className: 'dry-date-picker__day-text', children: [" ", cellContent] }) }, day)); })] })); }); const DatePicker = forwardRef(({ initialDate, dateFormat = 'MM/dd/yyyy', localization, disabledDates = [], rangeLimit, daysUntilBookingAllowed, holidays = [], onDateChange, onMonthChange, buttonText = 'Select Date', buttonIcon, position = 'bottom', isShowing: isShowingProp = false, selectedDate: selectedDateProp = null, onShow, onHide, weekStartDay = 'Sun', minDate, maxDate, disableWeekends = false, customDateRenderer, customStyles, multiSelect = false, selectedDates: selectedDatesProp = [], onDateRangeChange, additionalClassNames = '', onPrevMonthClick, onNextMonthClick, showWithoutButton = false, hasTimeSelector = false, hasYearAndMonthSelector = true, }, ref) => { const [isShowing, setIsShowing] = useState(isShowingProp); const [currentDate, setCurrentDate] = useState(initialDate || new Date()); const [selectedDate, setSelectedDate] = useState(selectedDateProp); const [years, setYears] = useState([]); const [hoveredDate, setHoveredDate] = useState(null); const [dateRangeSelection, setDateRangeSelection] = useState(false); const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); const [selectedDates, setSelectedDates] = useState(selectedDatesProp); const [time, setTime] = useState({ hours: 0, minutes: 0, }); const isDateDisabled = (date, disabledDates = [], rangeLimit, daysUntilBookingAllowed, minDate, maxDate, disableWeekends) => { const isInRange = rangeLimit ? generateUpdatedDates(rangeLimit, daysUntilBookingAllowed)?.some((d) => d.toDateString() === date.toDateString()) : true; const currentDate = new Date(); currentDate.setHours(0, 0, 0, 0); // Set to start of the day let allowedBookingDate = null; if (daysUntilBookingAllowed) { allowedBookingDate = new Date(currentDate); allowedBookingDate.setDate(currentDate.getDate() + daysUntilBookingAllowed); } const isBeforeBookingAllowed = daysUntilBookingAllowed ? date.getTime() < allowedBookingDate.getTime() : false; return (disabledDates.some((d) => d.toDateString() === date.toDateString()) || (minDate ? date < minDate : false) || (maxDate ? date > maxDate : false) || (disableWeekends ? date.getDay() === 0 || date.getDay() === 6 : false) || (rangeLimit && startDate && endDate ? Math.abs(startDate.getTime() - endDate.getTime()) / (1000 * 60 * 60 * 24) >= rangeLimit : false) || isBeforeBookingAllowed || !isInRange); }; // Refs const datepickerRef = useRef(null); // Initialize years for the dropdown useEffect(() => { const currentYear = new Date().getFullYear(); setYears(Array.from({ length: 21 }, (_, i) => currentYear - 10 + i)); }, []); // Localize the date string if a localization prop is provided useEffect(() => { if (localization) { currentDate.toLocaleDateString(localization); } }, [currentDate, localization]); // If the selectedDate prop changes, update the state useEffect(() => { setSelectedDate(selectedDateProp); }, [selectedDateProp]); // If the isShowing prop changes, update the state useEffect(() => { setIsShowing(isShowingProp); }, [isShowingProp]); // TODO-p1: this is running on every render, which is not ideal - find a better way to do this // If the selectedDates prop changes, update the state // useEffect(() => { // setSelectedDates(selectedDatesProp); // }, [selectedDatesProp]); // Function to toggle multiple date selection const toggleDate = useCallback((date) => { const index = selectedDates.findIndex((d) => d.toDateString() === date.toDateString()); if (index > -1) { setSelectedDates((prev) => { const newSelectedDates = [...prev]; newSelectedDates.splice(index, 1); return newSelectedDates; }); } else { setSelectedDates((prev) => [...prev, date]); } }, [selectedDates]); useEffect(() => { const currentYear = new Date().getFullYear(); setYears(Array.from({ length: 21 }, (_, i) => currentYear - 10 + i)); }, []); // Event Handlers const nextMonth = useCallback(() => { const newDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1); setCurrentDate(newDate); onMonthChange?.(newDate); onNextMonthClick?.(); }, [currentDate, onMonthChange, onNextMonthClick]); const prevMonth = useCallback(() => { const newDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1); setCurrentDate(newDate); onMonthChange?.(newDate); onPrevMonthClick?.(); }, [currentDate, onMonthChange, onPrevMonthClick]); const changeMonth = useCallback((value) => { const monthIndex = parseInt(value, 10); const year = currentDate.getFullYear(); const newDate = new Date(year, monthIndex, 1); setCurrentDate(newDate); onMonthChange?.(newDate); }, [currentDate, onMonthChange]); const changeYear = useCallback((value) => { const newDate = new Date(parseInt(value, 10), currentDate.getMonth(), 1); setCurrentDate(newDate); onMonthChange?.(newDate); }, [currentDate, onMonthChange]); const toggleDatePicker = () => { setIsShowing(!isShowing); if (isShowing) { onHide?.(); } else { onShow?.(); } }; const handleKeyDown = useCallback((event) => { if (event.key === 'Escape') { setIsShowing(false); onHide?.(); } }, [onHide]); const handleTouchStart = useCallback((event) => { // Implement touch event handling here }, []); const handleDateHover = useCallback((date) => { setHoveredDate(date); }, []); // Memoized components and variables const daysInMonth = useMemo(() => new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate(), [currentDate]); const firstDayOfMonth = useMemo(() => new Date(currentDate.getFullYear(), currentDate.getMonth(), 1).getDay(), [currentDate]); const buttonTextDisplay = useMemo(() => { if (selectedDate) { return formatDate(selectedDate, dateFormat); } return buttonText; }, [selectedDate, dateFormat, buttonText]); useEffect(() => { const handleClickOutside = (event) => { if (datepickerRef.current && !datepickerRef.current.contains(event.target)) { setIsShowing(false); onHide?.(); } }; document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [setIsShowing, onHide]); // Function to handle date click const handleDateClick = (day) => { const newDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), day, time.hours, time.minutes); if (!isDateDisabled(newDate, disabledDates, rangeLimit, daysUntilBookingAllowed, minDate, maxDate, disableWeekends)) { if (multiSelect) { toggleDate(newDate); } else if (dateRangeSelection) { if (startDate && newDate >= startDate) { setEndDate(new Date(newDate)); setDateRangeSelection(false); onDateRangeChange?.(startDate, newDate); // Notify parent } else { setStartDate(new Date(newDate)); setDateRangeSelection(true); setEndDate(null); onDateRangeChange?.(newDate, null); // Notify parent } } else { setSelectedDate(newDate); onDateChange?.(newDate); } } }; return (_jsxs("div", { ref: datepickerRef, className: `date-picker date-picker--${position} ${additionalClassNames}`, onKeyDown: handleKeyDown, onTouchStart: handleTouchStart, style: customStyles, tabIndex: 0, children: [!showWithoutButton && (_jsx(Button, { text: buttonTextDisplay, buttonType: 'outline-transparent', size: 'large', onClick: toggleDatePicker, icon: buttonIcon || (_jsx("svg", { xmlns: 'http://www.w3.org/2000/svg', width: '20', height: '20', viewBox: '0 0 20 20', fill: 'none', children: _jsx("path", { d: 'M17.5 8.33268H2.5M13.3333 1.66602V4.99935M6.66667 1.66602V4.99935M6.5 18.3327H13.5C14.9001 18.3327 15.6002 18.3327 16.135 18.0602C16.6054 17.8205 16.9878 17.4381 17.2275 16.9677C17.5 16.4329 17.5 15.7328 17.5 14.3327V7.33268C17.5 5.93255 17.5 5.23249 17.2275 4.69771C16.9878 4.2273 16.6054 3.84485 16.135 3.60517C15.6002 3.33268 14.9001 3.33268 13.5 3.33268H6.5C5.09987 3.33268 4.3998 3.33268 3.86502 3.60517C3.39462 3.84485 3.01217 4.2273 2.77248 4.69771C2.5 5.23249 2.5 5.93255 2.5 7.33268V14.3327C2.5 15.7328 2.5 16.4329 2.77248 16.9677C3.01217 17.4381 3.39462 17.8205 3.86502 18.0602C4.3998 18.3327 5.09987 18.3327 6.5 18.3327Z', stroke: '#344054', strokeWidth: '1.66667', strokeLinecap: 'round', strokeLinejoin: 'round' }) })) })), isShowing || (_jsxs("div", { className: classnames({ 'dry-date-picker__container-display': showWithoutButton, 'dry-date-picker__container': !showWithoutButton, }), children: [_jsx(DateNavigation, { nextMonth: nextMonth, prevMonth: prevMonth, changeMonth: changeMonth, changeYear: changeYear, minDate: minDate, maxDate: maxDate, currentDate: currentDate, years: years, hasYearAndMonthSelector: hasYearAndMonthSelector }), hasTimeSelector && (_jsxs("div", { className: 'dry-date-picker__time-selector', children: [_jsx("input", { type: 'number', value: time.hours, onChange: (e) => setTime({ ...time, hours: parseInt(e.target.value, 10) }), min: 0, max: 23, className: 'dry-date-picker__time-input' }), ":", _jsx("input", { type: 'number', value: time.minutes, onChange: (e) => setTime({ ...time, minutes: parseInt(e.target.value, 10) }), min: 0, max: 59, className: 'dry-date-picker__time-input' })] })), _jsx(DateGrid, { selectedDate: selectedDate, daysInMonth: daysInMonth, firstDayOfMonth: firstDayOfMonth, selectDate: handleDateClick, disabledDates: disabledDates, rangeLimit: rangeLimit, daysUntilBookingAllowed: daysUntilBookingAllowed, holidays: holidays, weekStartDay: weekStartDay, minDate: minDate, maxDate: maxDate, currentDate: currentDate, customDateRenderer: customDateRenderer, selectedDates: selectedDates, onDateHover: handleDateHover, hoveredDate: hoveredDate, dateRangeSelection: dateRangeSelection, startDate: startDate, endDate: endDate })] }))] })); }); export default DatePicker; //# sourceMappingURL=DatePicker.js.map