# PhoneField

## Кратко

`PhoneField` — [составной компонент](https://vega-ui.dev/ru/docs/glossary/#составной-компонент-compound-component), который объединяет поле телефона с маской и вложенный `Select` для кода страны. Это готовый слой ввода телефона для потребителя, а не просто свободная пара `Select` и `TextField`.

## Импорт

```tsx
import {
  PhoneField,
  PhoneFieldInput,
  PhoneFieldSelect,
  PhoneFieldSelectHiddenSelect,
  PhoneFieldSelectCombobox,
  PhoneFieldSelectValue,
  PhoneFieldSelectIcon,
  PhoneFieldSelectListbox,
  PhoneFieldSelectOption,
  type PhoneFieldProps,
} from '@vega-ui/react';
```

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

- `PhoneFieldProps`
- `PhoneFieldInputProps`
- `PhoneFieldSelectProps`

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

```tsx
import {
  Label,
  PhoneField,
  PhoneFieldInput,
  PhoneFieldSelect,
  PhoneFieldSelectCombobox,
  PhoneFieldSelectHiddenSelect,
  PhoneFieldSelectIcon,
  PhoneFieldSelectListbox,
  PhoneFieldSelectOption,
  PhoneFieldSelectValue,
} from '@vega-ui/react';
import { useId } from 'react';

export default function MinimalComposition() {
  const id = useId();

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)', width: '100%', maxWidth: 320 }}>
      <Label htmlFor={id}>Phone number</Label>
      <PhoneField defaultCode='US'>
        <PhoneFieldSelect>
          <PhoneFieldSelectHiddenSelect name='country' />
          <PhoneFieldSelectCombobox aria-label='Country'>
            <PhoneFieldSelectValue />
            <PhoneFieldSelectIcon />
          </PhoneFieldSelectCombobox>
          <PhoneFieldSelectListbox>
            <PhoneFieldSelectOption value='US'>US</PhoneFieldSelectOption>
            <PhoneFieldSelectOption value='GB'>GB</PhoneFieldSelectOption>
            <PhoneFieldSelectOption value='DE'>DE</PhoneFieldSelectOption>
          </PhoneFieldSelectListbox>
        </PhoneFieldSelect>
        <PhoneFieldInput id={id} name='phone' autoComplete='tel' />
      </PhoneField>
    </div>
  );
}
```

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

- `PhoneField`: состояние поля на уровне корня и контекст форматирования
- `PhoneFieldInput`: собственно поле ввода номера телефона

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

- `PhoneFieldSelect*`: вложенное семейство выбора кода страны на том же контракте подсистемы select
- `PhoneFieldSelectHiddenSelect`: нативное [участие в форме](https://vega-ui.dev/ru/docs/glossary/#участие-в-форме-form-participation) для кода страны

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

Типичная композиция:

1. `PhoneField`
2. `PhoneFieldSelect`
3. `PhoneFieldSelectHiddenSelect`
4. `PhoneFieldSelectCombobox`
5. `PhoneFieldSelectListbox`
6. `PhoneFieldSelectOption`
7. `PhoneFieldInput`

## Варианты

- Только поле ввода или полная композиция с выбором страны
- `strictMask` или нестрогий ввод
- Контролируемое значение поля или неконтролируемое использование в форме

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

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

## Частые ошибки

- Видимую отформатированную строку считают единственным [источником истины](https://vega-ui.dev/ru/docs/glossary/#источник-истины-source-of-truth).
- Смену страны не учитывают в логике валидации.
- Тестируют только набор с клавиатуры и пропускают вставку и автозаполнение.

## Примеры

### Базовый

#### Базовый: простая маска телефона по ISO-коду

Одного `code` достаточно для формата: маска держит код страны США и группирует цифры как `+1 201 555-0123`. Видимая `Label` называет поле, а `autoComplete='tel'` позволяет браузеру заполнить его.

```tsx
import { Label, PhoneField, PhoneFieldInput } from '@vega-ui/react';
import { useId } from 'react';

export default function SimpleIsoBasedPhoneMask() {
  const id = useId();

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)', width: '100%', maxWidth: 280 }}>
      <Label htmlFor={id}>Mobile number</Label>
      <PhoneField code='US'>
        <PhoneFieldInput id={id} name='phone' autoComplete='tel' placeholder='+1 201 555-0123' />
      </PhoneField>
    </div>
  );
}
```

#### Базовый: выбор страны с иконками флагов

Флаги из `@vega-ui/icons` стоят в каждом варианте рядом с названием и кодом страны. Страна контролируемая, поэтому триггер показывает только выбранный флаг, а его `aria-label` содержит название страны.

```tsx
import {
  Label,
  PhoneField,
  PhoneFieldInput,
  PhoneFieldSelect,
  PhoneFieldSelectCombobox,
  PhoneFieldSelectHiddenSelect,
  PhoneFieldSelectIcon,
  PhoneFieldSelectListbox,
  PhoneFieldSelectOption,
  PhoneFieldSelectValue,
} from '@vega-ui/react';
import { FlagDE, FlagFR, FlagGBUKM, FlagIN, FlagJP, FlagUS } from '@vega-ui/icons';
import { useId, useState } from 'react';

const countries = [
  { code: 'US', name: 'United States', callingCode: '+1', Flag: FlagUS },
  { code: 'GB', name: 'United Kingdom', callingCode: '+44', Flag: FlagGBUKM },
  { code: 'DE', name: 'Germany', callingCode: '+49', Flag: FlagDE },
  { code: 'FR', name: 'France', callingCode: '+33', Flag: FlagFR },
  { code: 'IN', name: 'India', callingCode: '+91', Flag: FlagIN },
  { code: 'JP', name: 'Japan', callingCode: '+81', Flag: FlagJP },
] as const;

export default function SelectWithCountryOptionsAndIcons() {
  const id = useId();
  const [country, setCountry] = useState<(typeof countries)[number]>(countries[1]);

  const onChangeCode = (code: string) => {
    const found = countries.find((item) => item.code === code);
    if (found) setCountry(found);
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)', width: '100%', maxWidth: 340 }}>
      <Label htmlFor={id}>Phone number</Label>
      <PhoneField code={country.code} onChangeCode={onChangeCode}>
        <PhoneFieldSelect>
          <PhoneFieldSelectHiddenSelect name='country' />
          <PhoneFieldSelectCombobox aria-label={`Country: ${country.name}`}>
            <PhoneFieldSelectValue>
              <country.Flag width={20} height={15} aria-hidden='true' />
            </PhoneFieldSelectValue>
            <PhoneFieldSelectIcon />
          </PhoneFieldSelectCombobox>
          <PhoneFieldSelectListbox>
            {countries.map(({ code, name, callingCode, Flag }) => (
              <PhoneFieldSelectOption key={code} value={code} label={name}>
                <span style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-6)' }}>
                  <Flag width={20} height={15} aria-hidden='true' />
                  {name}
                </span>
                <span style={{ opacity: 0.7 }}>{callingCode}</span>
              </PhoneFieldSelectOption>
            ))}
          </PhoneFieldSelectListbox>
        </PhoneFieldSelect>
        <PhoneFieldInput id={id} name='phone' autoComplete='tel' />
      </PhoneField>
    </div>
  );
}
```

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

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

Отформатированным текстом владеет родитель, выводит из него сохраняемые цифры и очищает его пустой строкой. Подсказка сообщает, что номер ещё не полный.

```tsx
import { Button, Label, PhoneField, PhoneFieldInput, Text } from '@vega-ui/react';
import { useId, useState } from 'react';

export default function ControlledValueWithClearAction() {
  const id = useId();
  const [phone, setPhone] = useState('+1 415 555-0132');
  const digits = phone.replace(/\D/g, '');

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)', width: '100%', maxWidth: 320 }}>
      <Label htmlFor={id}>Phone for delivery updates</Label>
      <div style={{ display: 'flex', gap: 'var(--spacing-4)' }}>
        <PhoneField code='US' value={phone} onChangeValue={setPhone} style={{ flex: 1 }}>
          <PhoneFieldInput id={id} autoComplete='tel' />
        </PhoneField>
        <Button variant='secondary' appearance='ghost' disabled={phone === ''} onClick={() => setPhone('')}>
          Clear
        </Button>
      </div>
      <Text size={2} style={{ color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums', marginBlockStart: 'var(--spacing-2)' }}>
        {digits.length === 11 ? `Stored as +${digits}` : 'Enter all 10 digits of the number.'}
      </Text>
    </div>
  );
}
```

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

#### Форма/интеграция: отправка неконтролируемой нативной формы

Поле отправляет отформатированный номер под `name='phone'`, а `PhoneFieldSelectHiddenSelect` — страну под `name='country'`. `required` — нативная валидация настоящего поля ввода.

```tsx
import {
  Button,
  Label,
  PhoneField,
  PhoneFieldInput,
  PhoneFieldSelect,
  PhoneFieldSelectCombobox,
  PhoneFieldSelectHiddenSelect,
  PhoneFieldSelectIcon,
  PhoneFieldSelectListbox,
  PhoneFieldSelectOption,
  PhoneFieldSelectValue,
  Text,
} from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';

export default function UncontrolledNativeFormSubmit() {
  const id = useId();
  const [submitted, setSubmitted] = useState('');

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    setSubmitted(`country=${data.get('country')}, phone=${data.get('phone')}`);
  };

  return (
    <form onSubmit={onSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)', width: '100%', maxWidth: 340 }}>
      <Label htmlFor={id}>Phone number</Label>
      <PhoneField defaultCode='DE'>
        <PhoneFieldSelect>
          <PhoneFieldSelectHiddenSelect name='country' />
          <PhoneFieldSelectCombobox aria-label='Country'>
            <PhoneFieldSelectValue />
            <PhoneFieldSelectIcon />
          </PhoneFieldSelectCombobox>
          <PhoneFieldSelectListbox>
            <PhoneFieldSelectOption value='DE'>DE</PhoneFieldSelectOption>
            <PhoneFieldSelectOption value='AT'>AT</PhoneFieldSelectOption>
            <PhoneFieldSelectOption value='CH'>CH</PhoneFieldSelectOption>
          </PhoneFieldSelectListbox>
        </PhoneFieldSelect>
        <PhoneFieldInput id={id} name='phone' autoComplete='tel' required />
      </PhoneField>
      <Button type='submit' style={{ alignSelf: 'flex-end', marginBlockStart: 'var(--spacing-9)' }}>Request a callback</Button>
      {submitted && (
        <Text size={2} role='status' style={{ color: 'var(--label-secondary)', marginBlockStart: 'var(--spacing-4)' }}>
          Submitted: {submitted}
        </Text>
      )}
    </form>
  );
}
```

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

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

Список стран открывается в верхнем слое поверх модального диалога. Форма читает оба значения через `FormData` и закрывает диалог при отправке.

```tsx
import type { CSSProperties, FormEvent } from 'react';
import {
  Button,
  Dialog,
  DialogClose,
  DialogContent,
  DialogTrigger,
  Heading,
  Icon,
  IconButton,
  Label,
  PhoneField,
  PhoneFieldInput,
  PhoneFieldSelect,
  PhoneFieldSelectCombobox,
  PhoneFieldSelectHiddenSelect,
  PhoneFieldSelectIcon,
  PhoneFieldSelectListbox,
  PhoneFieldSelectOption,
  PhoneFieldSelectValue,
  Text,
} from '@vega-ui/react';
import { XIcon } from '@vega-ui/icons';
import { useId, useState } from 'react';

export default function FullFieldInsideADialogForm() {
  const id = useId();
  const [open, setOpen] = useState(false);
  const [contact, setContact] = useState('');

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    setContact(`${data.get('phone')} (${data.get('country')})`);
    setOpen(false);
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-8)', width: '100%' }}>
      <Dialog open={open} onChangeOpen={setOpen}>
        <DialogTrigger asChild>
          <Button>Add emergency contact</Button>
        </DialogTrigger>
        <DialogContent aria-labelledby={`${id}-title`}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--spacing-8)' }}>
            <Heading id={`${id}-title`} as='h2' size={4} fontWeight={600}>Emergency contact</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>
          <form
            onSubmit={onSubmit}
            style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)', marginBlockStart: 'var(--spacing-12)' }}
          >
            <Label htmlFor={id}>Phone number</Label>
            <PhoneField defaultCode='FR'>
              <PhoneFieldSelect>
                <PhoneFieldSelectHiddenSelect name='country' />
                <PhoneFieldSelectCombobox aria-label='Country'>
                  <PhoneFieldSelectValue />
                  <PhoneFieldSelectIcon />
                </PhoneFieldSelectCombobox>
                <PhoneFieldSelectListbox>
                  <PhoneFieldSelectOption value='FR'>FR</PhoneFieldSelectOption>
                  <PhoneFieldSelectOption value='BE'>BE</PhoneFieldSelectOption>
                  <PhoneFieldSelectOption value='CH'>CH</PhoneFieldSelectOption>
                </PhoneFieldSelectListbox>
              </PhoneFieldSelect>
              <PhoneFieldInput id={id} name='phone' autoComplete='tel' required />
            </PhoneField>
            <Button type='submit' style={{ alignSelf: 'flex-end', marginBlockStart: 'var(--spacing-9)' }}>Save contact</Button>
          </form>
        </DialogContent>
      </Dialog>
      {contact && <Text size={2} role='status' style={{ color: 'var(--label-secondary)' }}>Emergency contact: {contact}</Text>}
    </div>
  );
}
```

### Ошибка

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

Номер проверяется при потере фокуса и при отправке. Неполный номер ставит `error` на поле и `aria-invalid` на поле ввода и связывает сообщение через `aria-describedby`; ввод последней цифры снимает ошибку.

```tsx
import { Button, HelperText, Label, PhoneField, PhoneFieldInput, Text } from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';

const countDigits = (phone: string) => phone.replace(/\D/g, '').length;

export default function InvalidNumberState() {
  const id = useId();
  const errorId = `${id}-error`;
  const [phone, setPhone] = useState('');
  const [invalid, setInvalid] = useState(false);
  const [saved, setSaved] = useState(false);

  const onChangeValue = (next: string) => {
    setPhone(next);
    if (invalid && countDigits(next) === 11) setInvalid(false);
  };

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    if (countDigits(phone) !== 11) {
      setInvalid(true);
      setSaved(false);
      return;
    }

    setSaved(true);
  };

  return (
    <form onSubmit={onSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)', width: '100%', maxWidth: 320 }}>
      <Label htmlFor={id}>Phone for two-factor codes</Label>
      <PhoneField code='US' value={phone} onChangeValue={onChangeValue} error={invalid}>
        <PhoneFieldInput
          id={id}
          autoComplete='tel'
          placeholder='+1 201 555-0123'
          aria-invalid={invalid}
          aria-describedby={invalid ? errorId : undefined}
          onBlur={() => setInvalid(countDigits(phone) > 1 && countDigits(phone) !== 11)}
        />
      </PhoneField>
      {invalid && <HelperText id={errorId} error>Enter all 10 digits of the US number.</HelperText>}
      <Button type='submit' style={{ alignSelf: 'flex-end', marginBlockStart: 'var(--spacing-9)' }}>Send a code</Button>
      {saved && (
        <Text size={2} role='status' style={{ color: 'var(--label-secondary)', marginBlockStart: 'var(--spacing-4)' }}>
          A code was sent to {phone}.
        </Text>
      )}
    </form>
  );
}
```

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

#### Отключённый: унаследовано от fieldset

Поле ввода и триггер страны — нативные элементы, поэтому отключённый `Fieldset` отключает оба и исключает их из формы. Переключатель меняет это состояние.

```tsx
import {
  Fieldset,
  Label,
  PhoneField,
  PhoneFieldInput,
  PhoneFieldSelect,
  PhoneFieldSelectCombobox,
  PhoneFieldSelectHiddenSelect,
  PhoneFieldSelectIcon,
  PhoneFieldSelectListbox,
  PhoneFieldSelectOption,
  PhoneFieldSelectValue,
  Switch,
  SwitchIndicator,
  SwitchInput,
} from '@vega-ui/react';
import { useId, useState } from 'react';

export default function DisabledInput() {
  const switchId = useId();
  const phoneId = useId();
  const [sameAsProfile, setSameAsProfile] = useState(true);

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-8)', width: '100%', maxWidth: 340 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-6)' }}>
        <Switch>
          <SwitchInput
            id={switchId}
            checked={sameAsProfile}
            onChange={(event) => setSameAsProfile(event.currentTarget.checked)}
          />
          <SwitchIndicator />
        </Switch>
        <Label htmlFor={switchId}>Use the number from my profile</Label>
      </div>
      <Fieldset
        disabled={sameAsProfile}
        style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)' }}
      >
        <Label htmlFor={phoneId}>Billing phone</Label>
        <PhoneField defaultValue='+44 7400 123456'>
          <PhoneFieldSelect>
            <PhoneFieldSelectHiddenSelect name='country' />
            <PhoneFieldSelectCombobox aria-label='Country'>
              <PhoneFieldSelectValue />
              <PhoneFieldSelectIcon />
            </PhoneFieldSelectCombobox>
            <PhoneFieldSelectListbox>
              <PhoneFieldSelectOption value='GB'>GB</PhoneFieldSelectOption>
              <PhoneFieldSelectOption value='IE'>IE</PhoneFieldSelectOption>
            </PhoneFieldSelectListbox>
          </PhoneFieldSelect>
          <PhoneFieldInput id={phoneId} name='billingPhone' autoComplete='tel' />
        </PhoneField>
      </Fieldset>
    </div>
  );
}
```

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

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

С `strictMask={false}` вставленный номер сохраняет свой код страны, а поле переключает страну под него; `onChangeCode` сообщает определённую страну. Со строгой маской по умолчанию та же вставка была бы приведена к коду текущей страны.

```tsx
import {
  Label,
  PhoneField,
  PhoneFieldInput,
  PhoneFieldSelect,
  PhoneFieldSelectCombobox,
  PhoneFieldSelectIcon,
  PhoneFieldSelectListbox,
  PhoneFieldSelectOption,
  PhoneFieldSelectValue,
  Text,
} from '@vega-ui/react';
import { useId, useState } from 'react';

export default function NonStrictMaskForLenientPasteBehavior() {
  const id = useId();
  const [country, setCountry] = useState('US');

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)', width: '100%', maxWidth: 340 }}>
      <Label htmlFor={id}>Customer phone</Label>
      <PhoneField defaultCode='US' strictMask={false} onChangeCode={setCountry}>
        <PhoneFieldSelect>
          <PhoneFieldSelectCombobox aria-label='Country'>
            <PhoneFieldSelectValue />
            <PhoneFieldSelectIcon />
          </PhoneFieldSelectCombobox>
          <PhoneFieldSelectListbox>
            <PhoneFieldSelectOption value='US'>US</PhoneFieldSelectOption>
            <PhoneFieldSelectOption value='GB'>GB</PhoneFieldSelectOption>
            <PhoneFieldSelectOption value='DE'>DE</PhoneFieldSelectOption>
            <PhoneFieldSelectOption value='FR'>FR</PhoneFieldSelectOption>
            <PhoneFieldSelectOption value='JP'>JP</PhoneFieldSelectOption>
          </PhoneFieldSelectListbox>
        </PhoneFieldSelect>
        <PhoneFieldInput id={id} autoComplete='tel' />
      </PhoneField>
      <Text size={2} style={{ color: 'var(--label-secondary)', marginBlockStart: 'var(--spacing-2)' }}>
        Paste +33 6 12 34 56 78 or +81 90-1234-5678: the country switches to match. Detected: {country}
      </Text>
    </div>
  );
}
```

#### Пограничный случай: эмодзи-флаги

Варианты и значение принимают любое содержимое, поэтому эмодзи-флаги работают без файлов изображений. Триггер читает выбранный код из `useSelectContext` и показывает только флаг — как изображение с названием страны.

```tsx
import {
  Label,
  PhoneField,
  PhoneFieldInput,
  PhoneFieldSelect,
  PhoneFieldSelectCombobox,
  PhoneFieldSelectHiddenSelect,
  PhoneFieldSelectIcon,
  PhoneFieldSelectListbox,
  PhoneFieldSelectOption,
  PhoneFieldSelectValue,
  useSelectContext,
} from '@vega-ui/react';
import { useId } from 'react';

const countries = [
  { code: 'BR', flag: '🇧🇷', name: 'Brazil', callingCode: '+55' },
  { code: 'MX', flag: '🇲🇽', name: 'Mexico', callingCode: '+52' },
  { code: 'US', flag: '🇺🇸', name: 'United States', callingCode: '+1' },
  { code: 'FR', flag: '🇫🇷', name: 'France', callingCode: '+33' },
] as const;

function SelectedFlag() {
  const { selected } = useSelectContext();
  const country = countries.find((item) => item.code === selected);

  if (!country) return null;

  return (
    <span role='img' aria-label={country.name}>
      {country.flag}
    </span>
  );
}

export default function EmojiFlags() {
  const id = useId();

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)', width: '100%', maxWidth: 340 }}>
      <Label htmlFor={id}>WhatsApp number</Label>
      <PhoneField defaultCode='BR'>
        <PhoneFieldSelect>
          <PhoneFieldSelectHiddenSelect name='country' />
          <PhoneFieldSelectCombobox aria-label='Country'>
            <PhoneFieldSelectValue>
              <SelectedFlag />
            </PhoneFieldSelectValue>
            <PhoneFieldSelectIcon />
          </PhoneFieldSelectCombobox>
          <PhoneFieldSelectListbox>
            {countries.map((country) => (
              <PhoneFieldSelectOption key={country.code} value={country.code} label={country.name}>
                <span style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-6)' }}>
                  <span aria-hidden='true'>{country.flag}</span>
                  {country.name}
                </span>
                <span style={{ opacity: 0.7 }}>{country.callingCode}</span>
              </PhoneFieldSelectOption>
            ))}
          </PhoneFieldSelectListbox>
        </PhoneFieldSelect>
        <PhoneFieldInput id={id} name='whatsapp' autoComplete='tel' />
      </PhoneField>
    </div>
  );
}
```

## Анатомия

### Обзор

`PhoneField` согласует маску ввода, состояние страны и поведение вложенного select между корнем поля и семейством `PhoneFieldSelect*`.

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

#### `PhoneField`

Корневой провайдер форматирования телефона и состояния страны.

#### `PhoneFieldInput`

Видимое текстовое поле для номера.

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

#### `PhoneFieldSelect`

Корень вложенного выбора кода страны.

#### `PhoneFieldSelectHiddenSelect`

Нативное [участие в форме](https://vega-ui.dev/ru/docs/glossary/#участие-в-форме-form-participation) для значения страны.

#### `PhoneFieldSelectCombobox`

Видимый триггер выбора страны.

#### `PhoneFieldSelectValue`

Текущая выбранная страна или собственное содержимое для её отображения.

#### `PhoneFieldSelectIcon`

Визуальная подсказка для триггера страны.

#### `PhoneFieldSelectListbox`

Контейнер вариантов стран. Рендерится в верхнем слое через `popover="manual"`, поэтому отдельная часть-портал не нужна.

#### `PhoneFieldSelectOption`

Выбираемый вариант страны.

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

Типичная композиция:

1. `PhoneField`
2. `PhoneFieldSelect`
3. `PhoneFieldSelectCombobox`
4. `PhoneFieldSelectListbox`
5. `PhoneFieldInput`

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

```tsx
import {
  Label,
  PhoneField,
  PhoneFieldInput,
  PhoneFieldSelect,
  PhoneFieldSelectCombobox,
  PhoneFieldSelectHiddenSelect,
  PhoneFieldSelectIcon,
  PhoneFieldSelectListbox,
  PhoneFieldSelectOption,
  PhoneFieldSelectValue,
} from '@vega-ui/react';
import { useId } from 'react';

export default function MinimalComposition() {
  const id = useId();

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)', width: '100%', maxWidth: 320 }}>
      <Label htmlFor={id}>Phone number</Label>
      <PhoneField defaultCode='US'>
        <PhoneFieldSelect>
          <PhoneFieldSelectHiddenSelect name='country' />
          <PhoneFieldSelectCombobox aria-label='Country'>
            <PhoneFieldSelectValue />
            <PhoneFieldSelectIcon />
          </PhoneFieldSelectCombobox>
          <PhoneFieldSelectListbox>
            <PhoneFieldSelectOption value='US'>US</PhoneFieldSelectOption>
            <PhoneFieldSelectOption value='GB'>GB</PhoneFieldSelectOption>
            <PhoneFieldSelectOption value='DE'>DE</PhoneFieldSelectOption>
          </PhoneFieldSelectListbox>
        </PhoneFieldSelect>
        <PhoneFieldInput id={id} name='phone' autoComplete='tel' />
      </PhoneField>
    </div>
  );
}
```

`Label` называет поле ввода, у триггера страны свой `aria-label`, а скрытый select отправляет страну вместе с номером. Без `PhoneFieldSelect` поле всё равно форматирует номер по одному `code`.

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

#### Поле ввода вне `PhoneField`

Это ломает общее форматирование и согласование со страной.

#### Вложенный select рассматривается как не связанный с полем ввода

Смена страны всё равно должна согласовываться с маской телефона и моделью валидации.

#### Богатый рендер вариантов без сохранения одной выбираемой строки

Это ослабляет поведение клавиатуры и списка (listbox).
