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

## Паттерны

### Полиморфный триггер

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

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

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

- гарантируйте ровно один дочерний элемент
- сохраняйте подходящий для задачи семантический элемент

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

- гибко
- проще ошибиться, чем при фиксированном рендере

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

export default function PolymorphicTrigger() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <IconButton variant='secondary' appearance='ghost' aria-label='Keyboard shortcuts'>
          <Icon><KeyboardIcon /></Icon>
        </IconButton>
      </DialogTrigger>
      <DialogContent aria-labelledby='shortcuts-title'>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--spacing-8)' }}>
          <Heading id='shortcuts-title' as='h2' size={4} fontWeight={600}>Keyboard shortcuts</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 size={3}>Ctrl+K opens search, Ctrl+/ toggles the sidebar.</Text>
        <DialogClose asChild>
          <Button style={{ marginBlockStart: 'var(--spacing-12)' }}>Got it</Button>
        </DialogClose>
      </DialogContent>
    </Dialog>
  );
}
```

### Стилизованная ссылка-действие

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

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

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

- выбирайте элемент `<a>` осознанно
- сохраняйте семантику навигации явной

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

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

```tsx
import { Button, Icon } from '@vega-ui/react';
import { BookOpenIcon } from '@vega-ui/icons';

export default function StyledLinkAction() {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-4)' }}>
      <Button type='button'>Create API key</Button>
      <Button asChild variant='secondary' appearance='ghost'>
        <a href='/docs/api/authentication'>
          <Icon size='sm'><BookOpenIcon /></Icon>
          Read the API guide
        </a>
      </Button>
    </div>
  );
}
```

### Написание низкоуровневых обёрток

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

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

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

- тщательно тестируйте слияние обработчиков событий
- сохраняйте семантику выбранного дочернего элемента явной

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

- фундамент для полиморфизма
- легко внести тонкие семантические регрессии

```tsx
import { type ReactNode, useState } from 'react';
import { Button, Slot } from '@vega-ui/react';

interface TrackClickProps {
  event: string
  onTrack(event: string): void
  children: ReactNode
}

function TrackClick({ event, onTrack, children }: TrackClickProps) {
  return <Slot onClick={() => onTrack(`analytics: ${event}`)}>{children}</Slot>;
}

export default function LowLevelWrapperAuthoring() {
  const [log, setLog] = useState<string[]>([]);

  const append = (entry: string) => {
    setLog((current) => [...current, entry]);
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 'var(--spacing-8)', width: '100%' }}>
      <div style={{ display: 'flex', gap: 'var(--spacing-4)' }}>
        <TrackClick event='upgrade_clicked' onTrack={append}>
          <Button type='button' onClick={() => append('button: open checkout')}>Upgrade plan</Button>
        </TrackClick>
        <Button type='button' variant='secondary' appearance='ghost' onClick={() => setLog([])}>Clear log</Button>
      </div>
      <ol aria-label='Click handlers in call order' style={{ margin: 0, paddingInlineStart: 'var(--spacing-12)', color: 'var(--label-secondary)', fontSize: 'var(--font-size-2)', fontFamily: 'var(--code-font-family)' }}>
        {log.map((entry, index) => <li key={index}>{entry}</li>)}
      </ol>
    </div>
  );
}
```

## Сравнение

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

Используйте `Slot`, только когда строите низкоуровневую полиморфную композицию. В обычном продуктовом коде предпочитайте высокоуровневые API `asChild`.

### `Slot` или `asChild` у компонентов

- Используйте `Slot`, когда пишете примитивы или обёртки.
- Используйте `asChild` у существующих компонентов VegaUI в продуктовом коде.

### `Slot` или прямой рендер элемента

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

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

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

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

- компонент уже предоставляет `asChild`
- вам нужен просто обычный элемент

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

### Композиция падает во время выполнения

#### Симптом

Рендер завершается ошибкой `Slot` о форме дочернего элемента.

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

Дочерний элемент не является ровно одним валидным React-элементом.

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

- посмотрите, что на самом деле передаётся в `children`

#### Решение

Передавайте один валидный React-элемент, а не обычный текст, массивы или фрагменты с несколькими узлами.

### Интерфейс выглядит правильно, но семантика неверна

#### Симптом

Элемент в слоте выглядит правильно стилизованным, но поведение или доступность нарушены.

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

У выбранного дочернего элемента неподходящая для задачи семантика.

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

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

#### Решение

Выберите правильный дочерний элемент и помните, что `Slot` меняет только рендер, но не семантику.

### Поведение событий кажется неожиданным

#### Симптом

Обработчики родителя и дочернего элемента словно конфликтуют.

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

Слияние пропсов создало пересекающееся поведение.

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

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

#### Решение

Упростите композицию и явно определите, кто отвечает за логику взаимодействия.
