Перейти к содержимому
v3.0.0
English
Русский
Документация

YearPicker

YearPicker — подсистема выбора года в семействе календарей.
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
year-picker.tsx · 5 строк
import { YearPicker, YearPickerCell, YearPickerLayout } from '@vega-ui/react';

<YearPicker aria-label='Year' year='2026-01-01' defaultSelected='2026-01-01' style={{ width: '100%' }}>
  <YearPickerLayout><YearPickerCell /></YearPickerLayout>
</YearPicker>
Поддержка браузеров
Широко доступно
  • Chrome 111
  • Edge 111
  • Firefox 113
  • Safari 15.4
  • Safari на iOS 15.4
Размер в бандле
История версий
Добавлен в v1.13.0, последнее изменение в v3.0.0

Кратко

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

Импорт

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

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

en-US
ru-RU
ar-EG
he-IL
minimal-composition.tsx · 13 строк
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.

a-page-of-years.tsx · 11 строк
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 означает, что ничего не выбрано.

selected-year.tsx · 28 строк
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, использует связанную подсказку для проверки на пустоту и очищает состояние при сбросе.

year-range-filter.tsx · 79 строк
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 на новую страницу, когда прежний активный год уходит из вида.

paged-years-with-own-buttons.tsx · 70 строк
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: приложение проверяет возраст после выбора и превращает подсказку, связанную с сеткой, в ошибку. Если год никогда не может быть допустимым, предпочитайте границы (см. Постраничный диапазон лет); проверяйте, когда пользователю нужно увидеть причину.

under-age-birth-year.tsx · 46 строк
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.

specific-years.tsx · 32 строки
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 (см. Адаптер календарной системы) годы следуют этому календарю. В японском календаре 2019 год разделён: Хэйсэй 31 длится с 1 января по 30 апреля, а Рэйва 1 начинается 1 мая, поэтому страница показывает для него две ячейки с ключами '2019-01-01' и '2019-05-01' и подписями 平成31年 и 令和元年. Арифметика лет, перелистывание и from / to считают их двумя годами.

era-change-in-the-japanese-calendar.tsx · 14 строк
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). В тайском буддийском календаре ячейка с ключом '2026-01-01' подписана พ.ศ. 2569 в локали th-TH; ключи, from и to остаются григорианскими ISO-датами.

years-counted-from-another-epoch.tsx · 14 строк
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, чтобы корень всегда знал видимую страницу.

valid-composition-patterns.tsx · 24 строки
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 скроллера отличается от размера страницы раскладки

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

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

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