@llselect/core
Version:
Low-level select: a minimal, flexible, framework-agnostic replacement for the native HTML select element.
103 lines • 5.27 kB
TypeScript
/**
* Logical actions a keyboard interaction can map to. Numeric values are
* implementation detail; never serialise them.
*/
export declare enum LLSelectAction {
/** Open the popup (no item activation). */
Open = 0,
/** Close the popup (no item activation). */
Close = 1,
/** Activate the currently focused option (select + maybe close). */
Select = 2,
/** Move focus to the next option (clamps at last). */
Next = 3,
/** Move focus to the previous option (clamps at first). */
Previous = 4,
/** Move focus to the first option. */
GotoFirst = 5,
/** Move focus to the last option. */
GotoLast = 6,
/** Jump focus down by a fixed page size. */
PageDown = 7,
/** Jump focus up by a fixed page size. */
PageUp = 8
}
/**
* Map a keydown event to a logical {@link LLSelectAction}, given whether the
* popup is currently open.
* - Returns `undefined` if the key should be left alone (no preventDefault,
* no library reaction).
* - Maps the action keys of the ARIA APG combobox pattern.
* - The pattern's printable-character typeahead is not mapped here - it needs
* the character, which an action enum cannot carry. The keydown handler
* runs {@link findTypeaheadIndex} before this mapping.
*
* @param inTextInput - true when focus is in the editable filter input. There,
* Space must type a space and Home/End must move the text caret, so those
* keys are NOT mapped to selection / first-last navigation. Selection is
* Enter only; option navigation is the arrow / page keys.
*/
export declare function getActionFromKey(ev: KeyboardEvent, isOpened: boolean, inTextInput?: boolean): LLSelectAction | undefined;
/**
* Compute a new focused-option index after applying a navigation action.
* Clamps to `[0, maxIndex]` (no wrap-around). Returns `-1` if there are no
* options (`maxIndex < 0`). The page size for PageUp/PageDown is a fixed
* constant.
*
* @param currentIndex - current focused index (`-1` for "none")
* @param maxIndex - largest valid index (`options.length - 1`)
*/
export declare function getUpdatedIndex(currentIndex: number, maxIndex: number, action: LLSelectAction): number;
/**
* Pause (in ms) after which the next typed character starts a new typeahead
* buffer instead of extending the old one. Native `<select>` implementations
* use 1 s (Blink / WebKit).
*/
export declare const TYPEAHEAD_TIMEOUT_MS = 1000;
/**
* Compute the typeahead buffer after a newly typed character.
* - If `elapsedMs` since the previous character exceeds
* {@link TYPEAHEAD_TIMEOUT_MS}, the character starts a fresh buffer.
*/
export declare function getUpdatedTypeaheadBuffer(buffer: string, char: string, elapsedMs: number): string;
/**
* Resolve where prefix typeahead moves the active option; `-1` = no match.
* Mirrors native `<select>` typeahead:
* - An option matches when its text starts with `buffer`, case-insensitive.
* - A one-character buffer searches from the option AFTER `currentIndex`, so
* repeated presses of one initial cycle through the options sharing it.
* - A buffer of one repeated character (e.g. `"aa"`) behaves exactly like its
* single character - it keeps cycling. It is never matched literally (the
* W3C APG example tries the literal `"aa"` prefix first; native does not,
* and a text really starting `"aa"` is still reached by the cycle).
* - Any other longer buffer searches from `currentIndex` itself, so extending
* the buffer stays on the current option while it still matches.
* - The search wraps around the whole list - deliberately, unlike the clamped
* arrow navigation (see `docs/llm/A11Y.md`): a search means "anywhere", and
* cycling needs the wrap.
* - Indexes where `textAt` returns `undefined` (disabled options) never match.
* - `currentIndex` `-1` means no option is active; the search starts at 0.
*
* The repeated-character test compares CODE POINTS, each lower-cased on its
* own: a key whose lower-case form expands to two code units (the Turkish
* dotted capital I) still counts, astral-plane characters compare whole, and
* a mid-repeat Shift ("aA") still cycles.
*
* @param textAt - match text of the option at an index, or `undefined` when
* that option must never match.
*/
export declare function findTypeaheadIndex(buffer: string, count: number, currentIndex: number, textAt: (index: number) => string | undefined): number;
/**
* Scroll `scrollParent` just enough so `child` is fully visible. No-op if
* `child` is already in view. Adjusts `scrollTop` directly rather than using
* `scrollIntoView`, so the page (window) does not scroll alongside.
*
* Uses viewport-rect deltas, NOT `offsetTop`: `offsetTop` is relative to the
* offset parent, which a theme could change by making a group container
* `position: relative` (optgroup), silently breaking the math. Rect deltas are
* correct regardless of nesting / theme CSS. `clientTop` / `clientHeight`
* exclude the parent's border so a bordered list stays exact. Measured to cost
* the same as the old `offsetTop` path (see docs/llm/DESIGN.md "Optgroup").
*/
export declare function ensureVisibleInScroll(child: HTMLElement, scrollParent: HTMLElement): void;
//# sourceMappingURL=keyboard.d.ts.map