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

## Паттерны

### Блок расширенных фильтров

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

- Один второстепенный блок формы должен оставаться скрытым, пока не понадобится.

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

- Триггер должен явно говорить, что станет доступно после раскрытия.
- Аккуратно обращайтесь со скрытыми ошибками формы, если раздел можно свернуть.

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

- Основная форма остаётся компактной.
- При слабых подписях может слишком хорошо прятать сложность или валидацию.

```tsx
import { useState } from 'react';
import {
  Button,
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
  Label,
  Text,
  TextField,
  TextFieldInput,
} from '@vega-ui/react';

export default function AdvancedFilters() {
  const [open, setOpen] = useState(false);
  const [owner, setOwner] = useState('');
  const [tag, setTag] = useState('');

  const activeCount = [owner, tag].filter((value) => value.trim() !== '').length;

  let summary = 'No advanced filters applied';
  if (activeCount === 1) summary = '1 advanced filter applied';
  if (activeCount > 1) summary = `${activeCount} advanced filters applied`;

  const clear = () => {
    setOwner('');
    setTag('');
  };

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-6)', width: '100%', maxWidth: 420 }}>
      <Collapsible open={open} onChangeOpen={setOpen}>
        <div style={{ display: 'flex', gap: 'var(--spacing-4)', alignItems: 'center' }}>
          <CollapsibleTrigger asChild>
            <Button size='sm' variant='secondary' appearance='ghost'>
              {open ? 'Hide advanced filters' : 'Show advanced filters'}
            </Button>
          </CollapsibleTrigger>
          {activeCount > 0 && (
            <Button size='sm' variant='secondary' appearance='ghost' onClick={clear}>Clear</Button>
          )}
        </div>
        <CollapsibleContent>
          <div style={{ display: 'grid', gap: 'var(--spacing-8)' }}>
            <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
              <Label htmlFor='filter-owner'>Owner</Label>
              <TextField>
                <TextFieldInput id='filter-owner' value={owner} onChange={(event) => setOwner(event.target.value)} />
              </TextField>
            </div>
            <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
              <Label htmlFor='filter-tag'>Tag</Label>
              <TextField>
                <TextFieldInput id='filter-tag' value={tag} onChange={(event) => setTag(event.target.value)} />
              </TextField>
            </div>
          </div>
        </CollapsibleContent>
      </Collapsible>
      <Text size={2} style={{ color: 'var(--label-secondary)' }}>
        {summary}
      </Text>
    </div>
  );
}
```

### Встроенная справка

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

- Один второстепенный блок пояснений должен оставаться рядом с текущим содержимым.

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

- Держите скрытое содержимое достаточно коротким, чтобы триггер по-прежнему воспринимался как встроенное раскрытие.
- Предпочитайте `Collapsible` вместо `Popover`, когда пояснение должно оставаться в потоке документа.

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

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

```tsx
import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger, Text } from '@vega-ui/react';

const lines = [
  { label: 'Team plan · 8 seats × $12', amount: '$96.00' },
  { label: 'Extra storage · 200 GB', amount: '$20.00' },
  { label: 'Annual discount −10%', amount: '−$11.60' },
  { label: 'VAT 19%', amount: '$19.84' },
];

export default function PriceBreakdown() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-6)', width: '100%', maxWidth: 360 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 'var(--spacing-8)' }}>
        <Text size={3} fontWeight={500}>Total due today</Text>
        <Text size={3} fontWeight={500} style={{ fontVariantNumeric: 'tabular-nums' }}>$124.24</Text>
      </div>
      <Collapsible>
        <CollapsibleTrigger asChild>
          <Button size='sm' variant='secondary' appearance='ghost' style={{ justifySelf: 'start' }}>Price breakdown</Button>
        </CollapsibleTrigger>
        <CollapsibleContent>
          <dl style={{ margin: 0, display: 'grid', gridTemplateColumns: '1fr auto', gap: 'var(--spacing-4) var(--spacing-8)' }}>
            {lines.map((line) => (
              <div key={line.label} style={{ display: 'contents' }}>
                <dt><Text size={2} style={{ color: 'var(--label-secondary)' }}>{line.label}</Text></dt>
                <dd style={{ margin: 0, textAlign: 'end' }}><Text size={2} style={{ fontVariantNumeric: 'tabular-nums' }}>{line.amount}</Text></dd>
              </div>
            ))}
          </dl>
        </CollapsibleContent>
      </Collapsible>
    </div>
  );
}
```

### Второстепенные детали в оверлеях

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

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

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

- Содержимое collapsible должно явно оставаться второстепенным по отношению к основной задаче.
- Проверяйте динамическую высоту на реалистичном содержимом оверлея.

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

- Добавляет постепенное раскрытие без ещё одного оверлея.
- При злоупотреблении плотные панели сложнее просматривать.

```tsx
import type { CSSProperties } from 'react';
import {
  Button,
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
  Dialog,
  DialogClose,
  DialogContent,
  DialogTrigger,
  Heading,
  Icon,
  IconButton,
  Text,
} from '@vega-ui/react';
import { XIcon } from '@vega-ui/icons';

const stackTrace = `ExportError: Request timed out after 30000 ms
    at fetchRows (exports/worker.ts:118:11)
    at async buildCsv (exports/csv.ts:42:20)
    at async runExport (exports/run.ts:77:5)
    at async Queue.process (queue/index.ts:203:9)
Request ID: 7f3c9a1e-52b4-4f0e-9d8a-0c61b2e4a913`;

export default function ErrorDetailsDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant='secondary' appearance='ghost'>Show failed export</Button>
      </DialogTrigger>
      <DialogContent aria-labelledby='export-error-title' aria-describedby='export-error-description' style={{ maxWidth: 480 }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--spacing-8)' }}>
          <Heading id='export-error-title' as='h2' size={4} fontWeight={600}>Export failed</Heading>
          <DialogClose asChild>
            <IconButton
              aria-label='Close'
              variant='secondary'
              appearance='ghost'
              style={{ flexShrink: 0, '--icon-button-size': '24px', '--icon-button-br-ratio': 0.5 } as CSSProperties}
            >
              <Icon size='xs'><XIcon strokeWidth={2.5} /></Icon>
            </IconButton>
          </DialogClose>
        </div>
        <Text id='export-error-description' size={3} asChild>
          <p style={{ marginBlock: 'var(--spacing-4) var(--spacing-6)', color: 'var(--label-secondary)' }}>The CSV export of 48 210 rows did not finish. Try again with a shorter date range.</p>
        </Text>
        <Collapsible>
          <CollapsibleTrigger asChild>
            <Button size='sm' variant='secondary' appearance='ghost'>Technical details</Button>
          </CollapsibleTrigger>
          <CollapsibleContent>
            <pre style={{ margin: 0, marginBlockStart: 'var(--spacing-4)', maxHeight: 160, overflow: 'auto', fontSize: 'var(--font-size-1)', lineHeight: 1.6, padding: 'var(--spacing-6)', borderRadius: 'var(--radius-3)', background: 'var(--fills-tertiary)', color: 'var(--label-secondary)' }}>
              {stackTrace}
            </pre>
          </CollapsibleContent>
        </Collapsible>
        <form method='dialog' style={{ display: 'flex', gap: 'var(--spacing-4)', justifyContent: 'flex-end', marginBlockStart: 'var(--spacing-12)' }}>
          <Button type='submit' variant='secondary' appearance='ghost'>Close</Button>
          <Button type='submit'>Try again</Button>
        </form>
      </DialogContent>
    </Dialog>
  );
}
```

## Сравнение

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

Используйте `Collapsible`, когда достаточно одного отдельного раскрывающегося раздела.

### `Collapsible` или `Accordion`

Используйте `Collapsible`, когда:

- функции нужен один раскрывающийся блок
- координация общего состояния между разделами не нужна

Используйте `Accordion`, когда:

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

Главный компромисс: `Collapsible` легче и проще, а `Accordion` даёт согласованное групповое раскрытие.

### `Collapsible` или `Popover`

Используйте `Collapsible`, когда:

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

Используйте `Popover`, когда:

- содержимое должно всплывать рядом с триггером

Главный компромисс: `Collapsible` сохраняет непрерывность раскладки, а `Popover` даёт более сильное локальное отделение.

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

- Достаточно одного раскрывающегося раздела.
- Содержимое должно оставаться встроенным.
- Функции нужна только простая модель раскрытия «триггер — содержимое».

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

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

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

### Состояния открытия и скрытия рассинхронизированы

#### Симптом

Логика родителя видит `open`, но содержимое ещё в процессе визуального перехода или уже скрыто.

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

Функция считает `onChangeOpen` и `onChangeHidden` одним и тем же сигналом жизненного цикла.

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

- Логируйте оба колбэка при открытии и закрытии.
- Сравните визуальное состояние с моментами вызова колбэков.

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

- Используйте `onChangeOpen` для владения состоянием.
- Используйте `onChangeHidden`, только когда раскладка или измерения зависят от фактической видимости.

### Пользовательский триггер перестаёт вести себя как триггер

#### Симптом

Раскрытие рендерится, но после пользовательской композиции поведение клавиатуры или семантика становятся неясными.

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

`asChild` использован с элементом, который не сохраняет поведение триггера.

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

- Изучите отрендеренный элемент триггера.
- Проверьте клик и поведение клавиатуры, а не только внешний вид.

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

- Используйте с `asChild` семантически подходящий дочерний элемент.
- Триггер должен оставаться явно интерактивным и доступным с клавиатуры.

### Динамическая высота содержимого обрезается или прыгает

#### Симптом

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

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

Раздел проверяли только с коротким статичным содержимым, а не с реальной динамической раскладкой.

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

- Раскройте раздел с реалистичным асинхронным содержимым или содержимым переменной высоты.
- Проверьте переход при росте и сворачивании содержимого.

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

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