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

# `lr-locale-picker`

- **Import** `import '@aceshooting/lyra-ui/components/lr-locale-picker.js';` (stable tag alias; registers the tag)
- **Class** `LyraLocalePicker`, also available unregistered from `@aceshooting/lyra-ui/components/forms/locale-picker/locale-picker.class.js`
- **Family** `components/forms/` — see `llms/index.md` for its siblings
- **Status** `stable` since `6.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** 13 parts, 25 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-locale-picker`

A closed-list locale switcher over the library's own locale registry. First-party invention (no
Web Awesome equivalent). With `locales` unset (the default), the offered rows are exactly
`getRegisteredLyraLocales()` — every locale with strings registered via `registerLyraLocale()`,
plus `en` — kept live via `subscribeLyraLocaleRegistry()`. Built directly on `lr-select`'s
trigger-button/`aria-activedescendant` listbox technique, not composed from it — a plain closed
list, no filter/free-text mode.

Host `aria-describedby` references resolve onto the role=combobox trigger before its local error and
hint guidance. The relationship tracks missing IDs, target replacement, removal/reinsertion,
reconnect, and document adoption. Removing `label`, `hint`, or `error-text` safely omits that
content while leaving native `null` property readback unchanged; explicit empty strings remain empty
and later supplied text renders normally.

Public `--lr-locale-picker-*` theme inputs stay undeclared on the host, so an ancestor theme
wrapper can override size-tier fallbacks; a value set directly on the element still wins.

**Properties:**

- `locales?: LyraLocaleCatalog` (attribute: false) — `LyraLocaleCatalog = readonly string[] |
readonly LyraLocaleEntry[]`, `LyraLocaleEntry { tag: string; label?: string; country?: string }`.
  `undefined` (the default) auto-discovers the registry; every supplied array (either form),
  including an authoritative `[]`, overrides it entirely — a curated subset, custom order,
  custom labels, or a locale offered before its strings are registered. Explicit catalogs are
  capped, cloned and frozen at assignment; mutate a new array/entry and reassign it to update the
  list. `country` (ISO 3166-1 alpha-2 or alpha-3) overrides a row's derived flag — e.g.
  showing Lebanon's flag for an `'ar'` row instead of the library's default Saudi Arabia mapping;
  only available on the `{tag,label,country}` object form, not the bare `string[]` form. Replacing
  the catalog while the listbox is open keeps keyboard navigation valid: an active row beyond the
  new end is rehomed to the last remaining row. Arrow/Home/End/typeahead changes scroll the active
  owned option into nearest view after render; replacement and disconnect cancel stale scrolls.
- `showFlags: boolean = true` — each row's leading `<lr-flag language={tag} variant="compact">`
  (or `<lr-flag country={country} variant="compact">` when the entry sets `country`); `false`
  omits the flag element entirely (not just visually).
- `triggerDisplay: LyraLocaleTriggerDisplay = 'flag-label'` (attribute `trigger-display`) —
  `'flag' | 'label' | 'flag-label'`. The default keeps the flag, label and chevron. `label`
  omits only the trigger flag; the menu keeps its flags and endonyms. `flag` centers the flag in
  a square and hides the visible label and chevron. The `trigger-label` remains available to
  assistive technology and describes the trigger's current language; its accessible name still
  follows `label`/the host `aria-label`. The square uses the shared/scoped trigger height with a
  24px floor. `showFlags=false` retains visible text in every mode. Selection, keyboard navigation,
  form values and the uncommitted effective-locale preview keep their usual behavior.
- `optionDisplay: LyraLocaleOptionDisplay = 'label-tag'` (attribute `option-display`) —
  `'label' | 'label-tag'`. The default renders each option row as the locale's label above its raw
  BCP-47 tag. `'label'` renders the label alone and **omits the `option-tag` element from the DOM**
  rather than hiding it with CSS — a visually hidden tag would still join the row's accessible name
  and would still be matched by `::part(option-tag)`, so under `'label'` that part matches nothing
  at all. The trigger, the row flags, `showFlags`, selection, keyboard navigation and form values
  are identical in both modes; only the option rows change.
- `value: string = ''` — the **committed** selection (form value, drives `lr-change`). While `''`
  and untouched, the trigger _displays_ `effectiveLocale` as a preview label, but
  `checkValidity()`/`required` are governed by the real `value`, which stays `''` until a real
  commit — mirrors a native `<select>` showing its first option's text without that being a
  committed selection.
- `defaultValue: string = ''` (attribute `value`, reflected) — the current reset default. Live
  `value` writes are non-reflecting and dirty; later default/attribute changes cannot overwrite
  them until `form.reset()` restores the current default.
- `customError: string | null` (attribute `custom-error`) — reflected consumer validation message.
- `required: boolean = false`, `disabled: boolean = false`, `name: string = ''` — standard
  form-associated properties.
- `label: string = ''`, `hint: string = ''`, `errorText: string = ''` (attribute `error-text`) —
  same opt-in form-control chrome as `lr-select` (props + matching named slots + parts); unset
  renders none of it.
- `open: boolean = false` (reflected).
- `size: LyraSize = 'm'` (reflected) — the same full scale as `lr-select`, accepting
  `2xs`/`xs`/`s`/`m`/`l`/`xl` and `small`/`medium`/`large`.

**Events:** `lr-change` (`detail: { value, previousValue, direction }`, **cancelable**) — fired on
every explicit pick; if not `defaultPrevented`, the component applies the pick itself via
`setLyraLocale(value)`. A listener calling `event.preventDefault()` leaves `value` updated but the
active locale untouched, so a host can persist the choice first and apply it later. `focus`/`blur`
are relayed once from the trigger as native `FocusEvent`s preserving `relatedTarget`.
`lr-invalid` is the single
bubbling/composed, cancelable alias of a failed native validity check.

`direction` (`'ltr' | 'rtl'`, typed as `LyraLocaleDirection`) is the picked locale's writing
direction, resolved through `getLyraLocaleDirection(value)` — a catalog's declared
`registerLyraLocale(tag, strings, { dir })` first, then `Intl.Locale`'s text-info surface where the
engine has it, then `'ltr'`. It is present on every `lr-change`, cancelled or not, and it is carried
precisely so applying the direction is a one-liner instead of an application-maintained table of RTL
tags:

```js
picker.addEventListener("lr-change", (e) => {
  document.documentElement.lang = e.detail.value;
  document.documentElement.dir = e.detail.direction;
});
```

The component still never writes `lang`/`dir` itself — a picker does not own the page — but it no
longer leaves the host to work the direction out. `getLyraLocaleDirection()` is exported from
`@aceshooting/lyra-ui/localization.js` for the same lookup outside an event handler (a persisted
choice applied on boot).

**Methods:** `focus(options?)`, `blur()`, and `click()` — all forward to the internal trigger
button and synchronously no-op under direct or fieldset disablement, same convention as
`lr-select`'s identical trio. `setCustomValidity(message)` sets or clears
a consumer-supplied error ("that locale is not enabled for your account"): a non-empty message
raises `customError` and blocks submission, `''` restores the picker's own computed validity so a
required picker with nothing committed goes back to `valueMissing`. It survives every
`value`/`required` change and a form reset. `getForm()` returns the browser-resolved owning form.

The open list is a nonmodal overlay: only its topmost owner handles Escape and outside pointer
dismissal; Escape restores the trigger, while Tab keeps ordinary document navigation. Disconnect or
cross-document adoption closes it. A required, uncommitted value recomputes its intrinsic localized
message when `.strings` or the effective locale changes; an author custom-validity message retains
priority until cleared.

**Slots:** `label`, `hint`, `error`.

**CSS parts:** `form-control`, `form-control-label`, `trigger`,
`trigger-flag` (the trigger's leading `<lr-flag>` for the current value, present only while
`showFlags` is on and `triggerDisplay` is not `label`), `trigger-label` (the current language,
visually hidden in flag-only mode), `listbox`, `option`, `option-flag` (present only while `showFlags` is on),
`option-label`, `option-tag` (the row's secondary line — the raw BCP-47 tag; rendered only while
`optionDisplay` is `label-tag`, and absent from the DOM entirely under `optionDisplay="label"`),
`expand-icon`,
`hint`, `error`.

**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 under "The required-field marker"
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`. The part is rendered only when there is label text, so an
unlabelled picker paints no stray glyph.

**Themeable custom properties:** `--lr-locale-picker-trigger-padding`,
`--lr-locale-picker-trigger-min-height`, `--lr-locale-picker-trigger-height` (unset by default, a
floor-only escape hatch — set a length to both floor and cap the trigger),
`--lr-locale-picker-font-size`, `--lr-locale-picker-expand-size` (all scaled by `size`), and
`--lr-locale-picker-trigger-hover-bg`, `--lr-locale-picker-open-border-color`,
`--lr-locale-picker-option-active-bg`, `--lr-locale-picker-option-selected-border-color`,
`--lr-locale-picker-option-selected-color`, and
`--lr-locale-picker-option-selected-font-weight`. The state hooks fall back to the previous brand,
quiet-brand, and semibold semantic tokens.

The listbox is a floating surface and paints from the **shared overlay-surface family** (16.0.0):
`--lr-overlay-surface` (default `var(--lr-color-surface-overlay)`), `--lr-overlay-border` (default
`var(--lr-color-border)`) and `--lr-overlay-shadow-anchored` (default `var(--lr-shadow-m)`), none of
them declared on `:host`, so one declaration on `:root` — or on any ancestor, to scope it — retints
this listbox together with every other floating surface. `--lr-overlay-radius` reaches the listbox
only as the middle arm of this component's own `--lr-locale-picker-radius`, which still wins when
set: a component-scoped override outranks the shared family, never the other way round.

`--lr-positioning-strategy` (16.0.0) — the listbox reads this same cascading `absolute`/`fixed`
override documented on `<lr-popover>` when it is (re)positioned, falling back to its own `fixed`
default when nothing is set. There is no per-instance `positioning-strategy` property on
`<lr-locale-picker>`; set the custom property on `:root`, a theme, or one clipping ancestor to
change every unset locale picker beneath it.

**Optional peer deps:** none directly — each row's `<lr-flag>` degrades to an empty render (no
peer warning duplication; `lr-flag` itself already logs one) when the optional
`@aceshooting/lyra-flags` package isn't installed and `showFlags` is left on.

A compact header can set `trigger-display="flag"` and
`--lr-locale-picker-trigger-height: 2.25rem` for a 36px square at the usual 16px root size.
Import `@aceshooting/lyra-ui/components/media/flag/flag-peer.js` to register the optional flag
resolver. Menu labels stay visible; a per-entry `country` override also reaches the compact trigger.

```html
<lr-locale-picker label="Language"></lr-locale-picker>
<script type="module">
  import { registerLyraLocale } from "@aceshooting/lyra-ui/localization.js";
  registerLyraLocale("fr", { close: "Fermer" });
  document
    .querySelector("lr-locale-picker")
    .addEventListener("lr-change", (e) => console.log(e.detail.value));
</script>
```

**Known gotchas:**

- selecting a row applies `setLyraLocale()` itself unless the listener calls
  `event.preventDefault()` on `lr-change` — it does not touch
  `document.documentElement.lang`/`dir`. Applying those is still the host's job, but the direction
  is no longer the host's to _derive_: read `event.detail.direction` (or call
  `getLyraLocaleDirection(tag)`), rather than keeping a hand-maintained list of RTL tags.
- no filter/free-text mode — for a catalog with hundreds+ of rows, roll your own with `lr-select`
  or `lr-combobox` instead.
- arrow-key navigation is vertical-only (Home/End/ArrowUp/ArrowDown); there is no
  ArrowLeft/ArrowRight remap under RTL, since there is no horizontal axis to remap.

**Additional API surface:**

- `--lr-locale-picker-gap` — Trigger and option child gap. Default: `var(--lr-space-xs)`.
- `--lr-locale-picker-radius` — Trigger/listbox/option corner radius. Default: `var(--lr-radius)`.
- `--lr-locale-picker-trigger-hover-bg` — Trigger hover background. Default: `var(--lr-color-brand-quiet)`.
- `--lr-locale-picker-open-border-color` — Open trigger border color. Default: `var(--lr-color-brand)`.
- `--lr-locale-picker-trigger-fill` — Resting trigger background. Default: `var(--lr-color-surface)`.
- `--lr-locale-picker-trigger-border-color` — Resting trigger border color. Default:
  `var(--lr-color-border)`.
- `--lr-locale-picker-trigger-hover-border-color` — Trigger border color under the pointer. Default:
  the resting `--lr-locale-picker-trigger-border-color`, so an unset hook leaves the hovered border
  exactly where it has always been.
- `--lr-form-control-focus-shadow` — The shared field halo, painted as a `box-shadow` while the
  trigger is focused or open. Default: `none`. Additive — the focus outline and the open border are
  never replaced by it.
- `--lr-locale-picker-option-selected-border-color` — Selected option border. Default: `var(--lr-color-brand)`.
- `--lr-locale-picker-option-selected-color` — Selected option text. Default: `var(--lr-color-brand)`.
