diff --git a/.changeset/funny-jeans-smile.md b/.changeset/funny-jeans-smile.md new file mode 100644 index 0000000000..5691b1625a --- /dev/null +++ b/.changeset/funny-jeans-smile.md @@ -0,0 +1,5 @@ +--- +'@alfalab/core-components-calendar': minor +--- + +Изменено поведение пропса clickableMonth. Добавлена возможность выбирать промежуток между месяцами. Первый клик по лейблу месяца выбирает весь месяц. Второй клик на следующий месяц выбирает промежуток между этими двумя месяцами. diff --git a/packages/calendar/src/components/calendar-mobile/Component.tsx b/packages/calendar/src/components/calendar-mobile/Component.tsx index 744f4235b9..6e1a87f845 100644 --- a/packages/calendar/src/components/calendar-mobile/Component.tsx +++ b/packages/calendar/src/components/calendar-mobile/Component.tsx @@ -5,10 +5,7 @@ import { ResizeObserver as ResizeObserverPolyfill } from '@juggle/resize-observe import cn from 'classnames'; import endOfDay from 'date-fns/endOfDay'; import isAfter from 'date-fns/isAfter'; -import isSameDay from 'date-fns/isSameDay'; import isSameMonth from 'date-fns/isSameMonth'; -import isThisMonth from 'date-fns/isThisMonth'; -import lastDayOfMonth from 'date-fns/lastDayOfMonth'; import startOfDay from 'date-fns/startOfDay'; import startOfMonth from 'date-fns/startOfMonth'; @@ -26,6 +23,8 @@ import { dateArrayToHashTable, generateMonths, generateWeeks, + getMonthEndTimestamp, + getMonthStartTimestamp, isRangeValue, limitDate, monthName, @@ -180,49 +179,69 @@ export const CalendarMonthOnlyView = ({ return activeMonths.findIndex((m) => isSameMonth(date, m.date)); }, [range.value, range.selectedFrom, activeMonth, activeMonths]); - // заголовок должен становиться активным если выбран весь доступный период в месяце - const isMonthActive = (currentMonthIndex: number) => { - if (value && isRangeValue(value)) { - const { date: initialMonthDate } = activeMonths[initialMonthIndex]; - const { date: currentMonthDate } = activeMonths[currentMonthIndex]; - - const firstAvailableDayOfMonth = startOfMonth(initialMonthDate).getTime(); - /** - * последний доступный день месяца в timestamp - * представлен в виде последнего календарного дня, либо в виде текущей даты для актуального месяца - */ - const lastAvailableDayOfMonth = isThisMonth(initialMonthDate) - ? startOfDay(new Date()).getTime() - : lastDayOfMonth(initialMonthDate).getTime(); - const { dateFrom, dateTo } = value; - - if ( - dateFrom && - dateTo && - isSameMonth(initialMonthDate, currentMonthDate) && - isSameDay(firstAvailableDayOfMonth, dateFrom) && - isSameDay(lastAvailableDayOfMonth, dateTo) - ) { - return true; - } + // заголовок должен становиться активным, если выбран весь доступный период в месяце + const isMonthActive = (currentMonthIndex: number): boolean => { + if (!value || !isRangeValue(value) || !value.dateFrom || !value.dateTo) { + return false; } - return false; + const { dateFrom, dateTo } = value; + + const { date: currentMonthDate } = activeMonths[currentMonthIndex]; + const monthStartTimestamp = getMonthStartTimestamp(currentMonthDate); + const monthEndTimestamp = getMonthEndTimestamp(currentMonthDate); + + // Проверяем, что выбранный диапазон полностью покрывает месяц + return dateFrom <= monthStartTimestamp && dateTo >= monthEndTimestamp; }; const handleClickMonthLabel = (index: number) => { - if (onChange) { - const { date } = activeMonths[index]; - const firstAvailableDayOfMonth = startOfMonth(date).getTime(); - const lastAvailableDayOfMonth = isThisMonth(date) - ? startOfDay(new Date()).getTime() - : lastDayOfMonth(date).getTime(); - - if (isMonthActive(index)) { - onChange(); - } else { - onChange(firstAvailableDayOfMonth, lastAvailableDayOfMonth); - } + if (!onChange) return; + + const { date: dateActiveMonths } = activeMonths[index]; + + // Вычисляем начало и конец месяца, по которому был произведен клик + const clickedMonthStartTimestamp = getMonthStartTimestamp(dateActiveMonths); + const clickedMonthEndTimestamp = getMonthEndTimestamp(dateActiveMonths); + + // Если значение не определено или не является диапазоном, то устанавливаем новый диапазон + if (!value || !isRangeValue(value) || !value.dateFrom || !value.dateTo) { + onChange(clickedMonthStartTimestamp, clickedMonthEndTimestamp); + + return; + } + + // Выбранный диапазон дат + const { dateFrom, dateTo } = value; + const selectedRangeStartDate = new Date(dateFrom); + const selectedRangeEndDate = new Date(dateTo); + const selectedRangeStartTimestamp = getMonthStartTimestamp(selectedRangeStartDate); + const selectedRangeEndTimestamp = getMonthEndTimestamp(selectedRangeEndDate); + + // Проверяем, является ли выбранный диапазон одним и тем же месяцем + const isSingleMonthSelected = + isSameMonth(selectedRangeStartDate, selectedRangeEndDate) && + dateFrom <= selectedRangeStartTimestamp && + dateTo >= selectedRangeEndTimestamp; + // Проверяем, является ли кликнутый месяц таким же, что и выбранный диапазон + const isSameMonthClicked = isSameMonth(selectedRangeStartDate, dateActiveMonths); + // Проверяем, находится ли кликнутый месяц внутри выбранного диапазона + const isClickedMonthInsideRange = + clickedMonthEndTimestamp >= selectedRangeStartTimestamp && + clickedMonthStartTimestamp <= selectedRangeEndTimestamp; + // Проверяем находится ли выбранный диапазон в пределах кликнутого месяца + const isRangeWithinSingleMonth = + selectedRangeStartTimestamp === dateFrom && selectedRangeEndTimestamp === dateTo; + + if (isSingleMonthSelected && isSameMonthClicked) { + onChange(); + } else if (isClickedMonthInsideRange || !isRangeWithinSingleMonth) { + onChange(clickedMonthStartTimestamp, clickedMonthEndTimestamp); + } else { + const newDateFrom = Math.min(selectedRangeStartTimestamp, clickedMonthStartTimestamp); + const newDateTo = Math.max(selectedRangeEndTimestamp, clickedMonthEndTimestamp); + + onChange(newDateFrom, newDateTo); } }; @@ -239,7 +258,7 @@ export const CalendarMonthOnlyView = ({ }; const renderMonth = (index: number) => { - const isAfterDate = isAfter(activeMonths[index].date, activeMonth); + const isAfterDate = isAfter(activeMonths[index].date, maxDate ?? new Date()); return (
diff --git a/packages/calendar/src/docs/description.mdx b/packages/calendar/src/docs/description.mdx index 4167010ba5..4efa247adf 100644 --- a/packages/calendar/src/docs/description.mdx +++ b/packages/calendar/src/docs/description.mdx @@ -15,10 +15,10 @@ render(() => { const format = React.useCallback((timestamp) => { if (!timestamp) return ''; - return new Intl.DateTimeFormat("ru-RU", { - year: "numeric", - month: "2-digit", - day: "2-digit" + return new Intl.DateTimeFormat('ru-RU', { + year: 'numeric', + month: '2-digit', + day: '2-digit', }).format(new Date(timestamp)); }, []); @@ -79,10 +79,10 @@ render(() => { const format = React.useCallback((timestamp) => { if (!timestamp) return ''; - return new Intl.DateTimeFormat("ru-RU", { - year: "numeric", - month: "2-digit", - day: "2-digit" + return new Intl.DateTimeFormat('ru-RU', { + year: 'numeric', + month: '2-digit', + day: '2-digit', }).format(new Date(timestamp)); }, []); @@ -136,10 +136,10 @@ render(() => { const format = React.useCallback((timestamp) => { if (!timestamp) return ''; - return new Intl.DateTimeFormat("ru-RU", { - year: "numeric", - month: "2-digit", - day: "2-digit" + return new Intl.DateTimeFormat('ru-RU', { + year: 'numeric', + month: '2-digit', + day: '2-digit', }).format(new Date(timestamp)); }, []); @@ -148,7 +148,9 @@ render(() => { }, [rangeBehavior]); const selectedRange = React.useMemo(() => { - return `${format(value ? value.dateFrom : undefined)} - ${format(value ? value.dateTo : undefined)}`; + return `${format(value ? value.dateFrom : undefined)} - ${format( + value ? value.dateTo : undefined, + )}`; }, [value]); const calendarStyles = { @@ -197,10 +199,10 @@ render(() => { const format = React.useCallback((timestamp) => { if (!timestamp) return ''; - return new Intl.DateTimeFormat("ru-RU", { - year: "numeric", - month: "2-digit", - day: "2-digit" + return new Intl.DateTimeFormat('ru-RU', { + year: 'numeric', + month: '2-digit', + day: '2-digit', }).format(new Date(timestamp)); }, []); @@ -209,7 +211,9 @@ render(() => { }, [rangeBehavior]); const selectedRange = React.useMemo(() => { - return `${format(value ? value.dateFrom : undefined)} - ${format(value ? value.dateTo : undefined)}`; + return `${format(value ? value.dateFrom : undefined)} - ${format( + value ? value.dateTo : undefined, + )}`; }, [value]); const allowSelectionFromEmptyRange = selectionMode === 'singleAndRange'; @@ -696,7 +700,7 @@ render(() => { shape={firstRadioValue} clickableMonth={true} value={value} - onChange={(dateFrom, dateTo) => setValue({dateFrom, dateTo})} + onChange={(dateFrom, dateTo) => setValue({ dateFrom, dateTo })} onClose={() => setOpen(false)} open={open} /> diff --git a/packages/calendar/src/docs/development.mdx b/packages/calendar/src/docs/development.mdx index 0681653c44..f3a7ccfcb7 100644 --- a/packages/calendar/src/docs/development.mdx +++ b/packages/calendar/src/docs/development.mdx @@ -11,7 +11,11 @@ import vars from '!!raw-loader!../vars.css'; ```jsx import { Calendar } from '@alfalab/core-components/calendar'; import { CalendarDesktop } from '@alfalab/core-components/calendar/desktop'; -import { CalendarMobile, CalendarMonthOnlyView, CalendarMonthOnlyViewHeader } from '@alfalab/core-components/calendar/mobile'; +import { + CalendarMobile, + CalendarMonthOnlyView, + CalendarMonthOnlyViewHeader, +} from '@alfalab/core-components/calendar/mobile'; ``` Из индекса импортируются responsive версия компонента. @@ -48,7 +52,7 @@ import { CalendarMobile, CalendarMonthOnlyView, CalendarMonthOnlyViewHeader } fr CalendarMobile, PeriodSlider, CalendarMonthOnlyView, - CalendarMonthOnlyViewHeader + CalendarMonthOnlyViewHeader, }} /> @@ -76,7 +80,7 @@ render(() => { zIndex: 2, width: '100%', padding: 'var(--gap-12) var(--gap-24)', - background: "var(--color-light-base-bg-primary)", + background: 'var(--color-light-base-bg-primary)', borderBottom: '1px solid var(--color-light-neutral-500)', }; @@ -85,11 +89,11 @@ render(() => {
- + ); diff --git a/packages/calendar/src/utils.ts b/packages/calendar/src/utils.ts index 2cb2e37743..d7b98ac212 100644 --- a/packages/calendar/src/utils.ts +++ b/packages/calendar/src/utils.ts @@ -11,6 +11,7 @@ import format from 'date-fns/format'; import isAfter from 'date-fns/isAfter'; import isBefore from 'date-fns/isBefore'; import isSameDay from 'date-fns/isSameDay'; +import isThisMonth from 'date-fns/isThisMonth'; import lastDayOfMonth from 'date-fns/lastDayOfMonth'; import max from 'date-fns/max'; import min from 'date-fns/min'; @@ -308,3 +309,8 @@ export function isRangeValue( ): value is { dateFrom?: number | undefined; dateTo?: number | undefined } { return Boolean(value) && typeof value === 'object'; } + +export const getMonthStartTimestamp = (date: Date) => startOfMonth(date).getTime(); + +export const getMonthEndTimestamp = (date: Date) => + isThisMonth(date) ? startOfDay(new Date()).getTime() : lastDayOfMonth(date).getTime(); diff --git a/packages/calendar/tsconfig.json b/packages/calendar/tsconfig.json index 28093ff9f2..91e41d78ef 100644 --- a/packages/calendar/tsconfig.json +++ b/packages/calendar/tsconfig.json @@ -17,6 +17,6 @@ { "path": "../modal" }, { "path": "../mq" }, { "path": "../shared" }, - { "path": "../typography" }, + { "path": "../typography" } ] }