UNPKG

@itwin/itwinui-react

Version:

A react component library for iTwinUI

574 lines (573 loc) 19.2 kB
import cx from 'classnames'; import * as React from 'react'; import { SvgChevronLeft, SvgChevronRight, SvgChevronLeftDouble, SvgChevronRightDouble, isBefore, Box, useId, useLayoutEffect, useWarningLogger, } from '../../utils/index.js'; import { IconButton } from '../Buttons/IconButton.js'; import { TimePicker } from '../TimePicker/TimePicker.js'; import { PopoverInitialFocusContext } from '../Popover/Popover.js'; let isSameDay = (a, b) => a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); let isInDateRange = (date, startDate, endDate) => { if (!date || !startDate || !endDate) return false; let minDate = new Date(startDate); let maxDate = new Date(endDate); let testDate = new Date(date); testDate && testDate.setHours(0, 0, 0, 0); minDate && minDate.setHours(0, 0, 0, 0); maxDate && maxDate.setHours(0, 0, 0, 0); return testDate > minDate && testDate < maxDate; }; let isSingleOnChange = (onChange, enableRangeSelect) => !enableRangeSelect; let defaultMonths = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; let defaultShortDays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']; let defaultLongDays = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', ]; export const generateLocalizedStrings = (locale) => { let shortWeekDayFormatter = new Intl.DateTimeFormat(locale, { weekday: 'short', }); let longWeekDayFormatter = new Intl.DateTimeFormat(locale, { weekday: 'long', }); let monthFormatter = new Intl.DateTimeFormat(locale, { month: 'long', }); let months = [ monthFormatter.format(new Date(2020, 0, 1)), monthFormatter.format(new Date(2020, 1, 1)), monthFormatter.format(new Date(2020, 2, 1)), monthFormatter.format(new Date(2020, 3, 1)), monthFormatter.format(new Date(2020, 4, 1)), monthFormatter.format(new Date(2020, 5, 1)), monthFormatter.format(new Date(2020, 6, 1)), monthFormatter.format(new Date(2020, 7, 1)), monthFormatter.format(new Date(2020, 8, 1)), monthFormatter.format(new Date(2020, 9, 1)), monthFormatter.format(new Date(2020, 10, 1)), monthFormatter.format(new Date(2020, 11, 1)), ]; let days = [ longWeekDayFormatter.format(new Date(2020, 10, 1)), longWeekDayFormatter.format(new Date(2020, 10, 2)), longWeekDayFormatter.format(new Date(2020, 10, 3)), longWeekDayFormatter.format(new Date(2020, 10, 4)), longWeekDayFormatter.format(new Date(2020, 10, 5)), longWeekDayFormatter.format(new Date(2020, 10, 6)), longWeekDayFormatter.format(new Date(2020, 10, 7)), ]; let shortDays = [ shortWeekDayFormatter.format(new Date(2020, 10, 1)).slice(0, 2), shortWeekDayFormatter.format(new Date(2020, 10, 2)).slice(0, 2), shortWeekDayFormatter.format(new Date(2020, 10, 3)).slice(0, 2), shortWeekDayFormatter.format(new Date(2020, 10, 4)).slice(0, 2), shortWeekDayFormatter.format(new Date(2020, 10, 5)).slice(0, 2), shortWeekDayFormatter.format(new Date(2020, 10, 6)).slice(0, 2), shortWeekDayFormatter.format(new Date(2020, 10, 7)).slice(0, 2), ]; return { months, shortDays, days, }; }; export const DatePicker = React.forwardRef((props, forwardedRef) => { let { date, onChange, localizedNames, className, setFocus = false, showTime = false, use12Hours = false, precision, hourStep, minuteStep, secondStep, useCombinedRenderer, combinedRenderer, hourRenderer, minuteRenderer, secondRenderer, meridiemRenderer, showYearSelection = false, enableRangeSelect = false, startDate, endDate, monthYearProps, calendarProps, monthProps, weekDayProps, dayProps, weekProps, isDateDisabled, applyBackground = true, showDatesOutsideMonth = true, ...rest } = props; let logWarning = useWarningLogger(); if ('development' === process.env.NODE_ENV) { let onlyOneRangePropPassed = (startDate ? 1 : 0) + (endDate ? 1 : 0) === 1; if (enableRangeSelect && onlyOneRangePropPassed) logWarning( '`DatePicker` with `enableRangeSelect` needs *both* `startDate` and `endDate` to either be `Date` or `undefined`. Passing `Date` to just one of them is not allowed.', ); } let monthNames = localizedNames?.months ?? defaultMonths; let shortDays = localizedNames?.shortDays ?? defaultShortDays; let longDays = localizedNames?.days ?? defaultLongDays; let [selectedDay, setSelectedDay] = React.useState(date); let [selectedStartDay, setSelectedStartDay] = React.useState(startDate); let [selectedEndDay, setSelectedEndDay] = React.useState(endDate); let [focusedDay, setFocusedDay] = React.useState( selectedStartDay ?? selectedDay ?? new Date(), ); let [displayedMonthIndex, setDisplayedMonthIndex] = React.useState( selectedStartDay?.getMonth() ?? selectedDay?.getMonth() ?? new Date().getMonth(), ); let [displayedYear, setDisplayedYear] = React.useState( selectedStartDay?.getFullYear() ?? selectedDay?.getFullYear() ?? new Date().getFullYear(), ); let [isSelectingStartDate, setIsSelectingStartDate] = React.useState(true); let needFocus = React.useRef(setFocus); React.useEffect(() => { if (needFocus.current) needFocus.current = false; }); let setMonthAndYear = React.useCallback((newMonth, newYear) => { setDisplayedMonthIndex(newMonth); setDisplayedYear(newYear); }, []); React.useEffect(() => { let currentDate = new Date(); setSelectedDay(date); setSelectedStartDay(startDate); setSelectedEndDay(endDate); if (!enableRangeSelect) setFocusedDay(date ?? currentDate); setMonthAndYear( startDate?.getMonth() ?? date?.getMonth() ?? currentDate.getMonth(), startDate?.getFullYear() ?? date?.getFullYear() ?? currentDate.getFullYear(), ); }, [date, setMonthAndYear, startDate, endDate, enableRangeSelect]); let popoverInitialFocusContext = React.useContext(PopoverInitialFocusContext); useLayoutEffect(() => { if (setFocus && popoverInitialFocusContext) popoverInitialFocusContext.setInitialFocus(-1); }, [popoverInitialFocusContext, setFocus]); let days = React.useMemo(() => { let offsetToFirst = new Date( displayedYear, displayedMonthIndex, 1, ).getDay(); if (0 === offsetToFirst && showDatesOutsideMonth) offsetToFirst = 7; let daysInMonth = []; for (let i = 1; i <= 42; i++) { let adjustedDay = i - offsetToFirst; daysInMonth.push( new Date(displayedYear, displayedMonthIndex, adjustedDay), ); } return daysInMonth; }, [displayedMonthIndex, displayedYear, showDatesOutsideMonth]); let weeks = React.useMemo(() => { let weeksInMonth = []; let weekCount = Math.ceil(days.length / 7); for (let i = 0; i < weekCount; i++) weeksInMonth.push(days.slice(7 * i, (i + 1) * 7)); return weeksInMonth; }, [days]); let getNewFocusedDate = (newYear, newMonth) => { let currentDate = selectedStartDay ?? selectedDay ?? new Date(); let newDate = new Date( newYear, newMonth, currentDate.getDate(), currentDate.getHours(), currentDate.getMinutes(), currentDate.getSeconds(), ); return newDate; }; let handleMoveToPreviousYear = () => { let newYear = displayedYear - 1; setMonthAndYear(displayedMonthIndex, newYear); setFocusedDay(getNewFocusedDate(newYear, displayedMonthIndex)); }; let handleMoveToNextYear = () => { let newYear = displayedYear + 1; setMonthAndYear(displayedMonthIndex, newYear); setFocusedDay(getNewFocusedDate(newYear, displayedMonthIndex)); }; let handleMoveToPreviousMonth = () => { let newMonth = 0 !== displayedMonthIndex ? displayedMonthIndex - 1 : 11; let newYear = 0 !== displayedMonthIndex ? displayedYear : displayedYear - 1; setMonthAndYear(newMonth, newYear); setFocusedDay(getNewFocusedDate(newYear, newMonth)); }; let handleMoveToNextMonth = () => { let newMonth = 11 !== displayedMonthIndex ? displayedMonthIndex + 1 : 0; let newYear = 11 !== displayedMonthIndex ? displayedYear : displayedYear + 1; setMonthAndYear(newMonth, newYear); setFocusedDay(getNewFocusedDate(newYear, newMonth)); }; let onDayClick = (day) => { if (enableRangeSelect) if (isSelectingStartDate) { if (day.getMonth() !== selectedStartDay?.getMonth()) setMonthAndYear(day.getMonth(), day.getFullYear()); let currentStartDate = selectedStartDay ?? new Date(); let newStartDate = new Date( day.getFullYear(), day.getMonth(), day.getDate(), currentStartDate.getHours(), currentStartDate.getMinutes(), currentStartDate.getSeconds(), ); setSelectedStartDay(newStartDate); setFocusedDay(newStartDate); if (isBefore(newStartDate, selectedEndDay)) selectedEndDay && onChange?.(newStartDate, selectedEndDay); else { setSelectedEndDay(newStartDate); onChange?.(newStartDate, newStartDate); } setIsSelectingStartDate(false); } else { if (day.getMonth() !== selectedEndDay?.getMonth()) setMonthAndYear(day.getMonth(), day.getFullYear()); let currentEndDate = selectedEndDay ?? new Date(); let newEndDate = new Date( day.getFullYear(), day.getMonth(), day.getDate(), currentEndDate.getHours(), currentEndDate.getMinutes(), currentEndDate.getSeconds(), ); setFocusedDay(newEndDate); if (isBefore(newEndDate, selectedStartDay)) { setSelectedStartDay(newEndDate); selectedEndDay && onChange?.(newEndDate, selectedEndDay); } else { setSelectedEndDay(newEndDate); selectedStartDay && onChange?.(selectedStartDay, newEndDate); setIsSelectingStartDate(true); } } else { if (day.getMonth() !== selectedDay?.getMonth()) setMonthAndYear(day.getMonth(), day.getFullYear()); let currentDate = selectedDay ?? new Date(); let newDate = new Date( day.getFullYear(), day.getMonth(), day.getDate(), currentDate.getHours(), currentDate.getMinutes(), currentDate.getSeconds(), ); setSelectedDay(newDate); setFocusedDay(newDate); isSingleOnChange(onChange, enableRangeSelect) && onChange?.(newDate); } }; let handleCalendarKeyDown = (event) => { if (event.altKey) return; if (!focusedDay) return; let adjustedFocusedDay = new Date(focusedDay); switch (event.key) { case 'ArrowDown': adjustedFocusedDay.setDate(focusedDay.getDate() + 7); if (adjustedFocusedDay.getMonth() !== displayedMonthIndex) handleMoveToNextMonth(); setFocusedDay(adjustedFocusedDay); needFocus.current = true; event.preventDefault(); break; case 'ArrowUp': adjustedFocusedDay.setDate(focusedDay.getDate() - 7); if (adjustedFocusedDay.getMonth() !== displayedMonthIndex) handleMoveToPreviousMonth(); setFocusedDay(adjustedFocusedDay); needFocus.current = true; event.preventDefault(); break; case 'ArrowLeft': adjustedFocusedDay.setDate(focusedDay.getDate() - 1); if (adjustedFocusedDay.getMonth() !== displayedMonthIndex) handleMoveToPreviousMonth(); setFocusedDay(adjustedFocusedDay); needFocus.current = true; event.preventDefault(); break; case 'ArrowRight': adjustedFocusedDay.setDate(focusedDay.getDate() + 1); if (adjustedFocusedDay.getMonth() !== displayedMonthIndex) handleMoveToNextMonth(); setFocusedDay(adjustedFocusedDay); needFocus.current = true; event.preventDefault(); break; case 'Enter': case ' ': case 'Spacebar': if (!isDateDisabled?.(focusedDay)) onDayClick(focusedDay); event.preventDefault(); break; } }; let getDayClass = (day) => { if (day.getMonth() !== displayedMonthIndex) return 'iui-calendar-day-outside-month'; let dayClass = 'iui-calendar-day'; let isSelectedDay = isSameDay(day, selectedDay) || (isSameDay(day, selectedStartDay) && isSameDay(day, selectedEndDay)); if (isSelectedDay) dayClass += '-selected'; else if (isSameDay(day, selectedStartDay)) dayClass += '-range-start'; else if (isSameDay(day, selectedEndDay)) dayClass += '-range-end'; if ( selectedStartDay && selectedEndDay && isInDateRange(day, selectedStartDay, selectedEndDay) ) dayClass += '-range'; if (isSameDay(day, new Date())) dayClass += '-today'; return dayClass; }; let dateTableId = useId(); return React.createElement( Box, { className: cx( 'iui-date-picker', { 'iui-popover-surface': applyBackground, }, className, ), ref: forwardedRef, ...rest, }, React.createElement( 'div', null, React.createElement( Box, { as: 'div', ...monthYearProps, className: cx('iui-calendar-month-year', monthYearProps?.className), }, showYearSelection && React.createElement( IconButton, { styleType: 'borderless', onClick: handleMoveToPreviousYear, 'aria-label': 'Previous year', size: 'small', }, React.createElement(SvgChevronLeftDouble, null), ), React.createElement( IconButton, { styleType: 'borderless', onClick: handleMoveToPreviousMonth, 'aria-label': 'Previous month', size: 'small', }, React.createElement(SvgChevronLeft, null), ), React.createElement( 'span', { 'aria-live': 'polite', }, React.createElement( Box, { as: 'span', id: dateTableId, title: monthNames[displayedMonthIndex], ...monthProps, className: cx('iui-calendar-month', monthProps?.className), }, monthNames[displayedMonthIndex], ), ' ', displayedYear, ), React.createElement( IconButton, { styleType: 'borderless', onClick: handleMoveToNextMonth, 'aria-label': 'Next month', size: 'small', }, React.createElement(SvgChevronRight, null), ), showYearSelection && React.createElement( IconButton, { styleType: 'borderless', onClick: handleMoveToNextYear, 'aria-label': 'Next year', size: 'small', }, React.createElement(SvgChevronRightDouble, null), ), ), React.createElement( Box, { as: 'div', ...weekDayProps, className: cx('iui-calendar-weekdays', weekDayProps?.className), }, shortDays.map((day, index) => React.createElement( 'div', { key: day, title: longDays[index], }, day, ), ), ), React.createElement( 'div', { onKeyDown: handleCalendarKeyDown, role: 'listbox', 'aria-labelledby': dateTableId, ...calendarProps, }, weeks.map((weekDays, weekIndex) => React.createElement( Box, { as: 'div', key: `week-${displayedMonthIndex}-${weekIndex}`, ...weekProps, className: cx('iui-calendar-week', weekProps?.className), }, weekDays.map((weekDay, dayIndex) => { let dateValue = weekDay.getDate(); let isDisabled = isDateDisabled?.(weekDay); let isOutsideMonth = weekDay.getMonth() !== displayedMonthIndex; if (isOutsideMonth && !showDatesOutsideMonth) return React.createElement(Box, { key: `day-${displayedMonthIndex}-${dayIndex}`, className: cx(getDayClass(weekDay), dayProps?.className), 'aria-hidden': true, }); return React.createElement( Box, { as: 'div', key: `day-${displayedMonthIndex}-${dayIndex}`, onClick: () => !isDisabled && onDayClick(weekDay), role: 'option', tabIndex: isSameDay(weekDay, focusedDay) ? 0 : -1, 'aria-disabled': isDisabled ? 'true' : void 0, ref: (element) => { if (isSameDay(weekDay, focusedDay) && needFocus.current) setTimeout(() => { element?.focus(); }); }, ...dayProps, className: cx(getDayClass(weekDay), dayProps?.className), }, dateValue, ); }), ), ), ), ), showTime && React.createElement(TimePicker, { date: selectedStartDay ?? selectedDay, use12Hours: use12Hours, precision: precision, hourStep: hourStep, minuteStep: minuteStep, secondStep: secondStep, useCombinedRenderer: useCombinedRenderer, combinedRenderer: combinedRenderer, hourRenderer: hourRenderer, minuteRenderer: minuteRenderer, secondRenderer: secondRenderer, meridiemRenderer: meridiemRenderer, onChange: (date) => isSingleOnChange(onChange, enableRangeSelect) ? onChange?.(date) : onChange?.( new Date( selectedStartDay?.getFullYear() ?? date.getFullYear(), selectedStartDay?.getMonth() ?? date.getMonth(), selectedStartDay?.getDate() ?? date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), ), new Date( selectedEndDay?.getFullYear() ?? date.getFullYear(), selectedEndDay?.getMonth() ?? date.getMonth(), selectedEndDay?.getDate() ?? date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), ), ), }), ); }); if ('development' === process.env.NODE_ENV) DatePicker.displayName = 'DatePicker';