'use client'; import type { ChangeEvent } from 'react'; import * as React from 'react'; import { Icon12Dropdown, Icon20ChevronLeftOutline, Icon20ChevronRightOutline, } from '@vkontakte/icons'; import { classNames } from '@vkontakte/vkjs'; import { ViewWidth } from '../../lib/adaptivity'; import { DEFAULT_MAX_YEAR, DEFAULT_MIN_YEAR, getMonths, getYears } from '../../lib/calendar'; import { addMonths, setMonth, setYear, subMonths } from '../../lib/date'; import { cacheDateTimeFormat } from '../../lib/intlCache'; import type { HTMLAttributesWithRootRef } from '../../types'; import { AdaptivityProvider } from '../AdaptivityProvider/AdaptivityProvider'; import { useConfigProvider } from '../ConfigProvider/ConfigProviderContext'; import { CustomSelect, type SelectProps } from '../CustomSelect/CustomSelect'; import { RootComponent } from '../RootComponent/RootComponent'; import { Tappable } from '../Tappable/Tappable'; import { Paragraph } from '../Typography/Paragraph/Paragraph'; import { VisuallyHidden } from '../VisuallyHidden/VisuallyHidden'; import styles from './CalendarHeader.module.css'; const formatterDateTimeFormatOptions = { year: 'numeric', month: 'long', } as const; const formatterDateTimeFormat = /*#__PURE__*/ cacheDateTimeFormat(); const monthDateTimeFormatOptions = { month: 'long', } as const; const monthDateTimeFormat = /*#__PURE__*/ cacheDateTimeFormat(); const yearDateTimeFormatOptions = { year: 'numeric', } as const; const yearDateTimeFormat = /*#__PURE__*/ cacheDateTimeFormat(); type ArrowMonthProps = Omit, 'onClick' | 'aria-label'>; export type CalendarHeaderTestsProps = { /** * Передает атрибут `data-testid` для дропдауна выбора месяца в заголовке календаря. */ monthDropdownTestId?: string | ((monthIndex: number) => string) | undefined; /** * Передает атрибут `data-testid` для дропдауна выбора года в заголовке календаря. */ yearDropdownTestId?: string | ((year: number) => string) | undefined; /** * Передает атрибут `data-testid` для кнопки перехода к следующему месяцу в заголовке календаря. */ nextMonthButtonTestId?: string | undefined; /** * Передает атрибут `data-testid` для кнопки перехода к предыдущему месяцу в заголовке календаря. */ prevMonthButtonTestId?: string | undefined; }; export interface CalendarHeaderProps extends Omit, 'onChange'>, CalendarHeaderTestsProps { /** * Отображаемая дата. */ viewDate: Date; /** * Скрывает иконку для переключения на предыдущий месяц. */ prevMonthHidden?: boolean | undefined; /** * Скрывает иконку для переключения на следующий месяц. */ nextMonthHidden?: boolean | undefined; /** * Отключает селекторы выбора месяца/года. */ disablePickers?: boolean | undefined; /** * `aria-label` для кнопки предыдущего месяца. */ prevMonthLabel?: string | undefined; /** * `aria-label` для кнопки следующего месяца. */ nextMonthLabel?: string | undefined; /** * `aria-label` для селектора месяца. */ changeMonthLabel?: string | undefined; /** * `aria-label` для селектора года. */ changeYearLabel?: string | undefined; /** * Кастомная иконка для кнопки предыдущего месяца. */ prevMonthIcon?: React.ReactNode | undefined; /** * Кастомная иконка для кнопки следующего месяца. */ nextMonthIcon?: React.ReactNode | undefined; /** * Дополнительные свойства для кнопки предыдущего месяца. */ prevMonthProps?: ArrowMonthProps | undefined; /** * Дополнительные свойства для кнопки следующего месяца. */ nextMonthProps?: ArrowMonthProps | undefined; /** * Функция для проверки блокировки месяца. */ isMonthDisabled?: ((monthNumber: number, year?: number) => boolean) | undefined; /** * Функция для проверки блокировки года. */ isYearDisabled?: ((yearNumber: number) => boolean) | undefined; /** * Обработчик изменения отображаемой даты. */ onChange: (viewDate: Date) => void; /** * Нажатие на кнопку переключения на следующий месяц. */ onNextMonth?: (() => void) | undefined; /** * Нажатие на кнопку переключения на предыдущий месяц. */ onPrevMonth?: (() => void) | undefined; } export const CalendarHeader = ({ viewDate, onChange, prevMonthHidden: prevMonthHiddenProp = false, nextMonthHidden: nextMonthHiddenProp = false, disablePickers = false, onNextMonth, onPrevMonth, prevMonthProps = {}, nextMonthProps = {}, prevMonthLabel = 'Предыдущий месяц', nextMonthLabel = 'Следующий месяц', changeMonthLabel = 'Изменить месяц', changeYearLabel = 'Изменить год', prevMonthIcon = ( ), nextMonthIcon = ( ), isMonthDisabled, isYearDisabled, monthDropdownTestId, yearDropdownTestId, prevMonthButtonTestId, nextMonthButtonTestId, ...restProps }: CalendarHeaderProps): React.ReactNode => { const { locale, direction } = useConfigProvider(); const onMonthsChange = React.useCallback( (_: ChangeEvent, newValue: SelectProps['value']) => onChange(setMonth(viewDate, Number(newValue))), [onChange, viewDate], ); const onYearChange = React.useCallback( (_: ChangeEvent, newValue: SelectProps['value']) => onChange(setYear(viewDate, Number(newValue))), [onChange, viewDate], ); const currentYear = viewDate.getFullYear(); const currentMonth = viewDate.getMonth(); const months = React.useMemo( () => getMonths(locale).map(({ value, label }) => ({ value, label: {label}, disabled: isMonthDisabled && isMonthDisabled(value), })), [locale, isMonthDisabled], ); const years = React.useMemo( () => getYears(currentYear, 100).map((year) => ({ ...year, disabled: isYearDisabled && isYearDisabled(year.value), })), [currentYear, isYearDisabled], ); const formatter = formatterDateTimeFormat(locale, formatterDateTimeFormatOptions); const { className: prevMonthClassName, ...restPrevMonthProps } = prevMonthProps; const { className: nextMonthClassName, ...restNextMonthProps } = nextMonthProps; let nextMonthHidden = nextMonthHiddenProp || (currentMonth === 11 && currentYear === DEFAULT_MAX_YEAR); if (isMonthDisabled && !nextMonthHidden) { nextMonthHidden = isMonthDisabled( currentMonth === 11 ? 0 : currentMonth + 1, currentMonth === 11 ? Math.min(currentYear + 1, DEFAULT_MAX_YEAR) : currentYear, ); } let prevMonthHidden = prevMonthHiddenProp || (currentMonth === 0 && currentYear === DEFAULT_MIN_YEAR); if (isMonthDisabled && !prevMonthHidden) { prevMonthHidden = isMonthDisabled( currentMonth === 0 ? 11 : currentMonth - 1, currentMonth === 0 ? Math.max(currentYear - 1, DEFAULT_MIN_YEAR) : currentYear, ); } const stopPropogationOfEscapeKeyboardEventWhenSelectIsOpen = React.useCallback( (event: React.KeyboardEvent, isOpen: boolean) => { if (isOpen && event.key === 'Escape') { event.stopPropagation(); } }, [], ); return ( {!prevMonthHidden && ( {prevMonthLabel}, {formatter.format(subMonths(viewDate, 1))} {direction === 'ltr' ? prevMonthIcon : nextMonthIcon} )} {disablePickers ? ( {monthDateTimeFormat(locale, monthDateTimeFormatOptions).format(viewDate)}   {yearDateTimeFormat(locale, yearDateTimeFormatOptions).format(viewDate)} ) : (
} onChange={onMonthsChange} forceDropdownPortal={false} selectType="accent" onInputKeyDown={stopPropogationOfEscapeKeyboardEventWhenSelectIsOpen} slotProps={{ input: { 'aria-label': changeMonthLabel, 'data-testid': typeof monthDropdownTestId === 'string' ? monthDropdownTestId : monthDropdownTestId?.(currentMonth), }, }} /> } onChange={onYearChange} forceDropdownPortal={false} selectType="accent" onInputKeyDown={stopPropogationOfEscapeKeyboardEventWhenSelectIsOpen} slotProps={{ input: { 'aria-label': changeYearLabel, 'data-testid': typeof yearDropdownTestId === 'string' ? yearDropdownTestId : yearDropdownTestId?.(currentYear), }, }} />
)} {!nextMonthHidden && ( {nextMonthLabel}, {formatter.format(addMonths(viewDate, 1))} {direction === 'ltr' ? nextMonthIcon : prevMonthIcon} )}
); };