@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
554 lines (553 loc) • 18.1 kB
JavaScript
"use client";
import _includesInstanceProperty from "core-js-pure/stable/instance/includes.js";
import _pushInstanceProperty from "core-js-pure/stable/instance/push.js";
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 (!_includesInstanceProperty(arrowKeys).call(arrowKeys, 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 (!_includesInstanceProperty(keysToHandle).call(keysToHandle, 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) => {
var _context;
acc[count] = acc[count] || [];
_pushInstanceProperty(_context = acc[count]).call(_context, 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