UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

551 lines (550 loc) 17.8 kB
"use client"; import { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; import { clsx } from 'clsx'; import useMountEffect from "../../shared/helpers/useMountEffect.js"; import { format, addMonths, addWeeks, addDays, isSameDay, isSameMonth, startOfDay, differenceInCalendarDays, differenceInMonths, setDate } from 'date-fns'; import { isDisabled, makeDayObject, toRange, getWeek, dayOffset, getCalendar } from "./DatePickerCalc.js"; import Button from "../button/Button.js"; import DatePickerContext from "./DatePickerContext.js"; import useIsomorphicLayoutEffect from "../../shared/helpers/useIsomorphicLayoutEffect.js"; import { DatePickerCalendarNav } from "./DatePickerCalendarNavigator.js"; import { formatDate } from "../date-format/DateFormatUtils.js"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; const datePickerCalendarDefaultProps = { hideNavigation: false, hideDays: false, onlyMonth: false, hideMonthLabel: false, hideNextMonthWeek: false, rtl: false, resetDate: true }; const arrowKeys = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']; const keysToHandle = ['Enter', 'Space', ...arrowKeys]; function DatePickerCalendar(restOfProps) { const props = { ...datePickerCalendarDefaultProps, ...restOfProps }; const { updateDates, setHasClickedCalendarDay, startDate, endDate, maxDate, minDate, startMonth, endMonth, hoverDate, setHoverDate, setSubmittedDates, views, setViews, props: { onDaysRender, yearNavigation }, translation: { DatePicker: { firstDay: defaultFirstDayOfWeek, selectedMonth } } } = useContext(DatePickerContext); const { id, nr, rtl, month, isRange, firstDayOfWeek = defaultFirstDayOfWeek, hideNavigation, locale, hideDays, onSelect, onKeyDown, resetDate, hideNextMonthWeek, onlyMonth, hideMonthLabel } = props; const tableRef = useRef(null); const days = useRef({}); const cache = useRef({}); const focusedDateRef = useRef(null); const pendingFocusDateRef = useRef(null); const currentDatesRef = useRef({ startDate, endDate, startMonth, endMonth }); useMountEffect(() => { setSubmittedDates({ startDate, endDate }); }); useEffect(() => { currentDatesRef.current = { startDate, endDate, startMonth, endMonth }; }, [endDate, endMonth, startDate, startMonth]); const onMouseLeaveHandler = useCallback(() => { setHoverDate(undefined); }, [setHoverDate]); const focusDayButton = useCallback(date => { if (tableRef.current) { const dateStr = format(date, 'yyyy-MM-dd'); const button = tableRef.current.querySelector(`td[data-date="${dateStr}"] button`); button?.focus({ preventScroll: true }); } }, []); const focusEndCalendar = useCallback(() => { const viewsContainer = tableRef.current?.closest('.dnb-date-picker__views'); const endTable = viewsContainer?.querySelectorAll('table')?.[1]; const endDateValue = currentDatesRef.current.endDate; const endButton = endDateValue ? endTable?.querySelector(`td[data-date="${format(endDateValue, 'yyyy-MM-dd')}"] button`) : null; if (endButton) { endButton.focus({ preventScroll: true }); } else { ; endTable?.focus({ preventScroll: true }); } }, []); useIsomorphicLayoutEffect(() => { if (pendingFocusDateRef.current) { focusDayButton(pendingFocusDateRef.current); pendingFocusDateRef.current = null; } }, [views, focusDayButton]); const callOnSelect = useCallback(event => { onSelect?.(event); }, [onSelect]); const getDays = useCallback(month => { let daysFromCalendar = getCalendar(month || new Date(), dayOffset(firstDayOfWeek), { onlyMonth, hideNextMonthWeek }).map(date => makeDayObject(date, { startDate, endDate, hoverDate, minDate, maxDate, month })); if (onDaysRender) { const changedDays = onDaysRender(daysFromCalendar, nr); if (Array.isArray(changedDays)) { daysFromCalendar = changedDays; } } days.current[format(month, 'yyyy-MM')] = daysFromCalendar; return daysFromCalendar; }, [endDate, firstDayOfWeek, hideNextMonthWeek, hoverDate, maxDate, minDate, nr, onDaysRender, onlyMonth, startDate]); const keyNavCalc = useCallback((date, keyCode) => { if (!arrowKeys.includes(keyCode)) { return date; } const dateHandler = /(ArrowLeft|ArrowRight)/g.test(keyCode) ? addDays : addWeeks; const shiftAmount = /(ArrowLeft|ArrowUp)/g.test(keyCode) ? -1 : 1; return dateHandler(date, shiftAmount); }, []); const findValid = useCallback((date, keyCode) => { if (!onDaysRender || !days.current) { return date; } const month = format(date, 'yyyy-MM'); if (!days.current[month]) { getDays(date); } if (Array.isArray(days.current[month])) { const foundDate = days.current[month].find(cur => isSameDay(cur.date, date)); if (foundDate?.date && (foundDate.isDisabled || foundDate.isSelectable === false || foundDate.isInactive)) { const nextDate = keyNavCalc(foundDate.date, keyCode); return findValid(nextDate, keyCode); } if (foundDate?.date) { return foundDate.date; } } return date; }, [onDaysRender, getDays, keyNavCalc]); const hasReachedEnd = useCallback(date => isDisabled(date, minDate, maxDate), [minDate, maxDate]); const onKeyDownHandler = useCallback(event => { const pressedKey = event.code; if (typeof onKeyDown === 'function') { return onKeyDown(event, tableRef, nr); } if (!keysToHandle.includes(pressedKey)) { return; } if (tableRef.current) { const td = document.activeElement?.closest('td[data-date]'); if (td && tableRef.current.contains(td)) { const dateStr = td.getAttribute('data-date'); if (dateStr) { focusedDateRef.current = startOfDay(new Date(dateStr)); } } } if (pressedKey === 'Enter' || pressedKey === 'Space') { event.preventDefault(); if (focusedDateRef.current) { const focusedDate = focusedDateRef.current; if (!isRange) { focusedDateRef.current = null; updateDates({ startDate: startOfDay(focusedDate), endDate: startOfDay(focusedDate) }, allDates => { callOnSelect({ ...allDates, event, nr, hidePicker: true }); }); return; } setHasClickedCalendarDay(true); const { startDate: currentStart, endDate: currentEnd } = currentDatesRef.current; if (!currentStart || resetDate && currentStart && currentEnd) { updateDates({ startDate: startOfDay(focusedDate), endDate: undefined }, allDates => { callOnSelect({ ...allDates, event, nr, hidePicker: false }); }); } else { focusedDateRef.current = null; const range = toRange(currentStart, focusedDate); updateDates({ startDate: startOfDay(range.startDate), endDate: startOfDay(range.endDate) }, allDates => { callOnSelect({ ...allDates, event, nr, hidePicker: true }); }); } return; } if (isRange && nr === 0) { focusEndCalendar(); } else { callOnSelect({ event, nr, hidePicker: true }); } return; } event.preventDefault(); const currentDates = currentDatesRef.current; const dateType = !isRange || nr === 0 ? 'start' : 'end'; const currentFocused = focusedDateRef.current || currentDates[`${dateType}Date`]; const viewMonth = views.find(v => v.nr === nr)?.month; let newDate = currentFocused ? keyNavCalc(currentFocused, pressedKey) : viewMonth || (isRange && nr === 1 ? addMonths(new Date(), 1) : new Date()); if (viewMonth && !currentFocused || viewMonth && currentFocused && Math.abs(differenceInMonths(newDate, viewMonth)) > 1) { newDate = !isRange ? viewMonth : setDate(viewMonth, 1); } newDate = findValid(newDate, pressedKey); if (hasReachedEnd(newDate)) { return; } if (onlyMonth || hideNavigation) { if (viewMonth && !isSameMonth(newDate, viewMonth)) { return; } } focusedDateRef.current = newDate; if (viewMonth && !isSameMonth(newDate, viewMonth)) { if (isRange) { const otherNr = nr === 0 ? 1 : 0; const otherViewMonth = views.find(v => v.nr === otherNr)?.month; if (otherViewMonth && isSameMonth(newDate, otherViewMonth)) { const viewsContainer = tableRef.current?.closest('.dnb-date-picker__views'); const otherTable = viewsContainer?.querySelectorAll('table')?.[otherNr]; const dateStr = format(newDate, 'yyyy-MM-dd'); const button = otherTable?.querySelector(`td[data-date="${dateStr}"] button`); if (button) { focusedDateRef.current = null; button.focus({ preventScroll: true }); return; } } } tableRef.current?.focus({ preventScroll: true }); const updatedViews = views.map(view => { if (view.nr === nr) { return { ...view, month: newDate }; } return view; }); setViews(updatedViews); pendingFocusDateRef.current = newDate; } else { focusDayButton(newDate); } }, [callOnSelect, findValid, focusDayButton, focusEndCalendar, hasReachedEnd, onKeyDown, updateDates, hideNavigation, isRange, keyNavCalc, nr, onlyMonth, resetDate, setHasClickedCalendarDay, views, setViews]); const cacheKey = useMemo(() => { return [nr, month, firstDayOfWeek, onlyMonth, hideNextMonthWeek, startDate, endDate, hoverDate, maxDate, minDate].join('|'); }, [nr, month, firstDayOfWeek, onlyMonth, hideNextMonthWeek, startDate, endDate, hoverDate, maxDate, minDate]); const weekDays = useMemo(() => { if (cache.current[cacheKey]) { return cache.current[cacheKey]; } let count = 0; const days = getDays(month).reduce((acc, cur, i) => { acc[count] = acc[count] || []; acc[count].push(cur); if (i % 7 === 6) { count++; } return acc; }, {}); cache.current[cacheKey] = Object.values(days); return cache.current[cacheKey]; }, [cacheKey, getDays, month]); return _jsxs("div", { className: clsx('dnb-date-picker__calendar', rtl && 'rtl'), lang: locale, children: [!hideNavigation && !onlyMonth && _jsxs("div", { className: "dnb-date-picker__header", children: [_jsx(DatePickerCalendarNav, { type: yearNavigation ? 'month' : 'both', id: id, nr: nr, date: month, locale: locale }), yearNavigation && _jsx(DatePickerCalendarNav, { type: "year", id: id, nr: nr, date: month, locale: locale })] }), onlyMonth && !hideMonthLabel && _jsx("div", { className: "dnb-date-picker__header dnb-date-picker__header--only-month-label", children: _jsx("label", { id: `${id}--title`, className: "dnb-date-picker__header__title dnb-no-focus", title: selectedMonth.replace(/%s/, formatDate(month, { locale, options: { month: 'long' } })), tabIndex: -1, children: formatDate(month, { locale, options: { month: 'long' } }) }) }), _jsxs("table", { role: "grid", className: "dnb-no-focus", tabIndex: 0, "aria-labelledby": onlyMonth && hideMonthLabel ? undefined : `${id}--title`, onKeyDown: onKeyDownHandler, onMouseLeave: onMouseLeaveHandler, ref: tableRef, children: [!hideDays && !onlyMonth && _jsx("thead", { "aria-hidden": true, children: _jsx("tr", { role: "row", className: "dnb-date-picker__labels", children: getWeek(dayOffset(firstDayOfWeek)).map(day => _jsx("th", { role: "columnheader", scope: "col", className: "dnb-date-picker__labels__day", "aria-label": formatDate(day, { locale, options: { weekday: 'long' } }), children: formatDate(day, { locale, options: { weekday: 'short' } }).substring(0, 2) }, day.toISOString())) }) }), _jsx("tbody", { children: weekDays.map((week, i) => { return _jsx("tr", { role: "row", className: "dnb-date-picker__days", children: week.map((day, i) => { const title = formatDate(day.date, { locale, options: { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' } }); const handleAsDisabled = day.isLastMonth || day.isNextMonth || day.isDisabled || day.isInactive; const dateType = day.isStartDate ? 'start' : day.isEndDate ? 'end' : undefined; const isSelectedDate = nr === 0 ? day.isStartDate : day.isEndDate; const paramsCell = { tabIndex: -1, ...(dateType && { id: `${id}--button-${dateType}` }), ...(isSelectedDate && { ['aria-selected']: true }) }; const paramsButton = { ...(isSelectedDate && { ['aria-current']: 'date' }) }; return _jsx("td", { role: "gridcell", "data-date": format(day.date, 'yyyy-MM-dd'), className: clsx('dnb-date-picker__day', 'dnb-no-focus', buildDayClassNames(day)), ...paramsCell, children: _jsx(Button, { size: "medium", variant: "secondary", text: day.date.getDate(), bounding: true, disabled: handleAsDisabled, tabIndex: handleAsDisabled ? 0 : -1, "aria-disabled": handleAsDisabled, "aria-label": title, ...paramsButton, onClick: handleAsDisabled ? undefined : ({ event }) => onSelectRange({ day, isRange, startDate, endDate, resetDate, event, setHasClickedCalendarDay, onSelect: state => { updateDates(state, dates => callOnSelect({ ...dates, event, nr, hidePicker: !isRange })); } }), onMouseOver: handleAsDisabled ? undefined : () => onHoverDay({ day, hoverDate, setHoverDate }), onFocus: handleAsDisabled ? undefined : () => onHoverDay({ day, hoverDate, setHoverDate }) }) }, 'day' + i); }) }, 'week' + i); }) })] })] }); } export default DatePickerCalendar; function onSelectRange({ day, isRange, startDate, endDate, onSelect, resetDate, event, setHasClickedCalendarDay }) { if (!isRange) { return onSelect({ startDate: startOfDay(day.date), endDate: startOfDay(day.date), event }); } setHasClickedCalendarDay(true); if (!startDate || resetDate && startDate && endDate) { return onSelect({ startDate: startOfDay(day.date), endDate: undefined, event }); } const daysToStartDate = Math.abs(differenceInCalendarDays(startDate, day.date)); const daysToEndDate = Math.abs(differenceInCalendarDays(endDate, day.date)); const range = toRange(endDate && !resetDate && daysToStartDate < daysToEndDate ? endDate : startDate, day.date); return onSelect({ startDate: startOfDay(range.startDate), endDate: startOfDay(range.endDate), event }); } function onHoverDay({ day, hoverDate, setHoverDate }) { if (day.isStartDate || day.isEndDate) { return; } if (!isSameDay(day.date, hoverDate)) { setHoverDate?.(day.date); } } function buildDayClassNames(day) { return clsx({ 'dnb-date-picker__day--start-date': day.isStartDate, 'dnb-date-picker__day--end-date': day.isEndDate, 'dnb-date-picker__day--preview': day.isPreview, 'dnb-date-picker__day--within-selection': day.isWithinSelection, 'dnb-date-picker__day--selectable': day.isSelectable, 'dnb-date-picker__day--inactive': day.isInactive, 'dnb-date-picker__day--disabled': day.isDisabled, 'dnb-date-picker__day--today': day.isToday }, day.className); } //# sourceMappingURL=DatePickerCalendar.js.map