UNPKG

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
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;