Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(calendar): select period by clicking on month #1465

Merged
merged 13 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/funny-jeans-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@alfalab/core-components-calendar': minor
---

Изменено поведение пропса clickableMonth. Добавлена возможность выбирать промежуток между месяцами. Первый клик по лейблу месяца выбирает весь месяц. Второй клик на следующий месяц выбирает промежуток между этими двумя месяцами.
103 changes: 61 additions & 42 deletions packages/calendar/src/components/calendar-mobile/Component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -26,6 +23,8 @@ import {
dateArrayToHashTable,
generateMonths,
generateWeeks,
getMonthEndTimestamp,
getMonthStartTimestamp,
isRangeValue,
limitDate,
monthName,
Expand Down Expand Up @@ -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);
}
};

Expand All @@ -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 (
<div className={styles.daysTable} id={`month-${index}`}>
Expand Down
42 changes: 23 additions & 19 deletions packages/calendar/src/docs/description.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}, []);

Expand Down Expand Up @@ -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));
}, []);

Expand Down Expand Up @@ -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));
}, []);

Expand All @@ -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 = {
Expand Down Expand Up @@ -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));
}, []);

Expand All @@ -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';
Expand Down Expand Up @@ -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}
/>
Expand Down
18 changes: 11 additions & 7 deletions packages/calendar/src/docs/development.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 версия компонента.
Expand Down Expand Up @@ -48,7 +52,7 @@ import { CalendarMobile, CalendarMonthOnlyView, CalendarMonthOnlyViewHeader } fr
CalendarMobile,
PeriodSlider,
CalendarMonthOnlyView,
CalendarMonthOnlyViewHeader
CalendarMonthOnlyViewHeader,
}}
/>

Expand Down Expand Up @@ -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)',
};

Expand All @@ -85,11 +89,11 @@ render(() => {
<div style={headerStyle}>
<CalendarMonthOnlyViewHeader />
</div>
<Gap size="m" direction="vertical" />
<Gap size='m' direction='vertical' />
<CalendarMonthOnlyView
value={value}
onChange={setValue}
showCurrentYearSelector={false}
value={value}
onChange={setValue}
showCurrentYearSelector={false}
/>
</>
);
Expand Down
6 changes: 6 additions & 0 deletions packages/calendar/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
2 changes: 1 addition & 1 deletion packages/calendar/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
{ "path": "../modal" },
{ "path": "../mq" },
{ "path": "../shared" },
{ "path": "../typography" },
{ "path": "../typography" }
]
}
Loading