# YearPicker

## Кратко

`YearPicker` — подсистема выбора года в семействе календарей. Он построен на `DataGridPicker`, поддерживает статичные сетки лет и сетки на основе скроллера и перелистывает годы группами фиксированного размера (`period`, по умолчанию `12`) для навигации, похожей на переход по десятилетиям.

## Импорт

```tsx
import {
  YearPicker,
  YearPickerCell,
  YearPickerItem,
  YearPickerLayout,
  YearPickerRow,
  YearPickerRowGroup,
  YearPickerScroller,
  YearPickerScrollerContent,
  YearPickerScrollerLayout,
  createYearPickerGrid,
  useYearPickerCellContext,
  useYearPickerContext,
  useYearPickerScrollerContext,
  type YearPickerCellContextData,
  type YearPickerCellProps,
  type YearPickerItemProps,
  type YearPickerLayoutProps,
  type YearPickerProps,
  type YearPickerRowGroupProps,
  type YearPickerRowProps,
  type YearPickerScrollerContentProps,
  type YearPickerScrollerLayoutProps,
  type YearPickerScrollerProps,
} from '@vega-ui/react';
```

## Экспортируемые типы

- `YearPickerProps`
- `YearPickerLayoutProps`
- `YearPickerRowGroupProps`
- `YearPickerRowProps`
- `YearPickerItemProps`
- `YearPickerCellProps`
- `YearPickerCellContextData`
- `YearPickerScrollerProps`
- `YearPickerScrollerContentProps`
- `YearPickerScrollerLayoutProps`

## Минимальная композиция

```tsx
import { YearPicker, YearPickerCell, YearPickerLayout } from '@vega-ui/react';

export const locales = ['en-US', 'ru-RU', 'ar-EG', 'he-IL'];

export default function MinimalComposition({ locale = 'en-US' }: { locale?: string }) {
  return (
    <YearPicker aria-label='Year' locale={locale} year='2026-01-01' style={{ width: '100%' }}>
      <YearPickerLayout>
        <YearPickerCell />
      </YearPickerLayout>
    </YearPicker>
  );
}
```

## Обязательные части

- `YearPicker`: корневая подсистема выбора года

## Необязательные части

- `YearPickerLayout`
- `YearPickerRowGroup`
- `YearPickerRow`
- `YearPickerItem`
- `YearPickerCell`: ячейка, размещаемая как шаблон раскладок
- `YearPickerScroller`
- `YearPickerScrollerContent`
- `YearPickerScrollerLayout`

## Порядок композиции

1. `YearPicker`
2. либо `YearPickerLayout` с шаблоном ячейки в дочерних элементах (`<YearPickerCell />` или собственным), либо собственная композиция из строк и элементов
3. необязательный слой скроллера для постраничных диапазонов лет

## Варианты

- Выбор: `single`, `multiple`, `range`
- Раскладка: статичная сетка или перелистывание лет на основе скроллера
- Период: явно заданный видимый `year` (строка `YYYY-MM-DD` любого дня этого года) или текущий год
- Локаль и календарная система: пропсы `locale` и `calendar`, по умолчанию — локаль среды выполнения и григорианский календарь; эры обрабатывает адаптер

## Пограничные случаи

- Большие диапазоны лет требуют дополнительной проверки фокуса, перелистывания и видимого контекста.
- `year` управляет видимым периодом, а не только выбором.
- В режиме скроллера перелистывание идёт по `period` лет; `period`, отличающийся от `rows × cols` раскладки, показывает неправильный диапазон.

## Типичные ошибки

- Неограниченные диапазоны лет без проверки удобства использования.
- Рассогласование правил родительского календаря и границ пикера лет.
- Низкоуровневый пикер лет там, где понятнее было бы переключение режимов календаря на более высоком уровне.

## Примеры

### Базовый

#### Базовый: страница лет

`YearPickerLayout` рендерит страницу из `rows × cols` лет (по умолчанию 12), которая начинается с года, заданного через `year`. Ключ ячейки — первый день года, поэтому `defaultSelected` — `'2026-01-01'`. У сетки нет видимого заголовка, поэтому ей передан `aria-label`.

```tsx
import { YearPicker, YearPickerCell, YearPickerLayout } from '@vega-ui/react';

export default function APageOfYears() {
  return (
    <YearPicker aria-label='Model year' locale='en-US' year='2026-01-01' defaultSelected='2026-01-01' style={{ width: '100%' }}>
      <YearPickerLayout>
        <YearPickerCell />
      </YearPickerLayout>
    </YearPicker>
  );
}
```

### Контролируемый/с состоянием

#### Контролируемый/с состоянием: выбранный год

`selected` и `onSelectCell` делают выбор контролируемым. Скроллер листает годы, а `year` держит в виду страницу выбранного года; `null` означает, что ничего не выбрано.

```tsx
import {
  Text,
  YearPicker,
  YearPickerCell,
  YearPickerScroller,
  YearPickerScrollerContent,
  YearPickerScrollerLayout,
} from '@vega-ui/react';
import { useState } from 'react';

export default function SelectedYear() {
  const [year, setYear] = useState<string | null>('2024-01-01');

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-6)', justifyItems: 'start', width: '100%' }}>
      <YearPicker aria-label='Model year' locale='en-US' year={year ?? '2026-01-01'} selected={year} onSelectCell={setYear}>
        <YearPickerScroller>
          <YearPickerScrollerContent>
            <YearPickerScrollerLayout>
              <YearPickerCell />
            </YearPickerScrollerLayout>
          </YearPickerScrollerContent>
        </YearPickerScroller>
      </YearPicker>
      <Text aria-live='polite' style={{ color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums' }}>Model year: {year ? year.slice(0, 4) : 'not set'}</Text>
    </div>
  );
}
```

### Форма/интеграция

#### Форма/интеграция: фильтр по диапазону лет

`selection='range'` выбирает промежуток лет. Форма отправляет первый год как есть, а конец — как последний день его года, читает оба значения через `FormData`, использует связанную подсказку для проверки на пустоту и очищает состояние при сбросе.

```tsx
import {
  Button,
  Fieldset,
  FieldsetLegend,
  HelperText,
  Text,
  YearPicker,
  YearPickerCell,
  YearPickerLayout,
} from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';

function getYearEnd(yearStart: string) {
  return `${yearStart.slice(0, 4)}-12-31`;
}

export default function YearRangeFilter() {
  const [years, setYears] = useState<string[]>([]);
  const [missing, setMissing] = useState(false);
  const [query, setQuery] = useState('');
  const hintId = useId();

  const [from, to] = years;

  const change = (next: string[]) => {
    setYears(next);
    setMissing(false);
  };

  const submit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);

    if (!data.get('from') || !data.get('to')) {
      setMissing(true);
      return;
    }

    setQuery(new URLSearchParams({ from: String(data.get('from')), to: String(data.get('to')) }).toString());
  };

  const reset = () => {
    setYears([]);
    setMissing(false);
    setQuery('');
  };

  return (
    <form onSubmit={submit} onReset={reset} style={{ display: 'grid', gap: 'var(--spacing-12)', justifyItems: 'start', width: '100%' }}>
      <Fieldset>
        <FieldsetLegend>Archive years</FieldsetLegend>
        <YearPicker
          aria-label='Archive years, 2015 to 2026'
          aria-describedby={hintId}
          locale='en-US'
          selection='range'
          year='2015-01-01'
          to='2026-01-01'
          selected={years}
          onSelectCell={change}
        >
          <YearPickerLayout>
            <YearPickerCell />
          </YearPickerLayout>
        </YearPicker>
        <HelperText id={hintId} error={missing}>
          {missing ? 'Pick the first and the last year.' : 'Pick the first and the last year of the search.'}
        </HelperText>
      </Fieldset>
      <input type='hidden' name='from' value={from ?? ''} />
      <input type='hidden' name='to' value={to ? getYearEnd(to) : ''} />
      <div style={{ display: 'flex', gap: 'var(--spacing-4)' }}>
        <Button type='submit'>Search</Button>
        <Button type='reset' variant='secondary' appearance='ghost'>Reset</Button>
      </div>
      <Text aria-live='polite'>{query ? `GET /archive?${query}` : ''}</Text>
    </form>
  );
}
```

### Раскладка/оверлей

#### Раскладка/оверлей: постраничные годы со своими кнопками

Скроллер отдаёт `prev()` и `next()` через `apiRef`, поэтому страницу можно менять кнопками вне сетки, и сообщает видимую страницу через `onChangePeriod` для подписи диапазона. Держите `period` скроллера равным `rows × cols` раскладки и переносите `active` на новую страницу, когда прежний активный год уходит из вида.

```tsx
import {
  Icon,
  IconButton,
  type IndexedSnapScrollerApiRef,
  Text,
  YearPicker,
  YearPickerCell,
  YearPickerScroller,
  YearPickerScrollerContent,
  YearPickerScrollerLayout,
} from '@vega-ui/react';
import { ChevronLeft, ChevronRight } from '@vega-ui/icons';
import { useRef, useState } from 'react';

const PERIOD = 20;

function getYear(key: string) {
  return Number(key.slice(0, 4));
}

export default function PagedYearsWithOwnButtons() {
  const scrollerRef = useRef<IndexedSnapScrollerApiRef>(null);
  const [page, setPage] = useState('2016-01-01');
  const [active, setActive] = useState('2026-01-01');
  const [selected, setSelected] = useState<string | null>(null);

  const firstYear = getYear(page);
  const lastYear = firstYear + PERIOD - 1;

  const changePage = (next: string) => {
    setPage(next);
    setActive((current) => {
      const year = getYear(current);
      if (year >= getYear(next) && year < getYear(next) + PERIOD) return current;

      return next;
    });
  };

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-4)', inlineSize: 'fit-content' }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <IconButton aria-label='Earlier years' size='sm' variant='secondary' appearance='ghost' onClick={() => scrollerRef.current?.prev()}>
          <Icon><ChevronLeft /></Icon>
        </IconButton>
        <Text fontWeight={600} aria-live='polite'>{firstYear}–{lastYear}</Text>
        <IconButton aria-label='Later years' size='sm' variant='secondary' appearance='ghost' onClick={() => scrollerRef.current?.next()}>
          <Icon><ChevronRight /></Icon>
        </IconButton>
      </div>
      <YearPicker
        aria-label='Founding year'
        locale='en-US'
        year='2016-01-01'
        active={active}
        onChangeActive={setActive}
        selected={selected}
        onSelectCell={setSelected}
      >
        <YearPickerScroller apiRef={scrollerRef} period={PERIOD} onChangePeriod={changePage}>
          <YearPickerScrollerContent>
            <YearPickerScrollerLayout rows={5} cols={4}>
              <YearPickerCell />
            </YearPickerScrollerLayout>
          </YearPickerScrollerContent>
        </YearPickerScroller>
      </YearPicker>
    </div>
  );
}
```

### Ошибка

#### Ошибка: год рождения младше допустимого возраста

Здесь год рождения не ограничен через `from` / `to`: приложение проверяет возраст после выбора и превращает подсказку, связанную с сеткой, в ошибку. Если год никогда не может быть допустимым, предпочитайте границы (см. [Постраничный диапазон лет](https://vega-ui.dev/ru/components/year-picker/guides/#постраничный-диапазон-лет)); проверяйте, когда пользователю нужно увидеть причину.

```tsx
import {
  HelperText,
  YearPicker,
  YearPickerCell,
  YearPickerScroller,
  YearPickerScrollerContent,
  YearPickerScrollerLayout,
} from '@vega-ui/react';
import { useId, useState } from 'react';

const MIN_AGE = 18;
const CURRENT_YEAR = 2026;

export default function UnderAgeBirthYear() {
  const [birthYear, setBirthYear] = useState<string | null>('2010-01-01');
  const messageId = useId();

  const age = birthYear ? CURRENT_YEAR - Number(birthYear.slice(0, 4)) : null;
  const invalid = age !== null && age < MIN_AGE;

  let message = 'Pick the year you were born.';
  if (age !== null) message = `You turn ${age} this year.`;
  if (invalid) message = `You must be at least ${MIN_AGE} to open an account. You turn ${age} this year.`;

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', justifyItems: 'start', width: '100%' }}>
      <YearPicker
        aria-label='Birth year'
        aria-describedby={messageId}
        locale='en-US'
        year={birthYear ?? '2000-01-01'}
        selected={birthYear}
        onSelectCell={setBirthYear}
      >
        <YearPickerScroller>
          <YearPickerScrollerContent>
            <YearPickerScrollerLayout>
              <YearPickerCell />
            </YearPickerScrollerLayout>
          </YearPickerScrollerContent>
        </YearPickerScroller>
      </YearPicker>
      <HelperText id={messageId} error={invalid} aria-live='polite'>{message}</HelperText>
    </div>
  );
}
```

### Отключённый

#### Отключённый: отдельные годы

Массив начал лет отключает ровно эти годы — здесь уже закрытые финансовые годы; `to` отключает будущие. Причина написана рядом с сеткой и связана через `aria-describedby`.

```tsx
import {
  HelperText,
  YearPicker,
  YearPickerCell,
  YearPickerLayout,
} from '@vega-ui/react';
import { useId } from 'react';

const closedYears = ['2022-01-01', '2023-01-01'];

export default function SpecificYears() {
  const hintId = useId();

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', justifyItems: 'start', width: '100%' }}>
      <YearPicker
        aria-label='Fiscal year to adjust'
        aria-describedby={hintId}
        locale='en-US'
        year='2018-01-01'
        to='2026-01-01'
        disabled={closedYears}
        style={{ width: '100%' }}
      >
        <YearPickerLayout rows={3} cols={3}>
          <YearPickerCell />
        </YearPickerLayout>
      </YearPicker>
      <HelperText id={hintId}>Fiscal years 2022 and 2023 are closed after the audit.</HelperText>
    </div>
  );
}
```

### Пограничный случай

#### Пограничный случай: смена эры в японском календаре

С календарной системой из `createCalendarSystem` (см. [Адаптер календарной системы](https://vega-ui.dev/ru/docs/i18n/#адаптер-календарной-системы)) годы следуют этому календарю. В японском календаре 2019 год разделён: Хэйсэй 31 длится с 1 января по 30 апреля, а Рэйва 1 начинается 1 мая, поэтому страница показывает для него две ячейки с ключами `'2019-01-01'` и `'2019-05-01'` и подписями 平成31年 и 令和元年. Арифметика лет, перелистывание и `from` / `to` считают их двумя годами.

```tsx
import { createCalendarSystem } from '@vega-ui/i18n';
import { YearPicker, YearPickerCell, YearPickerLayout } from '@vega-ui/react';

const japaneseCalendar = createCalendarSystem('japanese');

export default function EraChangeInTheJapaneseCalendar() {
  return (
    <YearPicker aria-label='Year' calendar={japaneseCalendar} locale='ja-JP' year='2018-01-01' style={{ width: '100%' }}>
      <YearPickerLayout>
        <YearPickerCell />
      </YearPickerLayout>
    </YearPicker>
  );
}
```

#### Пограничный случай: годы, отсчитываемые от другой эпохи

Календарь, который только перенумеровывает григорианские годы, может расширять `GregorianCalendar` (см. [Расширение GregorianCalendar](https://vega-ui.dev/ru/docs/i18n/#расширение-gregoriancalendar)). В тайском буддийском календаре ячейка с ключом `'2026-01-01'` подписана พ.ศ. 2569 в локали `th-TH`; ключи, `from` и `to` остаются григорианскими ISO-датами.

```tsx
import { createCalendarSystem } from '@vega-ui/i18n';
import { YearPicker, YearPickerCell, YearPickerLayout } from '@vega-ui/react';

const buddhistCalendar = createCalendarSystem('buddhist');

export default function YearsCountedFromAnotherEpoch() {
  return (
    <YearPicker aria-label='Year' calendar={buddhistCalendar} locale='th-TH' year='2026-01-01' to='2026-01-01' style={{ width: '100%' }}>
      <YearPickerLayout>
        <YearPickerCell />
      </YearPickerLayout>
    </YearPicker>
  );
}
```

## Анатомия

### Обзор

`YearPicker` оборачивает `DataGridPicker` для значений-лет и может рендерить либо статичные сетки лет, либо постраничные диапазоны лет на основе скроллера.

### Обязательные части

#### `YearPicker`

Обязательная. Отвечает за поведение выбора года и контекст видимого года.

### Необязательные части

#### `YearPickerLayout`

Необязательный стандартный рендерер сетки лет.

#### `YearPickerCell`

Шаблон ячейки. Дочерние элементы `YearPickerLayout` и `YearPickerScrollerLayout` обязательны и рендерятся по одному разу на каждый год внутри контекста ячейки, который читается через `useYearPickerCellContext()`; обычно это `<YearPickerCell />`. Размещайте собственное содержимое внутри `YearPickerCell`, чтобы сохранить состояния выбора, фокуса и отключения.

#### `YearPickerRowGroup`, `YearPickerRow`, `YearPickerItem`

Необязательные низкоуровневые части для композиции сетки.

#### `YearPickerScroller`, `YearPickerScrollerContent`, `YearPickerScrollerLayout`

Необязательная инфраструктура скроллера для постраничных диапазонов лет.

### Порядок композиции

1. `YearPicker`
2. необязательный слой скроллера
3. либо `YearPickerLayout`, либо собственная композиция из строк и элементов

### Корректные паттерны композиции

Композиция со скроллером: `YearPickerScroller` › `YearPickerScrollerContent` › `YearPickerScrollerLayout` › шаблон ячейки внутри `YearPicker`. Храните `year` в состоянии и обновляйте его из `onChangePeriod`, чтобы корень всегда знал видимую страницу.

```tsx
import {
  YearPicker,
  YearPickerCell,
  YearPickerScroller,
  YearPickerScrollerContent,
  YearPickerScrollerLayout,
} from '@vega-ui/react';
import { useState } from 'react';

export default function ValidCompositionPatterns() {
  const [year, setYear] = useState('2026-01-01');

  return (
    <YearPicker aria-label='Year' locale='en-US' year={year}>
      <YearPickerScroller onChangePeriod={setYear}>
        <YearPickerScrollerContent>
          <YearPickerScrollerLayout>
            <YearPickerCell />
          </YearPickerScrollerLayout>
        </YearPickerScrollerContent>
      </YearPickerScroller>
    </YearPicker>
  );
}
```

### Некорректные паттерны композиции

#### `period` скроллера отличается от размера страницы раскладки

Пользователь видит один период, а логика перелистывания считает, что находится на другом.

#### Собственные раскладки, игнорирующие готовые хелперы для лет

Становится сложнее сохранить правильный порядок лет и семантику перелистывания.
