# PageControl: Руководства

## Паттерны

### Индикаторы карусели

Когда использовать:

- короткой карусели или последовательности в hero-блоке нужна компактная навигация

Заметки о композиции:

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

Компромиссы:

- компактно и привычно
- недостаточно информативно для длинных последовательностей

```tsx
import { Card, Heading, PageControl, PageControlItem, Text } from '@vega-ui/react';
import { useId, useState } from 'react';

const slides = [
  { title: 'Plan your week', text: 'Drag tasks onto the calendar and see your free time at a glance.' },
  { title: 'Share with your team', text: 'Invite teammates to a board and assign tasks in one click.' },
  { title: 'Stay on track', text: 'Get a summary every Monday with what slipped and what is next.' },
];

export default function CarouselIndicators() {
  const prefix = useId();
  const [active, setActive] = useState(0);
  const slide = slides[active];

  return (
    <Card size='sm' style={{ display: 'grid', gap: 'var(--spacing-10)', justifyItems: 'center', width: '100%' }}>
      <div
        role='tabpanel'
        id={`${prefix}-panel`}
        aria-labelledby={`${prefix}-tab-${active}`}
        style={{ display: 'grid', alignContent: 'start', gap: 'var(--spacing-3)', minHeight: 96, textAlign: 'center' }}
      >
        <Heading as='h3' size={4} fontWeight={600}>{slide.title}</Heading>
        <Text size={2} style={{ color: 'var(--label-secondary)' }}>{slide.text}</Text>
      </div>
      <PageControl active={active} onChangeActive={setActive} aria-label='Feature tour'>
        {slides.map((item, index) => (
          <PageControlItem
            key={item.title}
            index={index}
            id={`${prefix}-tab-${index}`}
            aria-controls={`${prefix}-panel`}
            aria-label={`${item.title}, ${index + 1} of ${slides.length}`}
          />
        ))}
      </PageControl>
    </Card>
  );
}
```

### Точки прогресса по таймеру

Когда использовать:

- автопрокрутка или переход по таймеру — часть сценария

Заметки о композиции:

- используйте `PageControlProgress`
- согласуйте длительность с реальным таймингом автопрокрутки

Компромиссы:

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

```tsx
import { Button, PageControl, PageControlItem, PageControlProgress, Text } from '@vega-ui/react';
import { useState } from 'react';

const stories = ['New arrivals', 'Autumn sale', 'Gift cards', 'Store opening'];

export default function TimedProgressIndicators() {
  const [active, setActive] = useState(0);
  const [paused, setPaused] = useState(false);

  const advance = () => setActive((active + 1) % stories.length);

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-8)', justifyItems: 'center', width: '100%' }}>
      <Text size={4} fontWeight={600} aria-live={paused ? 'polite' : 'off'}>{stories[active]}</Text>
      <PageControl active={active} onChangeActive={setActive} aria-label='Stories'>
        {stories.map((story, index) => {
          if (paused) {
            return <PageControlItem key={story} index={index} aria-label={story} />;
          }

          return (
            <PageControlProgress
              key={story}
              index={index}
              duration={4000}
              aria-label={story}
              onAnimationEnd={index === active ? advance : undefined}
            />
          );
        })}
      </PageControl>
      <Button size='sm' variant='secondary' appearance='ghost' aria-pressed={paused} onClick={() => setPaused(!paused)}>
        {paused ? 'Resume autoplay' : 'Pause autoplay'}
      </Button>
    </div>
  );
}
```

### Индикаторы шагов над формой

Когда использовать:

- многошаговому сценарию нужны лёгкие подсказки о прогрессе

Заметки о композиции:

- если важны подписи, передавайте семантику шагов в другом месте
- используйте `active`, чтобы отражать реальный текущий шаг

Компромиссы:

- компактно
- объясняет меньше, чем степпер с подписями

```tsx
import { Button, HelperText, Label, PageControl, PageControlItem, Text, TextField, TextFieldInput } from '@vega-ui/react';
import { FormEvent, MouseEvent, useId, useState } from 'react';

const steps = ['Account', 'Workspace', 'Invite'];

export default function StepIndicatorAboveFormPages() {
  const id = useId();
  const [step, setStep] = useState(0);
  const [reached, setReached] = useState(0);
  const [result, setResult] = useState('');

  const goTo = (index: number) => {
    if (index > reached) return;
    setStep(index);
  };

  const next = (event: MouseEvent<HTMLButtonElement>) => {
    const fieldset = event.currentTarget.form?.elements.namedItem(`step-${step}`);
    if (!(fieldset instanceof HTMLFieldSetElement)) return;

    const fields = Array.from(fieldset.elements) as HTMLInputElement[];
    const valid = fields.every((field) => field.reportValidity());
    if (!valid) return;

    setStep(step + 1);
    setReached(Math.max(reached, step + 1));
  };

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    setResult(`Workspace “${data.get('workspace')}” created for ${data.get('email')}.`);
  };

  return (
    <form onSubmit={handleSubmit} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%' }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)', justifyItems: 'center' }}>
        <PageControl active={step} onChangeActive={goTo} aria-label='Sign-up steps'>
          {steps.map((label, index) => (
            <PageControlItem
              key={label}
              index={index}
              disabled={index > reached}
              aria-label={`Step ${index + 1}: ${label}`}
            />
          ))}
        </PageControl>
        <Text size={2} style={{ color: 'var(--label-secondary)' }}>Step {step + 1} of {steps.length}: {steps[step]}</Text>
      </div>

      <fieldset name='step-0' hidden={step !== 0} style={{ margin: 0, padding: 0, border: 'none' }}>
        <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
          <Label htmlFor={`${id}-email`}>Work email</Label>
          <TextField>
            <TextFieldInput id={`${id}-email`} name='email' type='email' autoComplete='email' required />
          </TextField>
        </div>
      </fieldset>

      <fieldset name='step-1' hidden={step !== 1} style={{ margin: 0, padding: 0, border: 'none' }}>
        <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
          <Label htmlFor={`${id}-workspace`}>Workspace name</Label>
          <TextField>
            <TextFieldInput id={`${id}-workspace`} name='workspace' required minLength={3} />
          </TextField>
        </div>
      </fieldset>

      <fieldset name='step-2' hidden={step !== 2} style={{ margin: 0, padding: 0, border: 'none' }}>
        <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
          <Label htmlFor={`${id}-invite`}>Invite a teammate (optional)</Label>
          <TextField>
            <TextFieldInput id={`${id}-invite`} name='invite' type='email' />
          </TextField>
        </div>
      </fieldset>

      <div style={{ display: 'flex', justifyContent: 'space-between' }}>
        <Button type='button' variant='secondary' appearance='ghost' disabled={step === 0} onClick={() => setStep(step - 1)}>
          Back
        </Button>
        {step < steps.length - 1 && <Button type='button' onClick={next}>Continue</Button>}
        {step === steps.length - 1 && <Button type='submit'>Create workspace</Button>}
      </div>
      {result && <HelperText role='status'>{result}</HelperText>}
    </form>
  );
}
```

## Сравнение

### Быстрое правило выбора

Используйте `PageControl` для коротких последовательностей, например шагов онбординга или каруселей. Используйте `Pagination` для настоящей навигации по страницам наборов результатов.

### `PageControl` и `Pagination`

- Используйте `PageControl` для коротких компактных последовательностей.
- Используйте `Pagination` для явных номеров страниц и навигации по наборам данных.

### `PageControl` и `Stepper`

- Используйте `PageControl`, когда индикаторы намеренно минимальны.
- Используйте более полный паттерн степпера, когда важны подписи шагов и более подробный статус.

### Выбирайте этот компонент, когда

- последовательность короткая
- достаточно компактных индикаторов

### Не выбирайте этот компонент, когда

- пользователям нужны номера страниц или адресуемые переходы
- каждому шагу нужны видимые подписи и более подробное состояние

## Решение проблем

### Активная точка не совпадает с реальной каруселью или шагом

#### Симптом

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

#### Вероятная причина

У родительской функциональности и `PageControl` нет общего источника истины.

#### Как проверить

- сравните текущий индекс содержимого с отрендеренным активным элементом `active`

#### Исправление

Управляйте и содержимым, и `PageControl` из одного родительского состояния.

### Точки прогресса анимируются, но ничего не переключается

#### Симптом

Заливка прогресса анимируется, но содержимое не меняется.

#### Вероятная причина

`PageControlProgress` отрендерен без соответствующей логики перехода между шагами в родительском сценарии.

#### Как проверить

- проверьте, обновляет ли родитель активное состояние по окончании анимации или по тику автопрокрутки

#### Исправление

Свяжите завершение прогресса с реальной логикой перехода или переключитесь на статичные элементы.

### Длинные последовательности трудно читать

#### Симптом

Ряд индикаторов становится трудно сосчитать или понять.

#### Вероятная причина

`PageControl` используется для слишком большого числа элементов.

#### Как проверить

- посчитайте видимые элементы и сопоставьте с тем, насколько пользователи их реально понимают

#### Исправление

Перейдите на `Pagination`, степпер с подписями или другой, более содержательный паттерн навигации.
