ranui
Version:
A framework-agnostic Web Components UI library built on native custom elements, with TypeScript types, light/dark theming, SSR and PWA support.
270 lines (269 loc) • 11.1 kB
TypeScript
import { RanElement } from '../../utils';
import '@/components/select/option';
import '@/components/dropdown';
import '@/components/select/dropdown-item';
import { FloatingController } from '../../utils/floating';
import type { Placement } from '../../utils/placement';
import '@/components/input';
import { EventManager } from '../../utils/builder';
interface Option {
label: string | number;
value: string | number;
disabled?: boolean;
}
/**
* @fires show - The panel is about to appear.
* @fires after-show - The panel has appeared and any entrance animation has finished.
* @fires hide - The panel is about to close.
* @fires after-hide - The panel has closed and any exit animation has finished.
*/
export declare class Select extends RanElement {
static formAssociated: boolean;
_internals?: ElementInternals;
_events: EventManager;
_searchEvents: EventManager;
removeTimeId?: NodeJS.Timeout;
_listboxId: string;
_activeIndex: number;
_slot: HTMLSlotElement;
_shadowDom: ShadowRoot;
_select: HTMLDivElement;
_selection: HTMLDivElement;
_search: HTMLInputElement;
_icon: HTMLElement;
_selectDropdown?: HTMLDivElement;
_selectionDropdown?: HTMLElement;
/**
* Positioning, portalling, scroll-following, the enter/exit animation and the
* show/hide events all live in the shared controller — r-popover drives the
* same one, so the two cannot drift apart the way their hand-copied
* reposition listeners had begun to.
*/
_floating: FloatingController;
_optionList: Option[];
_optionLabelMapValue: Map<string, string>;
_optionValueMapLabel: Map<string, string>;
_activeOption?: HTMLElement;
_text: HTMLSpanElement;
_selector: HTMLDivElement;
_label: HTMLLabelElement | undefined;
onSearch?: (this: HTMLElement, ev: Event) => unknown;
_typeaheadBuffer: string;
_typeaheadTimeId?: NodeJS.Timeout;
static get observedAttributes(): string[];
constructor();
get value(): string;
set value(value: string);
syncFormValue: () => void;
get required(): boolean;
set required(value: boolean | string);
/**
* @description: 获取字段上方的静态说明文字(label)。
*/
get label(): string;
/**
* @description: 设置字段上方的静态说明文字(label)。
*/
set label(value: string);
/**
* A static caption above the field — same pattern as r-input's `label`
* (see input/index.ts `listenLabel`), so a labeled select and a labeled
* input placed side by side in a form line up: same token, same "renders
* above, reserves its own space, never overlaps" behavior. Associated via
* `aria-label` (plain text) rather than `aria-labelledby` pointing at the
* rendered `<label>`'s id: that label lives inside this select's own shadow
* root, and a plain `aria-labelledby` id-ref cannot cross into a shadow
* tree — even the host's own child shadow root counts as a separate tree.
* Verified directly: with `aria-labelledby`, the combobox's computed
* accessible name came back empty in the accessibility tree.
*/
private _syncLabel;
/**
* Lets `required` be seen by form.checkValidity()/reportValidity()/:invalid,
* and mirrors it into aria-required/aria-invalid for assistive tech.
* Disabled selects never block submission, matching native semantics.
*/
private _updateValidity;
checkValidity(): boolean;
reportValidity(): boolean;
get validity(): ValidityState | undefined;
get validationMessage(): string;
/**
* @description: 原生 form.reset() 时恢复到 defaultValue(若有)或清空选中项
*/
formResetCallback(): void;
get defaultValue(): string;
set defaultValue(value: string);
get showSearch(): string;
set showSearch(value: string);
get type(): string;
set type(value: string);
/**
* Which side of the trigger the panel opens on, with an optional alignment.
*
* `bottom`, `bottom-end`, `top-center`, … — the same grammar r-popover takes,
* because both now position through the same controller. A bare side means
* `-start`. Typed rather than left as `string`: these are the values the
* positioner understands, and an editor should say so.
*/
get placement(): Placement;
set placement(value: Placement);
get sheet(): string;
set sheet(value: string);
get getPopupContainerId(): string;
set getPopupContainerId(value: string);
get dropdownclass(): string;
set dropdownclass(value: string);
get trigger(): string;
set trigger(value: string);
get disabled(): boolean;
set disabled(value: boolean | string | undefined | null);
initAria: () => void;
updateAriaExpanded: (isExpanded: boolean) => void;
/**
* Whether the dropdown is showing.
*
* This attribute *is* the state; nothing else infers it. The panel's
* `style.display` cannot serve that purpose because it lags the state by the
* length of the exit animation, and every read of it during that window
* answers about the frame rather than the intent. Reflecting it the way
* `<details open>` and `<dialog open>` do also puts the state where a
* consumer can reach it: `:host([open])` in CSS, `select.open = true` from
* script, and an attribute assertion in a test instead of a poll.
*/
get open(): boolean;
set open(value: boolean);
/** Drive the panel from `open`. Everything below it is the controller's. */
_applyOpen: () => void;
/** Open the dropdown. */
show: () => void;
/** Close the dropdown. */
hide: () => void;
/** Flip the dropdown between open and closed. */
toggle: () => void;
getDropdownOptions: () => HTMLElement[];
syncActiveState: () => void;
setActiveOptionByIndex: (targetIndex: number) => void;
/**
* Vertically center the closed-state label by matching its line-height to the
* host height.
*
* Guarded on a non-zero height: during a custom-element **upgrade** (SSR/DSD
* markup, or any element whose `value` attribute is present before it is laid
* out) `attributeChangedCallback` runs before `connectedCallback`, so
* `getBoundingClientRect()` still reports 0. Writing `line-height: 0px` then
* collapses the label to invisible even though its text is correct — the
* component looks empty until something re-selects the option. When there is
* no layout yet we clear the inline value and let the stylesheet decide;
* `connectedCallback` re-applies it once the element has a box.
*/
applyLabelLineHeight: () => void;
selectOptionElement: (optionElement: HTMLElement | null, shouldDispatch?: boolean) => void;
isDropdownOpen: () => boolean;
/**
* Walk from `from` in the direction of `step` (clamped to the list bounds)
* and return the first non-disabled option index, or -1 if none exists.
* Keeps keyboard navigation from ever landing on a disabled option.
*/
_nextEnabledIndex: (from: number, step: number) => number;
keydownSelect: (e: KeyboardEvent) => void;
/**
* WAI-ARIA combobox type-ahead: typing a printable character jumps to the
* next option (wrapping) whose label starts with the recently-typed
* characters, matching the behavior of a native `<select>`.
*/
_handleTypeahead: (e: KeyboardEvent) => void;
handlerExternalCss(): void;
/**
* @description: 移除 select dropdown
* @return {*}
*/
setSelectDropdownDisplayNone: () => void;
/**
* @description: 添加 select dropdown
* @return {*}
*/
setSelectDropdownDisplayBlock: () => void;
/**
* Position the panel against the trigger.
*
* Kept as a method because it is part of this element's surface, but the work
* is the shared controller's — including staying with the trigger when the
* page scrolls, which this component and r-popover used to implement
* separately, one carrying a comment that it mirrored the other.
*
* @param applyEntranceTransit - Only true for the call that opens the panel.
* Scroll/resize repositioning reuses this while already open and must not
* replay the entrance animation on every tick.
*/
placementPosition: (applyEntranceTransit?: boolean) => void;
/**
* @description: 设置下拉框
* @return {*}
*/
/**
* Toggle, not close-then-open. The old body ran both transitions on every
* click and let the surviving one depend on which animation timer happened to
* be in flight, so a run of clicks on a stationary pointer produced: open,
* close, open-and-immediately-close, and an `aria-expanded` that had come
* apart from what was on screen.
*/
selectMouseDown: (e: Event) => void;
removeDropDownTimeId: (e: Event) => void;
/**
* @description: 焦点移除的情况,需要移除 select 下拉框
* @return {*}
*/
selectBlur: (e: Event) => void;
/**
* @description: 选中一个选项的情况
* @param {MouseEvent} e
* @return {*}
*/
clickOption: (e: MouseEvent) => void;
/**
* @description: 初始化创建选项下拉框
* @return {*}
*/
createOption: () => void;
/**
* @description: 移除选项下拉框
* @return {*}
*/
removeSelectDropdown: () => void;
/**
* @description: 当 select 中有 option 元素的时候,给 dropdown 添加元素
* @return {*}
*/
addOptionToSlot: () => void;
createSelectDropdownContent: (options?: Option[]) => void;
setDefaultValue: () => void;
changeSearch: (e: Event) => void;
clickRemoveSelect: (e: Event) => void;
connectedCallback(): void;
/**
* Re-run the `value` → label/active-option reflection after connect.
*
* `syncSelectedFromValue` bails out when `_activeOption` already matches, which
* is exactly the situation left behind by an upgrade-time sync: the option was
* marked active but the label was written without layout. Clearing the cached
* option first forces a full, correct pass.
*/
reapplyValueAfterConnect: () => void;
/**
* (Re)wire the search-box listeners to match the current `showSearch` value.
* Reactive: abort any previously-registered search listeners first, then
* re-register only while `showSearch` is truthy. Safe to call on connect and
* on every `showSearch` attribute change.
*/
_applyShowSearch: () => void;
disconnectedCallback(): void;
attributeChangedCallback(name: string, oldValue: string, newValue: string): void;
/**
* Reflect a programmatic `value` change to the closed-state label. Lets
* `select.value = 'x'` (or setAttribute('value', 'x')) update the displayed
* selection without the consumer having to "nudge" the active option.
*/
syncSelectedFromValue: (value: string) => void;
}
export default Select;