<!-- GENERATED by scripts/build-llms.mjs from llms/forms.md — do not edit this file. -->

# `lr-phone-input`

- **Import** `import '@aceshooting/lyra-ui/components/lr-phone-input.js';` (stable tag alias; registers the tag)
- **Class** `LyraPhoneInput`, also available unregistered from `@aceshooting/lyra-ui/components/forms/phone-input/phone-input.class.js`
- **Family** `components/forms/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 14 parts, 17 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-phone-input`

A form-associated, country-aware telephone field. The submitted `value` is either canonical E.164
(for example `+352621123456`) or `''` while the editable input is empty, incomplete, or invalid.
Numbering-plan metadata and national formatting stay outside Lyra's base bundle: supply a
synchronous `LyraPhoneNumberAdapter`, or lazily create one from a `libphonenumber-js`-compatible module
with `loadLibphonenumberAdapter()`. Without an adapter, already-international E.164 input still
normalizes and validates; national input remains editable with `incomplete` validity. The loader
returns its discovered country catalog as a frozen array of frozen records.

For `default-country`, removing the attribute safely uses the existing country fallback while
leaving property   readback at native `null`; an existing selected country remains selected.
Explicit empty   strings remain empty, and later valid default-country values work normally.

Host `aria-describedby` references resolve onto the telephone input before its local hint/error
guidance. References track missing IDs, target replacement, removal/reinsertion, reconnect, and
adoption. The country selector keeps its separate existing accessible-name contract.

The country selector keeps the real native `<select>` (localized full country names in its popup,
native mobile pickers, keyboard type-ahead) but stretches it invisibly over a compact decorative
trigger showing the selected alpha-2 code plus a design-system chevron — long country names never
clip the closed control and the adjacent calling code isn't repeated. With `flags`, the trigger
also shows the selected country's `<lr-flag>`.

Public `--lr-phone-input-*` theme inputs stay undeclared on the host, so an ancestor theme wrapper
can override size and pill fallbacks; a value set directly on the element still wins.
The native country-selector target retains the shared icon-button hit floor, and the wrapper uses
the same rendered action-height ladder as input, number-input, and segmented time-input.

**Types:**

```ts
type LyraPhoneNumberStatus = "empty" | "incomplete" | "invalid" | "valid";

interface LyraPhoneCountry {
  readonly code: string; // ISO 3166-1 alpha-2
  readonly callingCode: string; // no leading "+"
  readonly label?: string; // overrides Intl.DisplayNames
}

type LyraPhoneNumberParseResult =
  | {
      status: "empty" | "incomplete" | "invalid";
      formatted?: string; // best-effort editable display text
      country?: string; // detected ISO alpha-2 code
    }
  | {
      status: "valid";
      e164: string; // required and E.164-shaped on the only successful branch
      formatted?: string;
      country?: string;
    };

interface LyraPhoneNumberAdapter {
  readonly countries?: readonly LyraPhoneCountry[];
  parse(input: string, country?: string): LyraPhoneNumberParseResult;
}
```

**Properties:**

- `value: string = ''` — canonical E.164 form/submission value. A programmatic assignment is parsed
  and normalized synchronously but emits no user event.
- `name: string = ''`, `disabled: boolean = false`, `required: boolean = false` — native-like
  form-control properties supplied by `FormAssociated`; inherited disabled fieldsets are included
  through `effectiveDisabled`.
- `defaultValue: string = ''` (attribute `value`) is the reset target, and `customError: string |
null` (attribute `custom-error`) carries a consumer-supplied validation message.
- `adapter?: LyraPhoneNumberAdapter` (attribute: false) — synchronous numbering-plan
  parser/formatter. No metadata implementation is imported by the component itself. Runtime
  results are validated exhaustively: unknown statuses, hostile getters, wrong optional-field
  types, and a `valid` result without E.164 all fail closed to `invalid`.
- `countries?: readonly LyraPhoneCountry[]` (attribute: false) — `undefined` discovers
  `adapter.countries`; every supplied array, including `[]`, is authoritative. Rows are copied and
  validated at the boundary; a row without a two-letter code or 1–3 digit calling code, a duplicate,
  or a throwing getter is skipped without hiding later valid rows. Explicit and adapter-provided
  catalogs are bounded to 512 rows and captured as frozen owned snapshots when assigned.
- `defaultCountry: string = ''` (attribute `default-country`) — selected when `country` has not been
  set explicitly.
- `flags: boolean = false` (reflected) — show the selected country's flag in the country trigger as
  `<lr-flag variant="compact" aria-label="">` (decorative; the native select already announces the
  country name). The `<lr-flag>` element definition is registered lazily the first time any
  `lr-phone-input` enables this, so nothing flag-related is bundled while it stays off. Flag
  _artwork_ still follows the standalone `<lr-flag>` contract: install the optional
  `@aceshooting/lyra-flags` peer and import
  `@aceshooting/lyra-ui/components/media/flag/flag-peer.js` once; without that registration the
  trigger simply omits the image. The open popup list stays text-only — a native `<option>` cannot
  contain elements.
- `size: LyraSize = 'm'` (reflected — the shared control ladder, so both `2xs`/`xs`/`s`/`m`/`l`/`xl`
  and `small`/`medium`/`large` are accepted; scales input padding, font size, and wrapper
  min-height; `size="s"` shares its outer control height with `lr-input`, `lr-select`, and
  `lr-combobox` without part overrides)
- `pill: boolean = false` (reflected) — rounds the field's corners to a full pill, mirroring
  `lr-input`'s own `pill`. It changes the private radius default to `--lr-radius-pill`, and the
  country trigger's leading corners follow; an inherited or direct `--lr-phone-input-radius`
  remains authoritative
- `country: string` — current uppercase ISO alpha-2 selection; falls back to `defaultCountry`, then
  the first explicit/adapter country. A requested or adapter-detected country absent from the
  effective catalog resolves to that same valid fallback before property, trigger, native select,
  calling-code, and parser projection. An empty effective catalog resolves to `''`. Changing the
  country reparses the editable number.
- `label: string = ''`, `hint: string = ''`, `errorText: string = ''` (attribute `error-text`) —
  visible form-field chrome; each has a matching named slot.
- `placeholder: string = ''` — forwarded to the native telephone input.
- `spellcheck: boolean = true`, `autocapitalize: string = ''`, `autoCorrect: string = ''`
  (attribute `autocorrect`) — forwarded to the internal telephone input's own `spellcheck`/
  `autocapitalize`/`autocorrect`; `spellcheck="false"` is parsed as `false` via a string-aware
  converter (Lit's default presence-based boolean converter would otherwise treat any attribute
  value, including the literal string `"false"`, as `true`).
- `accessibleLabel: string | null = null` (attribute `aria-label`) — forwarded to the internal
  telephone input. Name precedence is host `aria-label`, `phoneLabel`, visible `label`, then
  `placeholder`, and finally a localized generic "Phone" name, so a bare `<lr-phone-input>` with
  none of them set never reaches the accessibility tree unnamed. (The visible label part cannot
  stand in for it: it carries the native `hidden` attribute while there is no label text, which
  removes it from the accessibility tree entirely.)
- `phoneLabel: string = ''` (attribute `phone-label`) — explicit accessible-name override for the
  native telephone input.
- `countryLabel: string = 'Select'` (attribute `country-label`) — country-selector accessible name.
  Omitted copy uses the localized `select` message. Explicit text, including `'Select'` and `''`,
  wins over locale strings. Removing the attribute restores the declared `'Select'` property
  readback and resumes localization.
- `incompleteText: string = 'This phone number is incomplete.'` (attribute `incomplete-text`) —
  validation message for dial-like input that can still become valid with more digits. Omitted
  copy uses the localized `phoneInputIncomplete` message. Explicit nonempty text, including the
  English default, wins verbatim; an empty override retains the localized native error reason.
  Removing the attribute restores its declared English property default and resumes localization.
- `invalidText: string = 'The value is invalid.'` (attribute `invalid-text`) — completed-invalid
  validation message. Omitted copy uses localized `valueInvalid`; explicit nonempty text,
  including the English default, wins verbatim. An empty override retains the localized native
  error reason. Removing the attribute restores its declared English property default and resumes
  localization.
- `autocomplete: string = 'tel'`, `inputmode: 'tel'|'numeric'|'text' = 'tel'`,
  `enterkeyhint: string = ''` — forwarded to the internal `<input type="tel">`.
- `readonly: boolean = false` (reflected) — forwards to the native telephone input, locks the
  country selector and all user edit handlers, and bars validation while retaining focus,
  selection/copying, canonical form value, and submission.
- `autofocus: boolean = false` (reflected) — forwarded to the actual native telephone input; it is
  never stranded on the non-focusable host.
- readonly `input: HTMLInputElement | undefined` — the internal native telephone input.
- readonly `inputValue: string` — editable formatted/partial text, which remains available even when
  canonical `value` is `''`.
- `selectionStart`, `selectionEnd`, and `selectionDirection` — native selection getters/setters
  forwarded to the telephone input
- readonly `phoneStatus: LyraPhoneNumberStatus` — current parse state. The host also reflects it through
  `data-phone-status`.
- readonly `form`, `labels`, `validity`, `validationMessage`, `willValidate`, and
  `effectiveDisabled` — the shared form-associated native-like getters.

**Events:** each text edit emits native `InputEvent` `input` then `lr-input`; telephone-input commit
emits native `Event` `change` then `lr-change`; and a country pick emits both pairs in order:
`input`, `lr-input`, `change`, `lr-change`. Native events carry no custom detail; the aliases carry
`{ value, inputValue, country, valid, status }`.
Internal `focus`/`blur` are relayed once as realm-correct native `FocusEvent`s preserving `relatedTarget`.
`lr-invalid` has no detail and is the one bubbling/composed alias
when native validity fails. Programmatic value writes remain silent.

**Validity:** empty + `required` sets `valueMissing`; incomplete dial-like input sets `badInput`;
completed-invalid input sets `typeMismatch`; valid E.164 input clears all three. Partial or invalid
text remains in `inputValue`/the native input so validation never makes a number impossible to edit,
but its canonical submitted `value` is blank. Native validation feedback is anchored to the
telephone input, not the adjacent country selector.

**Methods:** `focus(options?)`, `blur()`, `select()`, `setSelectionRange()`, and `setRangeText()`
forward to the native telephone input. Range-text edits reparse the number and synchronize the
canonical value, form value, and validity.
`getForm()`, `setFormValue(value)`, `checkValidity()`, and `reportValidity()` come from
`FormAssociated`. `setCustomValidity(message)` sets or clears `customError` without discarding the
control's intrinsic phone-number validity. `resetValidity()` clears only that consumer error and
recomputes the current phone-number constraints; it does not change the editable/canonical value,
the reset default, or prior interaction state.
`form.reset()` restores the original declarative `value` and the default country.

**Slots:** `label`, `hint`, `error`, `country-prefix` (optional visual before the country selector,
such as a consumer-owned `<lr-flag>`; no flag package is imported automatically).

**CSS parts:** `form-control`, `form-control-label`, `input-wrapper`, `country-prefix`, `country`
(the selector region: invisible native select layered over the visual trigger), `country-select`,
`country-trigger` (visible, `aria-hidden` closed-state box), `flag` (the `<lr-flag>`, only with
`flags`), `country-code` (selected alpha-2 code, `data-placeholder` when no country exists),
`expand-icon`, `calling-code`, `input`, `hint`, `error`.

`error` is ordinary visible validation text referenced by the native telephone input through
`aria-describedby`, not a shadow `role="alert"`. Native invalid/focus feedback therefore has one
description path instead of being duplicated by a second live-region announcement.

**The required marker.** `required` with a non-empty `label` paints the library's shared marker on
`[part="form-control-label"]` — the one `::after` rule described above, not a copy of it, so
`--lr-form-control-required-content`, `--lr-form-control-required-color` and
`--lr-form-control-required-offset` retune or suppress it here exactly as they do on `lr-input`.
With no label text the part is hidden and no glyph is painted.

**Themeable custom properties:** `--lr-phone-input-padding-block` (scaled through the shared
form-control padding ladder), `--lr-phone-input-font-size`, and
`--lr-phone-input-control-min-height` (each scaled by `size`), plus `--lr-phone-input-control-height`
to pin an exact input-wrapper height (both floors and caps it — use it for pixel-matching an
`<lr-input>` or `<lr-select>` in the same toolbar row; undeclared by default, leaving the min height
as a floor only). The phone-number input and calling code are deliberately `dir="ltr"`/isolated
because telephone numbers are algorithmic content; surrounding form chrome and the country selector
inherit LTR/RTL and use logical spacing/borders.
The native country selector's hover and press backgrounds are supplemented in forced-colors mode
with dashed/solid `Highlight` outlines, so those states do not collapse when the UA flattens the
background tint to `Canvas`.

**Optional peer deps:** `libphonenumber-js` is declared optional but never imported by Lyra itself.
For full national parsing/formatting, install it in the consuming app and pass it through the
consumer-supplied lazy loader below. Because the import expression lives in consumer code, no
numbering metadata enters a bundle that does not opt in.

```ts
import "@aceshooting/lyra-ui/components/forms/phone-input/phone-input.js";
import { loadLibphonenumberAdapter } from "@aceshooting/lyra-ui/components/forms/phone-input/phone-input.class.js";

const phone = document.querySelector("lr-phone-input");
phone.adapter = await loadLibphonenumberAdapter(
  () => import("libphonenumber-js/min")
);
```

```html
<lr-phone-input
  name="mobile"
  label="Mobile number"
  hint="Used only for account security"
  default-country="LU"
  required
></lr-phone-input>
```

```ts
// Country flags in the trigger (optional): same peer contract as a standalone <lr-flag>.
import "@aceshooting/lyra-ui/components/media/flag/flag-peer.js";
```

```html
<lr-phone-input
  label="Mobile number"
  flags
  default-country="LU"
></lr-phone-input>
```

**Known gotchas:**

- An adapter's `parse()` method is synchronous because it runs on every keystroke. Load any optional
  module first, then assign the resolved adapter. Once an adapter is assigned, exceptions and
  malformed results fail closed to `invalid`; only the no-adapter mode uses the E.164-only fallback.
- A valid adapter result must include an E.164-shaped `e164`; the discriminated result type makes
  that requirement statically visible and runtime normalization prevents malformed success from
  entering form submission.
- Country names use `Intl.DisplayNames` and fall back to the ISO code; set `LyraPhoneCountry.label` for
  a product-specific name. Calling codes are data, not derived by the component.
- The component never imports `@aceshooting/lyra-flags` itself, with `flags` or without. `flags`
  lazily registers only the `<lr-flag>` element; the artwork resolver comes from the consumer's own
  `flag-peer.js` import (plus the installed peer package), so forgetting either shows a flagless
  trigger rather than erroring. `country-prefix` remains available for a fully consumer-owned
  adornment instead.
- The visible trigger (`country-trigger` and everything inside it) is `aria-hidden` by design; the
  layered native select is the accessible control. Don't move interactive content into those parts
  via `::part` styling tricks, and don't expect the flag inside the open popup list — a native
  `<option>` is text-only.

**Additional API surface:**

- `click()` — Activate the internal telephone input unless the form control is effectively disabled.
- `--lr-phone-input-flag-size` — Selected flag size, scaled by `size`.
- `--lr-phone-input-glyph-size` — Country selector glyph size, scaled by `size`.
- `--lr-phone-input-gap` — Country-trigger child gap. Default: `var(--lr-space-xs)`.
- `--lr-phone-input-radius` — Input-wrapper corner radius. Default: `var(--lr-radius)`.
- `--lr-phone-input-focus-border-color` — Focused row border color. Default: `var(--lr-color-brand)`.
- `--lr-phone-input-fill` — Resting input-row background, public since 16.0.0. Default:
  `var(--lr-color-surface)`.
- `--lr-phone-input-border-color` — Resting input-row border color. Default:
  `var(--lr-color-border)`. The invalid and focused states keep their own hooks and still win
  over it.
- `--lr-form-control-focus-shadow` — The shared field halo, painted as a `box-shadow` while the
  row holds focus. Default: `none`. Additive — the focus outline and the focused border are
  never replaced by it.
- `--lr-phone-input-invalid-border-color` — Invalid row border color. Default: `var(--lr-color-danger)`.
- `--lr-phone-input-country-hover-bg` — Country trigger hover background. Default: `var(--lr-color-brand-quiet)`.

---
