/** * Copyright Aquera Inc 2025 * * This source code is licensed under the BSD-3-Clause license found in the * LICENSE file in the root directory of this source tree. */ import { LitElement, html, } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { styles } from './nile-virtual-select.css'; import '../nile-icon'; import '../nile-popup/nile-popup'; import '../nile-tag/nile-tag'; import '../nile-checkbox/nile-checkbox'; import '../nile-loader/nile-loader'; import { animateTo, stopAnimations } from '../internal/animate'; import { classMap } from 'lit/directives/class-map.js'; import { query, state } from 'lit/decorators.js'; import { defaultValue } from '../internal/default-value'; import { FormControlController } from '../internal/form'; import { getAnimation, setDefaultAnimation, } from '../utilities/animation-registry'; import { HasSlotController } from '../internal/slot'; import { waitForEvent } from '../internal/event'; import { watch } from '../internal/watch'; import NileElement from '../internal/nile-element'; import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import type { NileFormControl } from '../internal/nile-element'; import type NileOption from '../nile-option/nile-option'; import type NilePopup from '../nile-popup/nile-popup'; import { ifDefined } from 'lit/directives/if-defined.js'; import type { VirtualOption, NileRemoveEvent, RenderItemConfig } from './types.js'; import { VirtualSelectSelectionManager } from './selection-manager.js'; import { VirtualSelectSearchManager } from './search-manager.js'; import { VirtualSelectRenderer } from './renderer.js'; import { PortalManager } from './portal-manager.js'; import { ResizeController } from '@lit-labs/observers/resize-controller.js'; import { VisibilityManager } from '../utilities/visibility-manager.js'; /** * Nile Virtual Select component. * * @tag nile-virtual-select * */ /** * @summary Virtual select component for handling large datasets with virtual scrolling. * @status stable * @since 2.0 * * @dependency nile-icon * @dependency nile-popup * @dependency nile-tag * @dependency nile-checkbox * * @slot label - The input's label. Alternatively, you can use the `label` attribute. * @slot prefix - Used to prepend a presentational icon or similar element to the combobox. * @slot clear-icon - An icon to use in lieu of the default clear icon. * @slot expand-icon - The icon to show when the control is expanded and collapsed. * @slot help-text - Text that describes how to use the input. Alternatively, you can use the `help-text` attribute. * * @event nile-change - Emitted when the control's value changes. * @event nile-clear - Emitted when the control's value is cleared. * @event nile-input - Emitted when the control receives input. * @event nile-focus - Emitted when the control gains focus. * @event nile-blur - Emitted when the control loses focus. * @event nile-show - Emitted when the select's menu opens. * @event nile-after-show - Emitted after the select's menu opens and all animations are complete. * @event nile-hide - Emitted when the select's menu closes. * @event nile-after-hide - Emitted after the select's menu closes and all animations are complete. * @event nile-invalid - Emitted when the form control has been checked for validity and its constraints aren't satisfied. * @event nile-search - Emitted when the user types in the search input. The event payload includes the search query for backend search functionality. * @event nile-scroll - Emitted when the user scrolls within the virtualized container. The event payload includes scroll position information. * @event nile-scroll-start - Emitted when the user starts scrolling within the virtualized container. * @event nile-scroll-end - Emitted when the user stops scrolling and reaches the bottom of the virtualized container (debounced). * * @csspart form-control - The form control that wraps the label, input, and help text. * @csspart form-control-label - The label's wrapper. * @csspart form-control-input - The select's wrapper. * @csspart form-control-help-text - The help text's wrapper. * @csspart combobox - The container the wraps the prefix, combobox, clear icon, and expand button. * @csspart prefix - The container that wraps the prefix slot. * @csspart display-input - The element that displays the selected option's label, an `` element. * @csspart listbox - The listbox container where options are slotted. * @csspart tags - The container that houses option tags when `multiple` is used. * @csspart tag - The individual tags that represent each multiselect option. * @csspart clear-button - The clear button. * @csspart expand-icon - The container that wraps the expand icon. * @csspart footer - The footer container with show selected and clear all options. */ @customElement('nile-virtual-select') export class NileVirtualSelect extends NileElement implements NileFormControl { static styles: CSSResultGroup = styles; private readonly formControlController = new FormControlController(this, { assumeInteractionOn: ['nile-blur', 'nile-input'], }); private readonly hasSlotController = new HasSlotController( this, 'help-text', 'label' ); private readonly portalManager = new PortalManager(this); @query('.select') popup: NilePopup; @query('.select__combobox') combobox: HTMLElement; @query('.select__display-input') displayInput: HTMLInputElement; @query('.select__value-input') valueInput: HTMLInputElement; @query('.virtualized') virtualizedContainer: HTMLElement; @state() private hasFocus = false; @state() displayLabel = ''; @state() selectedOptions: VirtualOption[] = []; @state() oldValue: string | string[] = ''; private scrollTimeout: number | undefined; private scrolling = false; private visibilityManager?: VisibilityManager; /** The name of the select, submitted as a name/value pair with form data. */ @property() name = ''; /** Array of all option items for virtual scrolling */ @property({ type: Array }) data: any = []; /** Original unfiltered option items for search functionality */ @state() private originalOptionItems: any = []; /** * The current value of the select. When `multiple` is enabled, the value will be an array of selected values. */ @property({ converter: { fromAttribute: (value: string) => value.split(' '), toAttribute: (value: string[]) => value.join(' '), }, }) value: string | string[] = ''; /** The default value of the form control. Primarily used for resetting the form control. */ @defaultValue() defaultValue: string | string[] = ''; /** The select's size. */ @property() size: 'small' | 'medium' | 'large' = 'medium'; /** Placeholder text to show as a hint when the select is empty. */ @property() placeholder = 'Select...'; /** Enable automatic resizing of tags area */ @property({ type: Boolean }) autoResize = false; /** Current search value */ @state() searchValue: string = ''; /** Enable search functionality */ @property({ type: Boolean, reflect: true }) searchEnabled = false; @property({ type: Boolean, reflect: true, attribute: true }) enableVisibilityEffect = false; /** Search input placeholder */ @property({attribute:'internal-search-placeholder'}) internalSearchPlaceHolder = 'Search...'; /** Disable local search filtering */ @property({ type: Boolean, reflect: true }) disableLocalSearch = false; /** Show loading state */ @property({ type: Boolean, reflect: true }) optionsLoading = false; /** Show loading state using nile-loader */ @property({ type: Boolean, reflect: true, attribute: true }) loading = false; /** Allows more than one option to be selected. */ @property({ type: Boolean, reflect: true }) multiple = false; /** Help text */ @property({ attribute: 'help-text', reflect: true }) helpText = ''; /** Error message */ @property({ attribute: 'error-message', reflect: true }) errorMessage = ''; /** Sets the input to a warning state */ @property({ type: Boolean }) warning = false; /** Sets the input to an error state */ @property({ type: Boolean }) error = false; /** Sets the input to a success state */ @property({ type: Boolean }) success = false; /** Disables the select control. */ @property({ type: Boolean, reflect: true }) disabled = false; /** Adds a clear button when the select is not empty. */ @property({ type: Boolean, reflect: true }) clearable = false; /** The select's open state. */ @property({ type: Boolean, reflect: true }) open = false; /** * Enable this option to prevent the listbox from being clipped when the component is placed inside a container with * `overflow: auto|scroll`. Hoisting uses a fixed positioning strategy that works in many, but not all, scenarios. */ @property({ type: Boolean }) hoist = false; /** Draws a filled select. */ @property({ type: Boolean, reflect: true }) filled = false; /** Draws a pill-style select with rounded edges. */ @property({ type: Boolean, reflect: true }) pill = false; /** The select's label. If you need to display HTML, use the `label` slot instead. */ @property() label = ''; /** * The preferred placement of the select's menu. Note that the actual placement may vary as needed to keep the listbox * inside of the viewport. */ @property({ reflect: true }) placement: 'top' | 'bottom' = 'bottom'; /** * By default, form controls are associated with the nearest containing `