UNPKG

@llselect/core

Version:

Low-level select: a minimal, flexible, framework-agnostic replacement for the native HTML select element.

1,553 lines 79.7 kB
import { type LLSelectWidthPolicy } from './positioning.js';
import type { LLSelectUiTranslationPack } from './i18n.js';
/**
 * What happens when the user clicks outside an open popup.
 *
 * - `'pass-through'` (default): close the popup; the outside click still
 *   triggers its normal action (button click, link navigation, etc.).
 * - `'block'`: close the popup only; the outside click is swallowed so no
 *   underlying handler or default action fires. Avoids accidental side
 *   effects when the user only intended to dismiss the dropdown.
 * @group Settings
 * @category Base
 */
export type LLSelectOutsideClickBehavior = 'pass-through' | 'block';
/**
 * Who initiated a chosen-state change, delivered to `onChange` as
 * `meta.source`.
 * - `'user'`: a pointer or keyboard interaction inside the widget - an
 *   option toggle, a tag's remove button, the clear button, the choose-all
 *   row.
 * - `'api'`: any programmatic call - `setChosenItem` / `setChosenItems`,
 *   `toggleItem`, the `choose*` bulk ops, `setItems` reconciliation.
 * @group Events
 */
export type LLSelectChangeSource = 'user' | 'api';
/**
 * Extra facts about one `onChange` firing, as the callback's third argument.
 * An object on purpose: future fields can be added without breaking the
 * callback signature.
 * @group Events
 */
export interface LLSelectChangeMeta {
    source: LLSelectChangeSource;
}
/**
 * Resolved (defaults applied) settings shared by all select variants.
 * Subclasses (`LLSelectSingle`, `LLSelectMultiple`) extend this with their
 * mode-specific options such as `onChange`.
 *
 * Settings are frozen after the constructor. What still changes at runtime:
 * - State changes by method, and never was a setting: the items
 *   (`setItems`), the chosen value (`setChosenItem` / `setChosenItems`),
 *   `disabled` (`setDisabled`).
 * - Two settings have a setter, because they are text: `uiTranslationPack`
 *   (`setUiTranslationPack`) and `placeholder` (`setPlaceholder`).
 * - Every other setting is fixed for the instance's lifetime.
 * - To change a fixed setting, build a new instance. One build takes about
 *   0.2 ms.
 * - If a setting must vary at runtime, use its function form where one
 *   exists. `filterable: (items) => boolean` is re-evaluated on every open
 *   (and consulted by closed-state typeahead - see the setting).
 * @group Settings
 * @category Base
 */
export interface LLSelectBaseSettings<T, GroupKey = string> {
    /**
     * Prefix used for every CSS class and DOM id the library generates
     * (default `'llselect'`). NOTE: the shipped themes target the default
     * prefix only - a custom prefix means bringing your own CSS. Reference the
     * resolved names via `instance.classIdMap` instead of hardcoding strings.
     * @group CSS
     */
    cssClassPrefix: string;
    /**
     * Text shown in the trigger when nothing is selected. App copy: an explicit
     * value always wins; when unset, the locale default
     * `uiTranslationPack.triggerPlaceholder` is used (`'Please select'` in English).
     * @group Trigger
     */
    placeholder: string;
    /**
     * Accessible name of the field, like the `<label>` text of a native
     * `<select>` (e.g. `'Country'`).
     * - Applied to the trigger, the popup listbox, and (while the filter is active)
     *   the filter input; per-mode wiring: `docs/llm/A11Y.md` "Accessible name".
     * - The name resolves by the FIRST set rung, mirroring the W3C
     *   accessible-name computation order:
     *   1. `ariaLabelledBy`.
     *   2. `ariaLabel` (this setting).
     *   3. `labelEl` - its element's id becomes the resolved `ariaLabelledBy`.
     *   4. None set: the field is unnamed. A combobox requires a name
     *      (WAI-ARIA 1.2), so one `console.warn` per page reports the first
     *      offender.
     * - `null` (default): this rung is skipped. An empty or whitespace-only
     *   string counts as unset too - the accname computation skips a blank
     *   `aria-label`, and so does the ladder.
     * @group Accessible name
     */
    ariaLabel: string | null;
    /**
     * Space-separated DOM id(s) of the visible label element(s) naming the
     * field; forwarded as `aria-labelledby` to the same elements as `ariaLabel`.
     * - Prefer this over `ariaLabel` when a visible label element exists: the
     *   spoken name then always matches the visible text.
     * - `null` (default): not forwarded; see `ariaLabel` for the naming
     *   requirement. An empty or whitespace-only string counts as unset too,
     *   same as `ariaLabel`.
     * - Rung 1 of the resolution order (the numbered list at `ariaLabel`): it
     *   wins whenever set, matching the ARIA name computation.
     * @group Accessible name
     */
    ariaLabelledBy: string | null;
    /**
     * The widget's visible label element - the one foreign element the library
     * touches. Emulates native `<label for>` (which cannot target these divs)
     * in both directions:
     * - clicking it focuses the trigger (focus ONLY; native `<select>` does not
     *   open on label click, neither does this);
     * - it feeds the accessible name as rung 3 of the resolution order (the
     *   numbered list at `ariaLabel`): with neither aria setting given, the
     *   label's id becomes the resolved `ariaLabelledBy` (an id is minted from
     *   `classIdMap.labelId` if the element has none) - a live reference, so
     *   later label text changes stay correct.
     * - `null` = no label element; the name ladder just skips this rung.
     * - `destroy()` removes the click listener and a minted id.
     * @group Accessible name
     */
    labelEl: HTMLElement | null;
    /**
     * Equality predicate for item values - return `true` when `a` and `b` are the
     * same item.
     * - Required for non-primitive `T`. The default compares by identity:
     *   `===`, except that `NaN` equals `NaN` (SameValueZero, the same rule
     *   `Set` uses), so every code path agrees on what "the same item" means.
     * - Used for selection, dedup, and matching the chosen item back to the list.
     * - Symmetric: do not depend on which argument is the candidate vs the
     *   existing item.
     * @group Items
     */
    compareFn: (a: T, b: T) => boolean;
    /**
     * See {@link LLSelectOutsideClickBehavior}.
     * @group Popup
     */
    outsideClickBehavior: LLSelectOutsideClickBehavior;
    /**
     * The trigger arrow slot's content ELEMENT (typically a dropdown chevron or
     * triangle). Called whenever the arrow may need to change - including on
     * every open/close - so the returned element can vary with `isOpened`.
     * - fn returns `null` - no arrow for that state.
     * - setting is `null` (default) - the library adds nothing to the arrow slot.
     * @group Trigger
     */
    createTriggerArrowContentElFn: ((state: {
        isOpened: boolean;
    }) => HTMLElement | SVGElement | null) | null;
    /**
     * Whether the trigger shows a clear (x) button that empties the selection.
     * - Default `false`.
     * - Clearing sets the empty value: `undefined` for a single select, `[]`
     *   for a multiple. It goes through the normal setters, so `onChange`
     *   fires with that empty value. There is no separate clear event.
     * - The empty value is not configurable, and no library makes it so:
     *   "nothing chosen" already exists before the first choice, so the value
     *   types carry `undefined` either way.
     * - If your model is a plain type like `string` and must never hold
     *   `undefined`, pick one of these:
     *   - Add a real "none" item (for example `''` shown as "(none)") and skip
     *     `clearable`. The model then stays `string` after the first choice,
     *     exactly like a native `<select>` with a placeholder option.
     *   - Keep `clearable` and coerce in `onChange`: `item ?? ''`.
     * - "Clear" means back to empty and the placeholder, never "back to some
     *   default option". If you want a default instead, set it yourself in
     *   `onChange`.
     * - The button sits in its OWN trigger slot (like the arrow, so it never
     *   collides with `createTriggerContentElFn`), is `tabindex="-1"`, and
     *   carries an `aria-label`. The theme hides it via `data-empty` while
     *   nothing is selected.
     * @group Trigger
     */
    clearable: boolean;
    /**
     * Content ELEMENT of the clear button (its x icon), mirroring `createTriggerArrowContentElFn`.
     * `null` (default) = the theme's CSS glyph. The library always owns the button,
     * its click (clears + stops propagation) and aria; this only fills the icon.
     * @group Trigger
     */
    createTriggerClearButtonContentElFn: (() => HTMLElement | SVGElement | null) | null;
    /**
     * Whether the popup includes a filter input.
     * - `false` (default): never.
     * - `true`: always.
     * - Predicate `(items) => boolean`: conditional - evaluated against the
     *   CURRENT full item list each time the popup OPENS (never mid-open; a
     *   `setItems` crossing the threshold applies on the next open). E.g.
     *   `filterable: (items) => items.length > 10`. A printable key pressed
     *   while CLOSED also calls the predicate, read-only, to decide whether
     *   prefix typeahead may take the key - keep it cheap and side-effect free.
     * The ARIA mode follows the evaluated value per open cycle: active =
     * trigger `role="button"`, focus moves to the input; inactive = exactly
     * like `filterable: false` (trigger stays `role="combobox"`, focus stays on
     * the trigger). See `docs/llm/A11Y.md` and `docs/llm/DESIGN.md`.
     * @group Filtering
     */
    filterable: boolean | ((items: readonly T[]) => boolean);
    /**
     * Chrome strings (AT labels + generated text): the i18n customization point. Resolved
     * against English: pass a language pack whole (`uiTranslationPack: zhTW` from
     * `@llselect/core/i18n`) or override single keys
     * (`uiTranslationPack: { ...zhTW, filterInputPlaceholder: '...' }`).
     * Key-by-key contract (incl. what `null` means where allowed):
     * {@link LLSelectUiTranslationPack}.
     * @group i18n
     */
    uiTranslationPack: LLSelectUiTranslationPack;
    /**
     * Predicate used by the filter input; return `true` to keep the item.
     * `null` (default) means the built-in case-insensitive substring match
     * against the item's resolved text (`itemToStringFn` / `itemToString`).
     * Pass a custom function for fuzzy / domain-specific matching.
     * - `query` is the RAW input value: not trimmed and not lower-cased. Normalize
     *   it yourself (the built-in lower-cases both sides; it does not trim).
     * - Not called while the query is empty (an empty box shows every item), but a
     *   whitespace-only query (e.g. `"  "`) does call it.
     * @group Filtering
     */
    filterFn: ((item: T, query: string) => boolean) | null;
    /**
     * The no-results message's visible content ELEMENT, without subclassing.
     * Mirrors `createItemContentElFn`: the library owns the message container
     * (`role="status"`, class, show/hide), this fills its content only.
     * - `query` is the current filter query (`''` when the filter is inactive or
     *   the list is simply empty), so "Nothing matches <query>" is possible.
     * - Return an `HTMLElement`: inserted as the content (you own it; include
     *   real text - the status region announces its TEXT content).
     * - `null` (setting default, or returned): plain text from
     *   `uiTranslationPack.popupListNoResults`.
     * Re-evaluated every time the message is shown (the query may differ). The DOM
     * is refreshed only when the resolved TEXT changes - the status region is keyed
     * on text so it announces once, not per keystroke - so rich content whose visible
     * markup varies while its text stays constant is not re-rendered.
     * @group Filtering
     */
    createPopupListNoResultsContentElFn: ((query: string) => HTMLElement | null) | null;
    /**
     * How the popup decides its width. Does NOT affect the trigger - trigger
     * width is always whatever your CSS says.
     *
     * - `'fit-content'` (default): popup width grows to its own content (items,
     *   filter input, ...) and never shrinks below the trigger's width - the
     *   native `<select>` dropdown behavior, minus its viewport overflow:
     *   auto-shifts and width-clamps when the natural width would not fit.
     *   Direction-aware: in an RTL context
     *   (`getComputedStyle(trigger).direction === 'rtl'`, read once per open)
     *   it right-aligns to the trigger and grows LEFTWARD, the mirror of LTR.
     * - `'match-trigger'`: popup width equals trigger width; long item text wraps
     *   inside the popup.
     * @group Popup
     */
    popupWidthPolicy: LLSelectWidthPolicy;
    /**
     * Predicate deciding whether an individual item is disabled. `null` (default)
     * = nothing disabled. A disabled item is not selectable (click / Enter) and is
     * skipped by keyboard nav; it keeps `role="option"` plus `aria-disabled`.
     * Re-evaluated on every render (never cached). For a generic `T` this is the
     * only way to mark items - the library cannot read a `disabled` field off an
     * unknown type. See `docs/llm/DESIGN.md`.
     * @group Disabling
     */
    itemDisabledFn: ((item: T) => boolean) | null;
    /**
     * When the control is disabled via `setDisabled(true)`, whether the trigger
     * stays in the tab order (`tabindex="0"`). `false` (default) takes it out
     * (`-1`). Set `true` so keyboard / AT users can focus the disabled control to
     * read a "why disabled" tooltip.
     * @group Disabling
     */
    focusableWhenDisabled: boolean;
    /**
     * Item -> display string, without subclassing.
     * - `null` (default) = `String(item)`.
     * - Read by the `itemToString` method's default; used for list text, the
     *   single trigger text, the option's accessible name, and the default
     *   filter. Inserted as `textContent` (plain text, NOT parsed as HTML).
     * - For rich content (icons etc.), pass `createItemContentElFn`.
     * @group Items
     */
    itemToStringFn: ((item: T) => string) | null;
    /**
     * Item -> the visible content ELEMENT of its list row, without subclassing.
     * - Return an `HTMLElement` and the library inserts it as-is (you own the
     *   node); it becomes the row's visible content.
     * - `null` = plain `textContent` from `itemToString`. This is the default,
     *   both when the setting is unset and when your function returns `null` for
     *   a particular item.
     * - Fills the VISIBLE content only. You never touch `aria-*`: when this
     *   returns an element the library sets the option's `aria-label` from
     *   `itemToString`, so the accessible name + match text stay owned by
     *   `itemToString` no matter what you render (icon-only, reordered, ...).
     *   To make the spoken/matched text differ from the visible content, set the
     *   two independently: `itemToStringFn` for the name/matching,
     *   `createItemContentElFn` for the look.
     * - For full control of the option element (tag / wiring), subclass
     *   `createItemEl` instead.
     * - Runs per rendered row per render, and re-runs whenever a row is
     *   rebuilt: open, filter, `setItems`, AND chosen-state changes (both modes
     *   replace the affected rows in place while the popup is open). Content
     *   that reads selection state (e.g. a checkmark on the chosen row via
     *   `createCheckmarkSvgEl`) therefore stays fresh; keep the function cheap.
     *
     * @example
     *   // List shows an icon + the item text; screen readers announce just that text.
     *   itemToStringFn: (lang) => lang.name,
     *   createItemContentElFn: (lang) => {
     *     const row = document.createElement('span')
     *     const icon = document.createElement('i')
     *     icon.className = `mdi mdi-${lang.icon}`
     *     icon.setAttribute('aria-hidden', 'true') // decorative
     *     row.append(icon, lang.name)
     *     return row
     *   }
     * @group Items
     */
    createItemContentElFn: ((item: T) => HTMLElement | null) | null;
    /**
     * Item -> its group's key (identity), enabling optgroup rendering.
     * - `null` (setting, default): grouping off - flat list, no headers.
     * - fn returns `null`: this item is in no group; renders ungrouped.
     * - Contiguous items with an equal key (per `groupKeyCompareFn`) form one
     *   group. By default non-contiguous data is first gathered into display
     *   order (`gatherGroups`); with `gatherGroups: false` the data must be
     *   pre-sorted by group. See `docs/llm/DESIGN.md`.
     * @group Grouping
     */
    itemToGroupKeyFn: ((item: T) => GroupKey | null) | null;
    /**
     * Whether the library gathers non-contiguous groups before rendering
     * (`true`, default). Grouping renders contiguous runs, so scattered items
     * sharing a key would otherwise produce a duplicate header per gap.
     * - `true`: the DISPLAY order is derived via {@link gatherItemsByGroupKey}:
     *   groups in first-appearance order, within-group order kept, ungrouped
     *   (`null`-key) items in place. The data itself (`items` / `getItems()`)
     *   is never reordered, and already-contiguous data is detected in one
     *   scan and used as-is.
     * - `false`: strict mode - you guarantee the data is pre-sorted by group; a
     *   key reappearing after a gap renders a duplicate header and
     *   `console.warn`s, so a broken sort is surfaced instead of silently
     *   fixed.
     * No effect while grouping is off (every key `null`).
     * @group Grouping
     */
    gatherGroups: boolean;
    /**
     * Equality for two group keys; decides whether items share a group (both
     * the `gatherGroups` gather and the contiguous-run rendering use it).
     * - `null` (default) = identity, the same rule as the default `compareFn`
     *   (`===`, plus `NaN` equals `NaN`); right for string / number keys.
     * - Supply only when `GroupKey` is an object without usable reference identity.
     * - Mirrors `compareFn`, one level up.
     * @group Grouping
     */
    groupKeyCompareFn: ((a: GroupKey, b: GroupKey) => boolean) | null;
    /**
     * Group key -> the header's display text. The i18n customization point: keep keys stable,
     * translate here.
     * - `null` (default) = `String(groupKey)`.
     * @group Grouping
     */
    groupKeyToStringFn: ((groupKey: GroupKey) => string) | null;
    /**
     * Predicate: is this whole group disabled?
     * - `null` (default) = no group disabled.
     * - `true` = every item in the group is treated as disabled (layers on top of
     *   `itemDisabledFn`).
     * @group Grouping
     */
    groupDisabledFn: ((groupKey: GroupKey) => boolean) | null;
    /**
     * Group header -> its visible content ELEMENT (icon / count badge / rich
     * markup), without subclassing. Mirrors `createItemContentElFn`.
     * - Return an `HTMLElement` and the library inserts it as the header's visible
     *   content; the group's accessible name stays `groupKeyToString` (on the
     *   container `aria-label`) and the label element stays `aria-hidden`.
     * - `null` (default, or returned for a group) = plain text from
     *   `groupKeyToString`.
     * - `itemsInGroup` is the group's items, so you can render "Fruits (4)" or a
     *   summary without recomputing the grouping.
     * @group Grouping
     */
    createGroupLabelContentElFn: ((groupKey: GroupKey, itemsInGroup: readonly T[]) => HTMLElement | null) | null;
    /**
     * Fired right after the popup opens. An `open()` call that does not actually
     * open the popup (already open, a disabled control, or a trigger scrolled out
     * of view or clipped) does not fire it. Fires in ADDITION to the protected
     * `onOpened` hook - the setting is for consumers, the hook for subclasses;
     * both run. `null` (default) = nothing.
     * @group Events
     */
    onOpen: (() => void) | null;
    /**
     * Fired right after the popup closes. A no-op `close()` does not fire it.
     * Additive with the protected `onClosed` hook, like {@link onOpen}.
     * @group Events
     */
    onClose: (() => void) | null;
}
/**
 * Constructor input for a resolved settings bag `S`: every field optional,
 * and `uiTranslationPack` accepts a PARTIAL pack (missing keys fall back to
 * English). Shared by the base / single / multiple `*SettingsInput` types;
 * use it for a subclass wrapper that extends the settings bag.
 * @group Settings
 * @category Base
 */
export type LLSelectSettingsInputOf<S extends {
    uiTranslationPack: LLSelectUiTranslationPack;
}> = Partial<Omit<S, 'uiTranslationPack'>> & {
    uiTranslationPack?: Partial<LLSelectUiTranslationPack>;
};
/**
 * Constructor-time settings input - every field is optional and missing
 * fields fall back to the library defaults.
 * @group Settings
 * @category Base
 */
export type LLSelectBaseSettingsInput<T, GroupKey = string> = LLSelectSettingsInputOf<LLSelectBaseSettings<T, GroupKey>>;
/**
 * Resolved CSS class names and DOM ids for one instance. Exposed on
 * `instance.classIdMap` so callers can reuse them in their own CSS or query
 * selectors instead of hard-coding the strings.
 * @group CSS & DOM
 */
export interface LLSelectClassIdMap {
    /** Class on `rootEl` (the caller-passed mount element). */
    rootClass: string;
    /**
     * Class on `triggerEl` (the interactive trigger). Its `role` is
     * `combobox` while the filter is inactive and `button` while a filterable popup
     * is open; see `docs/llm/A11Y.md`.
     */
    triggerClass: string;
    /** Class on the inner span where content (text/tags) is rendered. */
    triggerContentClass: string;
    /** Class on the inner span where the optional dropdown arrow lives. */
    triggerArrowClass: string;
    /** Class on the clear (x) button slot in the trigger (`clearable`). */
    triggerClearButtonClass: string;
    /** Class on `popupEl` (the outer popup wrapper, no ARIA role). */
    popupClass: string;
    /** Class on `popupListEl` (the inner element with `role="listbox"`). */
    popupListClass: string;
    /**
     * Class on the no-results message element (`role="status"`), shown below
     * the (empty) listbox when the visible item list has zero entries.
     */
    popupListNoResultsClass: string;
    /**
     * Class on the choose-all leading row (`LLSelectMultiple`, `chooseAllRow`
     * setting). Also carries `itemClass` plus `data-chosen-state="none|some|all"`
     * for the tri-state visual.
     */
    chooseAllRowClass: string;
    /** Class on every item element (`role="option"`) inside the popup list. */
    itemClass: string;
    /**
     * Extra class added to the currently keyboard-focused item element.
     * Use this to style the focused item.
     */
    itemFocusedClass: string;
    /**
     * Class added to a disabled item element (which also carries
     * `aria-disabled="true"`). A stable hook for styling / tooltip targeting.
     */
    itemDisabledClass: string;
    /**
     * Class on a group container (`role="group"`). A disabled group's container
     * also carries `aria-disabled="true"` + `data-disabled="true"`.
     */
    groupClass: string;
    /**
     * Class on the visible group label element (`aria-hidden`), inside the group
     * container above its items. A hook for styling / sticky headers.
     */
    groupLabelClass: string;
    /** Class on the tag-list container in `triggerDisplay: 'tags'` mode (multi). */
    tagsClass: string;
    /** Class on one tag chip (`triggerDisplay: 'tags'`). */
    tagClass: string;
    /** Class on a tag's remove (x) button; `aria-label` names the item, `tabindex="-1"`. */
    tagRemoveButtonClass: string;
    /**
     * Class on a tag chip whose item is effectively disabled (which also carries
     * `aria-disabled="true"`, and whose x button no longer removes it). Mirrors
     * `itemDisabledClass`; a stable hook for greying the inert chip.
     */
    tagDisabledClass: string;
    /**
     * Class added to `rootEl` while the popup is open. Use it as a CSS hook
     * for open-state styling (also available as `[data-state='open']` on the
     * trigger).
     */
    openClass: string;
    /** DOM `id` of `triggerEl`. Unique across instances. */
    triggerId: string;
    /**
     * DOM `id` minted onto the `labelEl` setting's element when it has none
     * (the resolved `ariaLabelledBy` then references it). Unique across
     * instances. Unused when `labelEl` is null or already carries an id.
     */
    labelId: string;
    /** DOM `id` of the trigger content span. Unique across instances. */
    triggerContentId: string;
    /**
     * DOM `id` of the hidden plain-text value span (root-level sibling of the
     * trigger). Unique across instances. Referenced by the filterable-mode
     * trigger's `aria-labelledby` chain so the closed button's accessible name
     * includes the current value as PLAIN TEXT - rich trigger content (tag
     * chips with labelled remove buttons) must not leak control names into the
     * field name (see the `ariaLabel` / `ariaLabelledBy` settings).
     */
    triggerValueId: string;
    /**
     * DOM `id` of `popupListEl` (the inner listbox). Unique across instances.
     * Referenced by the trigger's `aria-controls` attribute.
     */
    popupListId: string;
    /** Class on the filter input element inside the popup. */
    filterInputClass: string;
    /** DOM `id` of the filter input. Unique across instances. */
    filterInputId: string;
}
/**
 * Package-internal (not re-exported): subclasses detect it to pick fast paths.
 * SameValueZero (`===` plus `NaN` equals `NaN`), matching the `Set` those fast
 * paths use - `===` alone let a `NaN` item be chosen twice (QUALITY-92).
 * Second job: the reference-swap check in both `onItemsChanged` ("same value?",
 * a different question from item identity). Keep it SameValueZero for that too.
 */
export declare function defaultCompareFn<T>(a: T, b: T): boolean;
/** Package-internal test hook (not re-exported): reset the once-per-page unnamed-name warning. */
export declare function resetUnnamedNameWarning(): void;
/** Package-internal test hook (not re-exported): reset the once-per-page duplicate-items warning. */
export declare function resetDuplicateItemsWarning(): void;
/**
 * Abstract base for all select variants. Owns DOM scaffolding, ARIA wiring,
 * positioning, keyboard navigation, lazy popup-list rendering, and
 * outside-click handling. Subclasses (`LLSelectSingle`, `LLSelectMultiple`)
 * own chosen-state, decide what happens on item click, and customise the
 * trigger text via `renderTriggerContent`.
 *
 * @typeParam T - item value type. Use `unknown` (default) only when you
 *   intend to narrow inside templates / handlers; usually pass a concrete
 *   type like `string` or your domain object.
 * @typeParam GroupKey - the group key type `itemToGroupKeyFn` returns
 *   (`null` from that fn means "this item is in no group"). Defaults to
 *   `string`. Open it to objects only together with `groupKeyCompareFn`; see
 *   DESIGN.md "Data model".
 * @typeParam S - the resolved settings type, for subclasses that EXTEND the
 *   settings bag. Plain use never passes it. A subclass declares
 *   `extends LLSelectBase<T, GroupKey, MySettings>` and `this.settings` is
 *   typed `MySettings`; the constructor's `subclassSettings` param then only
 *   accepts exactly the extra fields.
 * @group Select classes
 */
export declare abstract class LLSelectBase<T = unknown, GroupKey = string, S extends LLSelectBaseSettings<T, GroupKey> = LLSelectBaseSettings<T, GroupKey>> {
    /**
     * The caller-passed mount element, now decorated as the select's root.
     * Library does not replace this node, so the caller's original reference,
     * id, and data-* attributes stay valid.
     * @group DOM elements
     */
    readonly rootEl: HTMLElement;
    /**
     * The interactive trigger element. Receives focus, click, and keydown
     * events; carries `aria-expanded`, `aria-controls`, and
     * `data-state="open|closed"`. Its `role` depends on the filter mode: `combobox`
     * while the filter is inactive (it then also hosts `aria-activedescendant`) and
     * `button` while a filterable popup is open (the filter input hosts
     * `aria-activedescendant`). See `docs/llm/A11Y.md`.
     * @group DOM elements
     */
    readonly triggerEl: HTMLElement;
    /**
     * Inner span inside the trigger where text/tags are written.
     * Subclasses' `renderTriggerContent` writes here so the sibling arrow slot
     * is preserved across re-renders.
     * @group DOM elements
     */
    readonly triggerContentEl: HTMLElement;
    /**
     * Hidden root-level span (sibling of the trigger) mirroring the current
     * value as plain text. Kept in sync by `commitTriggerContentToDom`;
     * referenced by the filterable-mode `aria-labelledby` chain (see
     * `classIdMap.triggerValueId`). Outside the trigger so
     * `triggerEl.textContent` stays exactly the visible content.
     * @group DOM elements
     */
    protected readonly triggerValueEl: HTMLElement;
    /**
     * The outer popup wrapper. Has no ARIA role itself - it just hosts the
     * popup chrome (future: filter input, toggle-all control) and the inner
     * `popupListEl`. Hidden via the `hidden` attribute when closed; positioned
     * via inline styles by the positioner when open.
     * @group DOM elements
     */
    readonly popupEl: HTMLElement;
    /**
     * The inner element with `role="listbox"`, holding the item children.
     * Sits inside `popupEl` so siblings (filter input, toggle-all) can live
     * above it without violating ARIA's "listbox children must be options"
     * rule. The trigger's `aria-controls` points to this element.
     * @group DOM elements
     */
    readonly popupListEl: HTMLElement;
    /**
     * Resolved class names and ids for this instance.
     * @group DOM elements
     */
    readonly classIdMap: LLSelectClassIdMap;
    /**
     * Resolved settings (defaults applied) - ONE bag for the whole hierarchy.
     * Typed by the class's `S` param: a subclass that extends the settings
     * passes its resolved extra fields through the constructor's
     * `subclassSettings` param, and this field is `S` with no re-typing
     * (see `LLSelectSingle` / `LLSelectMultiple`).
     * @group State (protected)
     */
    protected readonly settings: S;
    /** Raw explicit `placeholder` (constructor or `setPlaceholder`); `setUiTranslationPack` re-resolves against it. */
    private explicitPlaceholder;
    /** True when the constructor minted `classIdMap.labelId` onto `labelEl`; `destroy()` then removes it. */
    private labelElIdMinted;
    /** Click handler bound to `labelEl`: focus the trigger, never open (native label parity). */
    private readonly handleLabelElClick;
    /**
     * Current item list. Defensive copy of what `setItems` was given.
     * @group State (protected)
     */
    protected items: T[];
    /** Whether the popup is currently open; public reader {@link isOpened}. */
    private opened;
    /**
     * Index (into `items`) of the currently keyboard-focused item, or `-1`
     * when nothing is focused (closed popup, or no items).
     * @group State (protected)
     */
    protected focusedIndex: number;
    /** Control-level disabled state (whole select); toggled via `setDisabled`. */
    private disabled;
    /**
     * Who the change being applied right now is attributed to; the variants'
     * `onChange` firing reads it. Set to `'user'` by `withUserChangeSource`
     * and consumed (reset to `'api'`) by the first `onChange` fired, so a
     * nested api-driven change inside an `onChange` handler reports `'api'`.
     * @group State (protected)
     */
    protected changeSource: LLSelectChangeSource;
    private triggerArrowEl;
    /** The clear button once a trigger render has built it (`clearable` only), else `null`; every later trigger render swaps it through `createTriggerClearButtonEl` (or keeps it, if that override returns the same element). */
    private triggerClearButtonEl;
    private positioner;
    /**
     * Whether the Popover API exists (feature-detected once per instance).
     * When true the popup renders in the top layer while open - above every
     * stacking context, immune to containing-block-creating ancestors - while
     * staying in place in the DOM. See `docs/llm/DESIGN.md` "In-place popup".
     */
    private readonly popoverSupported;
    private itemEls;
    private focusedEl;
    /**
     * The leading row element (`LLSelectMultiple`'s choose-all) for the current
     * render, or `undefined` when absent. Never part of `itemEls`.
     */
    private leadingRowEl;
    /** Whether keyboard focus sits on the leading row (mutually exclusive with an item focus). */
    private leadingRowFocused;
    private outsideHandler;
    private focusOutHandler;
    /**
     * Block-mode only. Suppresses the browser's default focus shift on outside
     * mousedown so the focusout-close path cannot race ahead of the click
     * capture and detach it before the click fires.
     */
    private blockMouseDownHandler;
    /**
     * Element that owns `aria-activedescendant` and receives keydown for option
     * navigation. Equals the filter input while the filter is active, else the
     * trigger. Re-pointed by `syncFilterModeToDom` (constructor + every open).
     */
    private comboboxEl;
    /**
     * Filter input element. Always built into the popup DOM and always wired
     * (a `hidden` input receives no events); kept `hidden` while the filter is
     * inactive.
     */
    private filterInputEl;
    /**
     * No-results message element (`role="status"`). Always built (like the
     * filter input), sits AFTER the listbox inside `popupEl` so the listbox
     * keeps its options-only children contract; `hidden` while the visible
     * list has entries.
     */
    private popupListNoResultsEl;
    /**
     * Text last written into the no-results region, or `null` while it is hidden.
     * Guards a re-announcement: `role="status"` speaks on every content change, so
     * a keystroke that keeps the list empty must not rewrite identical text (A11Y.md:
     * announced once per appearance). Reset to `null` when the region hides, so the
     * next appearance announces again.
     */
    private lastNoResultsText;
    /**
     * Whether the filter input is active for the CURRENT open cycle. Evaluated
     * from the `filterable` setting (predicate form reads the current items) in
     * the constructor and on every `open()` - never re-evaluated mid-open, so
     * the focus host cannot be yanked while the popup is up.
     */
    private filterActive;
    private query;
    private filteredItems;
    private typeaheadBuffer;
    private typeaheadLastTime;
    /** Memoized display order of `items` (`gatherGroups` gather); `undefined` = recompute on next need. */
    private gatheredItems;
    private composing;
    /**
     * @param targetEl - mount element. Becomes `rootEl`; its existing children
     *   are wiped and replaced with the trigger + popup structure. Pre-set
     *   classes / id / data-* attributes on this element are preserved.
     * @param settings - optional partial settings. Missing fields use defaults
     *   ({@link LLSelectBaseSettings}).
     * @param subclassSettings - for subclasses that EXTEND the settings bag: their
     *   own fields, already resolved (defaults applied). Typed by the class's
     *   `S` param, so it accepts exactly the extra fields and nothing else.
     *   Merged into `this.settings` right here, so the bag is complete before
     *   any base construction code (e.g. `createFilterInputEl` reading the
     *   pack) can read it.
     * @group Lifecycle
     */
    constructor(targetEl: HTMLElement, settings?: LLSelectSettingsInputOf<S>, subclassSettings?: Omit<S, keyof LLSelectBaseSettings<T, GroupKey>>);
    /**
     * Evaluate the `filterable` setting against the current items: booleans
     * pass through, the predicate form is called with the full item list.
     */
    private computeFilterActive;
    /**
     * Mirror `filterActive` onto the DOM + wiring it decides: the trigger's
     * role (`button` while active, `combobox` while not), the filter input's
     * `hidden` flag, and which element `comboboxEl` points at (the
     * `aria-activedescendant` / focus host). Called from the constructor and
     * from `open()` after re-evaluation.
     */
    private syncFilterModeToDom;
    /**
     * Reflect the field's accessible name (`ariaLabel` / `ariaLabelledBy`) onto
     * the elements that carry it. Runs with `syncFilterModeToDom` (constructor +
     * every open) because the trigger's wiring depends on the mode:
     * - Trigger, filter inactive (`role="combobox"`): the name directly; the
     *   combobox VALUE already comes from the trigger content.
     * - Trigger, filter active (`role="button"`): a button's name would
     *   otherwise be its content (the current value) with no field name, so
     *   `aria-labelledby` chains label + content span. With only `ariaLabel`
     *   there is no label element to reference, so the chain starts at the
     *   trigger itself - the accname algorithm substitutes its `aria-label`.
     * - Filter input: the field name replaces the `uiTranslationPack.filterInputAriaLabel`
     *   fallback (while the filter is active the input IS the field's combobox).
     * - Listbox: the field name, both modes.
     */
    private syncFieldNameToDom;
    /**
     * Open the popup. Builds item elements lazily, attaches the positioner
     * (which auto-closes if the trigger is scrolled out of view), wires the
     * outside-click handler, and moves keyboard focus into the item list.
     * No-op if already open.
     * @group Open & close
     */
    open(): void;
    /**
     * Close the popup. Detaches positioner and outside-click listener, clears
     * the item DOM, and resets focused-item state. No-op if already closed.
     *
     * Focus return is decided automatically: when `filterable: true` and DOM
     * focus is still on the filter input at the moment of close (Esc on empty
     * filter, single-select pick, click on non-focusable area outside), focus
     * is returned to the trigger. Tab-away and outside clicks on focusable
     * elements have already moved focus elsewhere, so we leave it alone.
     * @group Open & close
     */
    close(): void;
    /**
     * Rebuild the trigger and (while open) the popup list from current state.
     * - Use it after mutating item OBJECTS in place (e.g.
     *   `users[0].name = 'X'`). The library cannot detect that on its own.
     * - It re-derives the display order (the `gatherGroups` gather).
     * - While the filter is active, it re-runs the filter against the current
     *   item text.
     * - The refresh is purely visual: it does NOT fire `onChange` and does NOT
     *   run `onItemsChanged`.
     * - Orchestrator: composes `renderTrigger` + `renderPopupList`; touches no
     *   DOM directly.
     * @group Lifecycle
     */
    rerender(): void;
    /**
     * Tear down the instance: close the popup (which detaches every document /
     * window listener and the positioner), unwire `labelEl` (click listener
     * removed, a minted id removed), remove the library's class and
     * inline styles from the caller's mount element, and empty it. Idempotent.
     * The instance must not be used afterwards.
     * - REQUIRED before discarding an instance that might be OPEN (framework
     *   wrappers: call this on unmount) - skipping it there leaks the
     *   outside-click / focusout / scroll / resize listeners.
     * - Discarding a CLOSED instance without `destroy()` leaks nothing; it only
     *   leaves the root class and `overflow-anchor` style on the mount.
     * @group Lifecycle
     */
    destroy(): void;
    /**
     * Open if closed, close if open.
     * @group Open & close
     */
    toggle(): void;
    /**
     * Whether the popup is currently open.
     * - Pairs with `isDisabled()` (state read via method).
     * - The same state is mirrored on the DOM as CSS hooks:
     *   `data-state="open|closed"` on the trigger, `classIdMap.openClass` on
     *   the root.
     * @group Open & close
     */
    isOpened(): boolean;
    /**
     * Return the current item list, in data order (as passed to `setItems`).
     * - The rendered list may differ in order and content: see `getVisibleItems`.
     * - Returns the LIVE internal array, typed read-only. Do not mutate it
     *   (TS blocks it; plain-JS callers must treat it as frozen).
     * - Structural mutation would silently bypass chosen-state reconciliation,
     *   re-filtering, and re-render. Replace the list via `setItems` instead.
     * - Mutating item OBJECTS + `rerender()` is the supported in-place path.
     * @group Items
     */
    getItems(): readonly T[];
    /**
     * Return the resolved UI strings: the built-in English defaults merged
     * with the `uiTranslationPack` setting.
     * - Reuse these in your own UI instead of keeping a second translation
     *   source. For example, a tag remove button tooltip:
     *   `sel.getUiTranslationPack().tagRemoveButtonAriaLabel(label)`.
     * - Returns the LIVE object. Treat it as immutable, like `getItems`.
     * @group i18n
     */
    getUiTranslationPack(): Readonly<LLSelectUiTranslationPack>;
    /**
     * Replace the UI-translation pack at runtime, so switching language needs
     * no re-`new`.
     * - One of the two settings with a runtime setter (the other is
     *   `setPlaceholder`); both are copy. The rule: {@link LLSelectBaseSettings}.
     * - The pack is resolved exactly like the constructor's: merged over the
     *   built-in English pack, NOT over the previously set pack.
     * - An explicit constructor `placeholder` keeps winning over the new pack's
     *   `triggerPlaceholder`.
     * - Re-renders the trigger and the open popup.
     * - Also re-applies the pack-owned attributes `rerender()` cannot reach:
     *   the filter input placeholder and its fallback `aria-label`.
     * - The clear button needs no such step: `rerender()` rebuilds it, and the
     *   rebuild reads the new pack - unless a `createTriggerClearButtonEl`
     *   override returns the previous element, which then owns the label.
     * @group i18n
     */
    setUiTranslationPack(uiTranslationPack: Partial<LLSelectUiTranslationPack>): void;
    /**
     * Replace the trigger placeholder text at runtime. It is one of the two
     * settings with a runtime setter (the other is `setUiTranslationPack`);
     * both are copy. The rule: {@link LLSelectBaseSettings}.
     * - `null` = fall back to the pack default (`uiTranslationPack.triggerPlaceholder`),
     *   mirroring an unset constructor `placeholder`. An explicit value keeps
     *   winning over later `setUiTranslationPack` calls, exactly like the
     *   constructor input.
     * - Takes effect immediately. Visible only while nothing is chosen - the
     *   placeholder never renders otherwise (the trigger is still re-rendered,
     *   which also refreshes the hidden accessible-value mirror).
     * @group Trigger
     */
    setPlaceholder(placeholder: string | null): void;
    /**
     * Replace the item list.
     * - The input is shallow-copied; later external mutation does not affect
     *   the select.
     * - Items MUST be unique under `compareFn` (it defines item identity, and the
     *   selection is a set). Duplicates render stale selection DOM; the default
     *   compareFn warns once per page, a custom compareFn is the caller's
     *   responsibility (not scanned, to keep large lists cheap).
     * - If the popup is open, it re-renders now. While closed, the DOM is
     *   built lazily on the next `open()`.
     * - Both shipped variants re-render the trigger content from `onItemsChanged`
     *   (the multiple count total, custom content that reads `items`).
     * - `LLSelectMultiple`'s `triggerDisplay: 'tags'` mode is opt-in; `'count'` is
     *   the default.
     * - When `triggerDisplay` is `'tags'`, that content render is one chip per
     *   chosen item, unless `createTriggerContentElFn` replaces the content.
     * - Subclasses may reconcile chosen-state via {@link onItemsChanged}
     *   (e.g. single mode drops a chosen value that is no longer in the list).
     * @group Items
     */
    setItems(items: readonly T[]): void;
    /**
     * Warn (once per page, never throw) when the item list has duplicates under
     * the DEFAULT compareFn - an O(n) Set check. A custom compareFn is documented
     * only: an O(n^2) scan would tax large lists (see PERFORMANCE-31), and keeping
     * its identity unique is the caller's responsibility.
     * - The Set is SameValueZero, and so is the default compareFn, so a repeated
     *   `NaN` item counts as a duplicate on both sides.
     */
    private warnOnDuplicateItems;
    /**
     * Enable or disable the whole control. Disabled: the trigger gets
     * `aria-disabled` + `data-disabled` (never the native `disabled` attribute,
     * which would suppress the hover / focus events a tooltip needs), opening is
     * blocked, an open popup closes, and the trigger leaves the tab order unless
     * `focusableWhenDisabled` is set. Stored as state, mirroring `setItems` /
     * `setChosenItems` (this design keeps mutable state out of settings).
     * @group Disabling
     */
    setDisabled(value: boolean): void;
    /**
     * Whether the whole control is disabled.
     * @group Disabling
     */
    isDisabled(): boolean;
    /** Reflect `this.disabled` onto the trigger's ARIA / data / tabindex. */
    private syncDisabledStateToDom;
    /**
     * Recompute the trigger's tabindex from every input that owns it: disabled
     * state (with `focusableWhenDisabled`), and the filterable open cycle -
     * while the filter input is the focus host the trigger leaves the tab
     * order, so the open widget stays a single tab stop and Shift+Tab exits
     * instead of landing on the trigger with the popup still open
     * (`docs/llm/A11Y.md` "Focus").
     */
    private syncTriggerTabindex;
    /**
     * Subclass hook: called once after the popup finishes opening. Default no-op.
     * The `onOpen` setting fires alongside this (both run) - hook for subclass
     * logic, setting for consumer notification.
     * @group Subclassing: reactions
     */
    protected onOpened(): void;
    /**
     * Subclass hook: called once after the popup finishes closing. Pairs with the `onClose` setting (both run).
     * @group Subclassing: reactions
     */
    protected onClosed(): void;
    /**
     * Subclass hook: called after the chosen state actually changed, right
     * before the variant's `onChange` setting fires (hook first, both run -
     * same pairing as `onOpened` / `onClosed`). Default no-op.
     * @group Subclassing: reactions
     */
    protected onChosenChanged(): void;
    /**
     * Called after `setItems` finishes. Override to reconcile state that
     * depends on the item list (e.g. clear a chosen value that disappeared).
     * Default no-op.
     * @group Subclassing: reactions
     */
    protected onItemsChanged(): void;
    /**
     * Orchestrator: composes the clear-button rebuild + `renderTriggerContent` +
     * `renderTriggerArrow` to (re)build the whole trigger from state; touches no
     * DOM directly. Subclasses normally override {@link renderTriggerContent},
     * not this.
     * - Runs on every change of the chosen value, the placeholder or the pack.
     * - `rerender()` runs it too.
     * - While `clearable` is on, the first run builds the clear button and every
     *   later run swaps it for whatever `createTriggerClearButtonEl` returns: a
     *   fresh button from the base implementation, like the arrow. An override
     *   may return the previous element; it is then kept in place.
     * - If the button is rebuilt and the old one held focus - on it or inside
     *   its icon - the rebuilt BUTTON gets it.
     * - If the builder returned the same element, nothing was rebuilt, and focus
     *   goes back to the node that held it, if that node is still inside the
     *   button and can take focus; otherwise focus stays on the button.
     * - `setItems` runs `renderTriggerContent` alone, because only the content
     *   reads the list (the multiple count total, a custom
     *   `createTriggerContentElFn`'s `items`).
     * - A `setItems` that drops the chosen entry runs the whole trigger.
     * - Runs once from the `LLSelectSingle` / `LLSelectMultiple` constructor, right
     *   after `super()`.
     * - On that first run a FURTHER subclass's own fields are still `undefined`:
     *   JS runs a subclass's field initializers only after its super constructor
     *   returns (virtual-call-in-constructor). An override of a trigger method
     *   (`renderTriggerContent`, or a `create*El` it calls) that reads such a
     *   field sees `undefined` there.
     * - The recipe: put construction-time configuration in the typed
     *   `subclassSettings` constructor param. `this.settings` is complete before
     *   any construction code runs.
     * - For genuine instance state, tolerate defaults during construction, or call
     *   `rerender()` at the end of your own constructor.
     * - The popup-list methods do NOT run here; they wait for `open()`. See
     *   DESIGN.md "Customization model".
     * @group Subclassing: rendering
     */
    protected renderTrigger(): void;
    /**
     * Write the trigger's content slot (`triggerContentEl`), replacing whatever
     * was there; the sibling arrow slot is untouched. The single DOM-writing
     * primitive behind every `renderTriggerContent` path.
     * - `string` -> set as `textContent` (plain text, NOT parsed as HTML). Used
     *   for the default placeholder / `itemToString` text / count summary.
     * - `HTMLElement` -> inserted as-is via `replaceChildren`; caller owns the
     *   node. Used for whatever the `createTriggerContentElFn` setting returned.
     * - Also mirrors the value into the hidden `triggerValueEl` (the accessible
     *   name source): the string itself, else `plainTextValue`, else the
     *   element's `textContent`. Pass `plainTextValue` whenever the element
     *   contains labelled controls (tag remove buttons) or icon-only content -
     *   the mirror is what AT announces as the field's value.
     * Called by `renderTriggerContent` - the base default and the `LLSelectSingle`
     * / `LLSelectMultiple` overrides.
     * @group Subclassing: rendering
     */
    protected commitTriggerContentToDom(content: HTMLElement | string, plainTextValue?: string): void;
    /**
     * Mirror the empty/filled state onto the trigger's `data-empty` attribute
     * (`"true"` when `isEmpty()`, else `"false"`). A CSS / AT styling hook,
     * independent of the rendered content. Called by the subclass
     * `renderTriggerContent` overrides.
     * @group Subclassing: rendering
     */
    protected syncEmptyStateToDom(): void;
    /**
     * Whether the control currently has no selection (drives `data-empty`).
     * Base default is always `true` (the base trigger only shows the
     * placeholder); `LLSelectSingle` / `LLSelectMultiple` override it.
     * @group Subclassing: semantics
     */
    protected isEmpty(): boolean;
    /**
     * Orchestrator: composes the `*ToDom` primitives to (re)build the trigger's
     * content slot from state; touches no DOM directly. Override in subclasses to
     * display the chosen value(s); this base default commits the placeholder and
     * the empty flag. Always write via `commitTriggerContentToDom` (content) and
     * `syncEmptyStateToDom` (the `data-empty` flag), never `triggerContentEl`
     * directly, so the sibling arrow slot is always preserved.
     * @group Subclassing: rendering
     */
    protected renderTriggerContent(): void;
    /**
     * Orchestrator: composes the `*ToDom` / `*El` primitives to (re)build the
     * trigger's arrow slot from state; touches no DOM directly. Calls
     * `createTriggerArrowContentEl` with the current `isOpened` and commits whatever it returns
     * (including `null` -> no arrow for this state).
     */
    private renderTriggerArrow;
    /**
     * Trigger arrow element for the given open state.
     * - Default reads `createTriggerArrowContentElFn`; `null` (setting unset, or returned for
     *   a state) = no arrow for that state.
     * - Override only when extending; for one-off arrows pass the setting.
     *   Mirrors `createTriggerClearButtonEl` / `createItemContentEl`.
     * @group Subclassing: rendering
     */
    protected createTriggerArrowContentEl(state: {
        isOpened: boolean;
    }): HTMLElement | SVGElement | null;
    /**
     * Write the trigger's arrow slot: clear it, then append `el` if non-null.
     * - `el = null`: clear only, leaving the slot empty (no arrow this state).
     * The sole mutator of the arrow slot; called by `renderTriggerArrow`.
     */
    private commitTriggerArrowContentElToDom;
    /**
     * Orchestrator: composes `createItemEl` (build) + `computePopupSegments` +
     * `commitPopupSegmentsToDom` (write) to rebuild the popup list from
     * `getVisibleItems()`; touches no DOM directly. Called by `open()` and by
     * `setItems()` while open. Also clamps `focusedIndex` if the list shrank and
     * re-applies focus visuals.
     * - Extend it by wrapping: override, do your work before or after, then
     *   call `super.renderPopupList()`. The ui-select bridge frees its row
     *   scopes this way. Or override one of the methods it calls through `this`:
     *   `createItemEl`, `createPopupListLeadingRowEl`, `itemToGroupKey`,
     *   `getVisibleItems`.
     * - Its other internals stay private on purpose. They re-establish the
     *   `itemEls[i] <-> getVisibleItems()[i]` alignment as one unit, so no
     *   subclass can leave keyboard nav or `aria-activedescendant` half-synced.
     * @group Subclassing: rendering
     */
    protected renderPopupList(): void;
    /**
     * Split the flat visible list into render segments: ungrouped item elements
     * and contiguous same-key groups. Pure computation - resolves keys via
     * `itemToGroupKey` (the overridable method; all-`null` keys = flat list) and key
     * equality via `groupKeyCompareFn`, touches no DOM. Group headers are NOT
     * added to `itemEls`, so `itemEls[i]`
     * stays aligned with `getVisibleItems()[i]` and keyboard nav skips headers for
     * free. `console.warn`s once per non-contiguous key reappearance (unsorted
     * data would otherwise emit a duplicate header for the same group) -
     * reachable with `gatherGroups: false`; the default gather feeds this an
     * already-contiguous list.
     */
    private computePopupSegments;
    /** Replace every popup-list child with the leading row (when present) + the rendered segments. */
    private commitPopupSegmentsToDom;
    /**
     * Build a detached group container: `role="group"` named by `groupKeyToString`,
     * an `aria-hidden` visible label element, then the group's item elements. The
     * label content comes from `createGroupLabelContentEl` (rich header) when
     * non-null, else the plain label text. `aria-disabled` + `data-disabled` when
     * the group is disabled. Override for full control of the group element
     * (mirrors `createItemEl`).
     *
     * @param key - the group's key
     * @param index - group index in the current render; builds a stable id
     * @param items - the group's items (for rich content / counts)
     * @param itemEls - the group's already-built option elements
     * @group Subclassing: rendering
     */
    protected createGroupEl(key: GroupKey, index: number, items: readonly T[], itemEls: HTMLElement[]): HTMLElement;
    /**
     * Group header -> its visible content element (icon / count badge / rich
     * markup). Mirrors `createItemContentEl`.
     * - Default reads `createGroupLabelContentElFn`, else `null` so `createGroupEl`
     *   uses plain text from `groupKeyToString`.
     * - The group's accessible name stays `groupKeyToString` (container `aria-label`);
     *   this fills only the visible, `aria-hidden` label content.
     * - Override only when extending; for one-off rich headers pass the setting.
     * @group Subclassing: rendering
     */
    protected createGroupLabelContentEl(key: GroupKey, itemsInGroup: readonly T[]): HTMLElement | null;
    /**
     * Re-render a single item's element in place instead of rebuilding the
     * whole popup list. The DOM work is O(1) regardless of list size, so
     * flipping one selection in a 10k-item list does not recreate 10k nodes
     * (the lookup to find the item is O(n), but that is a cheap comparison
     * loop next to DOM mutation). No-op if the popup is closed or the item is
     * not in the current list. Used by multi-select toggle.
     * @group Subclassing: rendering
     */
    protected replacePopupListItemElInDom(item: T): void;
    /**
     * Build the DOM element for one item. The base implementation sets `id`,
     * `role="option"`, a click handler, and fills the visible content via
     * {@link createItemContentEl} (which reads `createItemContentElFn`), falling
     * back to `textContent` from {@link itemToString}. When the content is
     * custom (non-null), the option's `aria-label` is set from `itemToString`
     * so the accessible name stays the plain `itemToString` text. For one-off rich content
     * (icons etc.) prefer the `createItemContentElFn` setting; override this only
     * to control the whole element (tag, extra wiring).
     *
     * @param item - the item value
     * @param index - index in `this.items`; used to build a stable id so
     *   `aria-activedescendant` can point to this element across re-renders.
     * @group Subclassing: rendering
     */
    protected createItemEl(item: T, index: number): HTMLElement;
    /**
     * Map an item to its display string. The library calls this everywhere it
     * needs an item's text: list rows, the single trigger text, default filter.
     * - Default reads the `itemToStringFn` setting, else `String(item)`.
     * - Configure via `itemToStringFn` (no subclass needed).
     * - Override only when extending (a new select type); your override replaces
     *   the default. For HTML content, subclass `createItemEl`.
     * @group Subclassing: semantics
     */
    protected itemToString(item: T): string;
    /**
     * Item -> the visible content of its list row (icon + text etc.).
     * - Default reads `createItemContentElFn`, else `null` so `createItemEl` uses
     *   the plain-text default from `itemToString`.
     * - Override only when extending; for one-off rich content pass the setting.
     * @group Subclassing: rendering
     */
    protected createItemContentEl(item: T): HTMLElement | null;
    /**
     * Whether `item` is effectively disabled - by `itemDisabledFn`, or because
     * its group is disabled (`groupDisabledFn`). Group-disabled layers on top,
     * so every item-disabled behavior (no selection, keyboard skip, aria)
     * covers grouped items with no extra code. False when neither applies.
     * The whole-control disabled state (`isDisabled()`) is a separate layer,
     * not part of this answer.
     * @group Subclassing: semantics
     */
    protected isItemEffectivelyDisabled(item: T): boolean;
    /**
     * Map an item to its group key, or `null` when it belongs to no group.
     * The authoritative method: rendering, the `gatherGroups` gather, and the
     * disabled layer all resolve keys through this method, so an override
     * drives them all - returning keys turns grouping on even with the setting
     * unset (all-`null` keys = flat list). An override reading external state
     * must call `rerender()` after that state changes (same contract as
     * mutating item objects).
     * - Default reads `itemToGroupKeyFn`, else `null` (grouping off).
     * - Override only when extending; configure via the setting.
     * @group Subclassing: semantics
     */
    protected itemToGroupKey(item: T): GroupKey | null;
    /**
     * Map a group key to its header display text.
     * - Default reads `groupKeyToStringFn`, else `String(key)`.
     * @group Subclassing: semantics
     */
    protected groupKeyToString(key: GroupKey): string;
    /**
     * Whether the whole group `key` is disabled per `groupDisabledFn` (false when unset).
     * @group Subclassing: semantics
     */
    protected isGroupDisabled(key: GroupKey): boolean;
    /**
     * First enabled index scanning from `start` (inclusive) by `step` (+1 / -1).
     * Returns -1 if no enabled item lies in that direction. Used to skip disabled
     * items during keyboard nav and initial focus.
     * @group Subclassing: focus
     */
    protected findNextEnabledIndex(start: number, step: number, list: readonly T[]): number;
    /**
     * Resolve a nav target index to the nearest enabled item. Arrows / Home / End
     * stay put when no enabled item lies in the travel direction; Page falls back
     * to the opposite direction so it lands as far as it can.
     */
    private findEnabledIndexForAction;
    /**
     * Called when an item is activated (click or keyboard select). Default
     * no-op; subclasses implement their selection behaviour (single mode picks
     * and closes, multiple mode toggles and keeps the popup open).
     * @group Subclassing: reactions
     */
    protected onItemActivated(_item: T): void;
    /**
     * Optional non-item `role="option"` row pinned at the TOP of the listbox:
     * inside the arrow-key ring (ArrowUp from the first item reaches it, Home
     * lands on it, up-actions clamp there) but never inside `itemEls`, so the
     * `itemEls[i] <-> getVisibleItems()[i]` alignment is untouched. Rebuilt on
     * every `renderPopupList`. Base default: `null` = no leading row.
     * `LLSelectMultiple` builds its choose-all row here (`chooseAllRow` setting).
     * @group Subclassing: rendering
     */
    protected createPopupListLeadingRowEl(): HTMLElement | null;
    /**
     * Run `fn` with chosen-state changes attributed to the user. The library
     * wraps exactly its pointer / keyboard entry points with it - option
     * activation, the tag remove button, the clear button, the choose-all row;
     * everything else reports `'api'`.
     * @group Subclassing: reactions
     */
    protected withUserChangeSource<R>(fn: () => R): R;
    /**
     * Subclass hook: the leading row was activated - Enter while it is focused
     * (subclasses also wire their row's click handler to this). Default no-op.
     * @group Subclassing: reactions
     */
    protected onLeadingRowActivated(): void;
    /**
     * Decide which item to focus when the popup opens. Default focuses the
     * first item (or no-op if the list is empty). Override to focus the
     * currently chosen item, last-used item, etc.
     * @group Subclassing: focus
     */
    protected focusInitial(): void;
    /**
     * Move keyboard focus to the item at `index`. The value is clamped to
     * `[-1, items.length-1]`; pass `-1` to clear focus. Updates the focused
     * class, `aria-activedescendant`, and scrolls the item into view. No-op
     * if the clamped value equals the current focused index.
     * @group Subclassing: focus
     */
    protected setFocusedIndex(index: number): void;
    /**
     * Move keyboard focus onto the leading row. Returns whether the row is now
     * focused (`false` = none is rendered, nothing changed). The item focus is
     * cleared (`focusedIndex` becomes -1). Protected so a subclass can wire its
     * leading row's click to focus-then-activate (mirroring how item clicks
     * call `setFocusedIndex` before `onItemActivated`) and use it in
     * `focusInitial` (the leading row is the listbox's FIRST option).
     * @group Subclassing: focus
     */
    protected focusLeadingRow(): boolean;
    /**
     * Rebuild the leading row in place (tri-state / text refresh) without
     * touching the item elements - O(1) DOM work, mirroring
     * `replacePopupListItemElInDom`. Falls back to a full `renderPopupList`
     * when the row becomes inapplicable (builder returns `null`). No-op while
     * closed or when no leading row is rendered.
     * @group Subclassing: rendering
     */
    protected replaceLeadingRowElInDom(): void;
    /**
     * Make the DOM reflect `focusedIndex`: move the focused class onto the focused
     * item element, point `aria-activedescendant` at it, and scroll it into view;
     * when `focusedIndex` is -1 or out of range, clear the class and the attribute.
     * Reads `itemEls`, so it only has an effect while the popup is open (the list
     * exists). Called after `focusedIndex` changes (`setFocusedIndex`) and after
     * the list is rebuilt (`renderPopupList`).
     */
    private syncFocusedIndexToDom;
    /**
     * Snapshot the window scroll position and return a function that restores
     * it. Opening the popup must never move the page, but Firefox auto-scrolls
     * the active option of a multiselectable listbox into view at the document
     * level when the popup is shown - even though the popup is `position: fixed`.
     * The returned restore runs synchronously and once more on the next frame,
     * since that accessibility scroll can land after the current layout flush.
     * No-op when nothing actually scrolled, so it never fights real user
     * scrolling (and stays silent under jsdom, which has no `window.scrollTo`).
     * Restores with `behavior: 'instant'`: the two-arg `scrollTo` obeys the
     * page's CSS `scroll-behavior`, so under `scroll-behavior: smooth` the
     * correction would render as a visible glide instead of a revert.
     */
    private captureWindowScroll;
    /**
     * The element a pointer event actually hit. `ev.target` retargets to the shadow
     * host when llselect is hosted inside an app's shadow root, so an inside click
     * would read as outside and close the popup; `composedPath()[0]` pierces the
     * boundary. With no shadow tree (llselect uses none itself) this equals `ev.target`.
     * - Limitation: a CLOSED host shadow root truncates `composedPath()` at the root,
     *   so `[0]` is only the host and an inside click still reads as outside. A
     *   document-level listener cannot see into a closed root; hosting in an OPEN
     *   shadow root avoids it.
     */
    private eventTargetNode;
    private attachOutsideClick;
    private detachOutsideClick;
    /**
     * Close the popup when keyboard focus leaves the widget entirely (e.g. Tab
     * away). `focusout` bubbles, so listening on `rootEl` catches focus leaving
     * any descendant; `relatedTarget` is the element gaining focus (or `null`).
     * The check is written against `rootEl.contains` rather than "the trigger
     * lost focus" so a future in-popup control - filter input, checkbox - keeps
     * the popup open while it holds focus.
     */
    private attachFocusOut;
    private detachFocusOut;
    private handleKeydown;
    /**
     * Native-`<select>`-style prefix typeahead for a printable-character
     * keydown on the trigger.
     * - Returns `true` when the event was consumed.
     * - Runs only while the filter is inactive. With the popup open the filter
     *   input owns typing; while closed, a would-be-filterable open cycle
     *   leaves the keys alone (the `filterable` predicate is evaluated fresh -
     *   the cached `filterActive` can be stale between opens).
     * - Space never joins the buffer; it stays the activate/open key (A11Y.md).
     * - While closed: opens the popup first, then searches relative to
     *   {@link computeTypeaheadClosedStartIndex} - NOT to the convenience focus
     *   `focusInitial` parked, which would skip the first match.
     * - Typing itself never changes the value and never fires `onChange`;
     *   activation stays Enter / Space / click.
     * - Match rule, cycling, and wrap: {@link findTypeaheadIndex}. No match
     *   leaves the active option and the buffer as they are.
     */
    private handleTypeaheadKeydown;
    /**
     * The option the closed-state typeahead treats as current, as an index into
     * `list`, when the typed character is the keystroke that opens the popup.
     * - The search starts AFTER this option: the opening keystroke is always a
     *   one-character buffer, because `close()` and a refused `open()` both
     *   empty the buffer.
     * - Default `-1`: no current option, so the first match from the top wins.
     * - The focus `focusInitial` parks on open is a convenience, not a
     *   selection - it must not shift this search.
     * - Single mode overrides this with the chosen item's index, so a typed
     *   initial cycles past the current selection like a native `<select>`.
     * - Search internals: `findTypeaheadIndex` in `keyboard.ts`.
     * @group Subclassing: focus
     */
    protected computeTypeaheadClosedStartIndex(_list: readonly T[]): number;
    private createTriggerEl;
    /**
     * Build the clear (x) button for the `clearable` trigger slot.
     * - The library owns the button, its click (stops propagation so it never
     *   toggles the popup, then `clearSelection`; a no-op while the control is
     *   disabled) and its `aria-label` (text from
     *   `uiTranslationPack.triggerClearButtonAriaLabel`).
     * - `createTriggerClearButtonContentElFn` optionally fills the icon; else the
     *   theme's CSS glyph draws it.
     * - The theme hides the button via `data-empty` while nothing is chosen.
     * - It runs on every trigger render (`renderTrigger`).
     * - The first run builds the button.
     * - For `LLSelectSingle` / `LLSelectMultiple` and their subclasses, that
     *   first run is the variant constructor's render, right after `super()`,
     *   so it happens before a FURTHER subclass's field initializers.
     * - A direct `LLSelectBase` subclass gets the button on its first trigger
     *   render (its own `renderTrigger()` call, or `rerender()` /
     *   `setPlaceholder`); until then the trigger is unrendered and there is no
     *   button.
     * - Every later run (a value change, `setPlaceholder`,
     *   `setUiTranslationPack`, `rerender()`) swaps the button in place, unless
     *   this method returns the previous element - then it stays in place.
     * - The base implementation returns a fresh element each run, so, like the
     *   arrow, nothing put on it from outside survives a render; customize it
     *   here.
     * - An override that returns the same element every time owns everything
     *   the rebuild would otherwise refresh on it, including its `aria-label`
     *   after `setUiTranslationPack`.
     * - An override that reads subclass fields calls `rerender()` at the end of
     *   its constructor, like every other trigger method.
     * @group Subclassing: rendering
     */
    protected createTriggerClearButtonEl(): HTMLElement;
    /**
     * The element holding DOM focus if that is `el` or a descendant, else `null`.
     * - Read from `el`'s own root: inside a shadow root `document.activeElement`
     *   is the shadow HOST, so the answer must come from the `ShadowRoot`'s
     *   `activeElement` (the `Document`'s otherwise).
     * - The result can sit inside an open shadow root under `el`, so
     *   `el.contains()` may reject it; use `containsComposed` for that check.
     * - A CLOSED shadow root is opaque, so its host is returned.
     */
    private focusedElementIn;
    /** Whether `node` is `el` or inside it, walking up through open shadow hosts. */
    private containsComposed;
    /** Whether DOM focus is on `el` or inside it (see `focusedElementIn`). */
    private isFocused;
    /**
     * Build the clear button on the first trigger render, and swap it for a fresh
     * one on every later render, always through `createTriggerClearButtonEl` (the
     * overridable builder) - so `rerender()` repairs an override that reads
     * subclass fields. Keeps DOM focus on the new button when the old one held it.
     * No-op without `clearable`.
     * The order of the steps is load-bearing:
     * 1. Read which node holds focus, if it is the old button or inside it.
     * 2. If one does, park focus on the old button itself. A content fn that
     *    hands back the same icon element each time reparents that icon into
     *    the new button, and the reparenting must not move the focused node.
     * 3. Build the new button.
     * 4. Insert it.
     * 5. If step 1 found focus, move focus to the new button - or, if that
     *    element cannot take focus, to the trigger.
     * 6. Only then remove the old one.
     * Why this order:
     * - Removing the old button first drops DOM focus to `<body>` in every
     *   engine.
     * - Where the engine also fires `focusout` on that removal, its
     *   `relatedTarget` is `null`. The open popup's focus-out guard reads that
     *   as focus leaving the widget.
     * - Moving focus first makes the new button the `relatedTarget`, inside the
     *   root.
     * A builder override that returns the SAME element every time is allowed:
     * the element is kept in place, and focus goes back to the node step 1
     * found, because nothing was rebuilt - unless the override detached that
     * node or moved it out of the button, in which case focus stays on the
     * button.
     */
    private replaceTriggerClearButtonElInDom;
    /**
     * Clear button's visible content (its x icon).
     * - Default reads `createTriggerClearButtonContentElFn`; `null` (setting
     *   unset, or returned) = no icon - the theme's CSS glyph draws the x.
     * - Override only when extending; for one-off icons pass the setting.
     * @group Subclassing: rendering
     */
    protected createTriggerClearButtonContentEl(): HTMLElement | SVGElement | null;
    /**
     * Empty the selection (invoked by the clear button). Base is a no-op; single
     * clears to `undefined`, multiple to `[]`. Goes through the normal setters, so
     * `onChange` fires with the empty value. The wipe is total - chosen disabled
     * items are cleared too (native `<select>` parity; `unchooseAll` is the
     * enabled-only bulk op).
     * @group Subclassing: semantics
     */
    protected clearSelection(): void;
    /**
     * The filter input lives inside the popup, above the listbox. Always built
     * (`hidden` when `filterable: false`) so a future runtime toggle is a CSS
     * flip rather than a DOM rebuild. See `docs/llm/DESIGN.md`.
     */
    private createFilterInputEl;
    /**
     * Return the items the popup list renders, in display order.
     *
     * ```text
     * items                (setItems)
     *   |  gather          (only with grouping on; result cached until setItems)
     *   v
     * display base list
     *   |  filter query    (only while a query is active)
     *   v
     * visible items        (this method's return value)
     *   |  render
     *   v
     * DOM rows
     * ```
     *
     * - If no filter query is active (including while closed), it returns the
     *   full list, gathered per `gatherGroups` when grouping is on.
     * - If a filter query is active, it returns the matching subset.
     * - Disabled items are included. They render (grayed); only actions skip
     *   them (keyboard focus, the choose-all row's subset).
     * - Returns the LIVE internal array, typed read-only. Never mutate it
     *   (see `getItems`).
     * - Subclasses use it too (e.g. for selection-by-index), and may override
     *   it to add a step: `LLSelectMultiple` does for `hideChosenRows`.
     * @group Items
     */
    getVisibleItems(): readonly T[];
    /**
     * The base list in DISPLAY order: `items` gathered per `gatherGroups`
     * (memoized until the next `setItems`), or `items` as-is while grouping is
     * off or `gatherGroups` is false. Filtering and rendering read this, never
     * `items` directly - so the gather runs lazily, at first need after a
     * `setItems`.
     */
    private getDisplayBaseItems;
    /**
     * Return the filter input's current query, exactly the string passed to
     * `filterFn`.
     * - It is `''` while the popup is closed, the filter is inactive, or the
     *   input is empty.
     * - It resets on close: each open cycle starts empty.
     * @group Filtering
     */
    getFilterQuery(): string;
    /**
     * Per-item match predicate for the filter input.
     * - Default reads `filterFn`; else case-insensitive substring on
     *   `itemToString`.
     * - Override only when extending (subclass-wide custom matching); for a
     *   one-off match rule pass the setting.
     * @group Subclassing: semantics
     */
    protected matchesQuery(item: T, query: string): boolean;
    /**
     * Recompute `filteredItems` from the current `items` and `query`. Pure state
     * update: does NOT touch the DOM (the caller re-renders the list separately).
     * No-op when `filterable: false`; an empty query keeps every item. Called from
     * `open()`, from `setItems()`, and on each filter-input event.
     */
    private recomputeFilteredItems;
    /**
     * Input event on the filter field: re-filter, re-render the list, move the
     * active option to the first match. IME composition is guarded - we wait
     * for `compositionend` and filter once with the composed text.
     */
    private handleSearchInputEvent;
    /** Outer popup wrapper. No ARIA role; structural only. */
    private createPopupEl;
    /**
     * Build the no-results message element. `role="status"` announces its
     * appearance politely; it lives OUTSIDE the listbox (options-only children)
     * and its text comes from `uiTranslationPack.popupListNoResults`.
     */
    private createPopupListNoResultsEl;
    /**
     * The no-results message's visible content (rich empty-state).
     * - Default reads `createPopupListNoResultsContentElFn`; `null` (setting
     *   unset, or returned) = plain text from `uiTranslationPack.popupListNoResults`.
     * - Override only when extending; for one-off content pass the setting.
     * @group Subclassing: rendering
     */
    protected createPopupListNoResultsContentEl(query: string): HTMLElement | null;
    /**
     * Mirror the visible-list-empty state onto the no-results message element:
     * `hidden` while there is at least one visible item; when shown, fill its
     * content - `createPopupListNoResultsContentEl(query)` first, else the plain
     * text from `uiTranslationPack.popupListNoResults`. Same null-branch shape as
     * `createItemEl` / `createGroupEl`.
     * - The write is SKIPPED when the resolved text matches what is already shown,
     *   so a still-empty next keystroke does not re-announce (see `lastNoResultsText`).
     */
    private syncPopupListNoResultsToDom;
    /** Inner element with `role="listbox"`. Holds item children. */
    private createPopupListEl;
}
//# sourceMappingURL=base.d.ts.map