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

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
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