import { __decorate } from "tslib";
// Based on: https://github.com/microsoft/fast/blob/%40microsoft/fast-foundation_v2.49.5/packages/web-components/fast-foundation/src/select/select.ts
import { attr, html, observable, Observable, volatile } from '@ni/fast-element';
import { AnchoredRegion, DesignSystem, Select as FoundationSelect, ListboxOption, SelectPosition, applyMixins, StartEnd, DelegatesARIASelect } from '@ni/fast-foundation';
import { findLastIndex, keyArrowDown, keyArrowUp, keyEnd, keyEnter, keyEscape, keyHome, keySpace, uniqueId } from '@ni/fast-web-utilities';
import { arrowExpanderDown16X16 } from '@ni/nimble-tokens/dist/icons/js';
import { styles } from './styles';
import { DropdownAppearance } from '../patterns/dropdown/types';
import { errorTextTemplate } from '../patterns/error/template';
import { mixinErrorPattern } from '../patterns/error/types';
import { iconExclamationMarkTag } from '../icons/exclamation-mark';
import { isListOption, isListOptionGroup, template } from './template';
import { FilterMode } from './types';
import { diacriticInsensitiveStringNormalizer } from '../utilities/models/string-normalizers';
import { FormAssociatedSelect } from './models/select-form-associated';
import { slotTextContent } from '../utilities/models/slot-text-content';
import { mixinRequiredVisiblePattern } from '../patterns/required-visible/types';
const isOptionSelectable = (el) => {
    return !el.visuallyHidden && !el.disabled && !el.hidden;
};
const isOptionPlaceholder = (el) => {
    return el.disabled && el.hidden;
};
const isOptionOrGroupVisible = (el) => {
    return !el.visuallyHidden && !el.hidden;
};
/**
 * A nimble-styled HTML select.
 */
export class Select extends mixinErrorPattern(mixinRequiredVisiblePattern(FormAssociatedSelect)) {
    constructor() {
        super(...arguments);
        this.appearance = DropdownAppearance.underline;
        this.appearanceReadOnly = false;
        this.fullBleed = false;
        this.filterMode = FilterMode.none;
        this.clearable = false;
        this.loadingVisible = false;
        /**
         * @internal
         */
        this.displayPlaceholder = false;
        /**
         * @internal
         */
        this.open = false;
        /**
         * The unique id for the internal listbox element.
         *
         * @internal
         */
        this.listboxId = uniqueId('listbox-');
        /** @internal */
        this.hasOverflow = false;
        /**
         * @internal
         */
        this.filteredOptions = [];
        /**
         * @internal
         */
        this.filter = '';
        /**
         * The space available in the viewport for the listbox when opened.
         *
         * @internal
         */
        this.availableViewportHeight = 0;
        this._value = '';
        this.forcedPosition = false;
        this.selectedOptionObserver = new MutationObserver(() => {
            this.updateDisplayValue();
        });
    }
    /**
     * The component is collapsible when in single-selection mode with no size attribute.
     *
     * @internal
     */
    get collapsible() {
        return !(this.multiple || typeof this.size === 'number');
    }
    /** @internal */
    get labelContent() {
        if (!this.$fastController.isConnected) {
            return '';
        }
        return slotTextContent(this.labelSlot);
    }
    /**
     * @internal
     */
    connectedCallback() {
        super.connectedCallback();
        this.forcedPosition = !!this.positionAttribute;
        if (this.open) {
            this.initializeOpenState();
        }
        this.observeSelectedOptionTextContent();
    }
    disconnectedCallback() {
        super.disconnectedCallback();
        this.selectedOptionObserver?.disconnect();
    }
    get value() {
        Observable.track(this, 'value');
        return this._value;
    }
    set value(next) {
        const prev = this._value;
        let newValue = next;
        if (this.options !== undefined && this.options.length > 0) {
            const newValueIndex = this.options.findIndex(el => el.value === newValue);
            const prevSelectedValue = this.options[this.selectedIndex]?.value ?? null;
            const nextSelectedValue = this.options[newValueIndex]?.value ?? null;
            if (newValueIndex === -1
                || prevSelectedValue !== nextSelectedValue) {
                newValue = '';
                this.selectedIndex = newValueIndex;
            }
            newValue = this.firstSelectedOption?.value ?? newValue;
        }
        if (prev !== newValue) {
            this._value = newValue;
            super.valueChanged(prev, newValue);
            Observable.notify(this, 'value');
            this.updateDisplayValue();
        }
    }
    /**
     * @internal
     */
    get displayValue() {
        Observable.track(this, 'displayValue');
        return this.firstSelectedOption?.text ?? '';
    }
    /**
     * @internal
     */
    anchoredRegionChanged(_prev, _next) {
        if (this.anchoredRegion !== undefined && this.control !== undefined) {
            this.anchoredRegion.anchorElement = this.control;
        }
    }
    /**
     * @internal
     */
    controlChanged(_prev, _next) {
        if (this.anchoredRegion !== undefined && this.control !== undefined) {
            this.anchoredRegion.anchorElement = this.control;
        }
    }
    /**
     * @internal
     */
    slottedOptionsChanged(prev, next) {
        const value = this.value;
        this.options.forEach(o => {
            const notifier = Observable.getNotifier(o);
            notifier.unsubscribe(this, 'value');
            notifier.unsubscribe(this, 'hidden');
            notifier.unsubscribe(this, 'disabled');
        });
        prev?.filter(isListOptionGroup).forEach(el => {
            const notifier = Observable.getNotifier(el);
            notifier.unsubscribe(this, 'hidden');
            notifier.unsubscribe(this, 'visuallyHidden');
            notifier.unsubscribe(this, 'listOptions');
        });
        const options = this.getSlottedOptions(next);
        super.slottedOptionsChanged(prev, options);
        options.forEach(o => {
            const notifier = Observable.getNotifier(o);
            notifier.subscribe(this, 'value');
            notifier.subscribe(this, 'hidden');
            notifier.subscribe(this, 'disabled');
        });
        next?.filter(isListOptionGroup).forEach(el => {
            this.updateAdjacentSeparatorState(el);
            const notifier = Observable.getNotifier(el);
            notifier.subscribe(this, 'hidden');
            notifier.subscribe(this, 'visuallyHidden');
            notifier.subscribe(this, 'listOptions');
        });
        this.setProxyOptions();
        this.updateValue();
        // We need to force an update to the filteredOptions observable
        // (by calling 'filterOptions()) so that the template correctly updates.
        this.filterOptions();
        if (value) {
            this.value = value;
        }
    }
    /**
     * @internal
     */
    clickHandler(e) {
        // do nothing if the select is disabled
        if (this.disabled) {
            return;
        }
        let optionClicked = false;
        if (this.open) {
            const captured = e.target.closest('option,[role=option]');
            optionClicked = captured !== null;
            if (captured?.disabled) {
                return;
            }
        }
        const previousSelectedIndex = this.selectedIndex;
        super.clickHandler(e);
        if (this.open
            && this.selectedIndex !== previousSelectedIndex
            && optionClicked) {
            this.updateValue(true);
        }
        this.open = this.collapsible && !this.open;
    }
    /**
     * Updates the value when an option's value changes.
     *
     * @param source - the source object
     * @param propertyName - the property to evaluate
     *
     * @internal
     * @override
     */
    handleChange(source, propertyName) {
        // don't call super.handleChange so hidden options can be selected programmatically
        const sourceElement = source;
        switch (propertyName) {
            case 'value': {
                this.updateValue();
                break;
            }
            case 'selected': {
                if (isListOption(sourceElement) && sourceElement.selected) {
                    this.selectedIndex = this.options.indexOf(sourceElement);
                }
                else {
                    this.clearSelect();
                }
                break;
            }
            case 'hidden': {
                if (isListOption(sourceElement)) {
                    sourceElement.visuallyHidden = sourceElement.hidden;
                    this.updateAdjacentSeparatorState(sourceElement);
                }
                else if (isListOptionGroup(sourceElement)) {
                    sourceElement.listOptions.forEach(e => {
                        e.visuallyHidden = sourceElement.hidden;
                    });
                    this.updateAdjacentSeparatorState(sourceElement);
                }
                this.filterOptions();
                this.updateDisplayValue();
                break;
            }
            case 'visuallyHidden': {
                if (isListOptionGroup(sourceElement)
                    || isListOption(sourceElement)) {
                    this.updateAdjacentSeparatorState(sourceElement);
                }
                break;
            }
            case 'disabled': {
                this.updateDisplayValue();
                break;
            }
            case 'listOptions': {
                // force refresh of slotted options for groups
                this.slottedOptionsChanged(this.slottedOptions, this.slottedOptions);
                break;
            }
            default:
                break;
        }
    }
    /**
     * Prevents focus when size is set and a scrollbar is clicked.
     *
     * @param e - the mouse event object
     *
     * @override
     * @internal
     */
    mousedownHandler(e) {
        if (e.offsetX >= 0 && e.offsetX <= this.listbox?.scrollWidth) {
            return super.mousedownHandler(e);
        }
        return this.collapsible;
    }
    /**
     * @internal
     */
    regionLoadedHandler() {
        this.focusAndScrollOptionIntoView();
    }
    /**
     * Sets the multiple property on the proxy element.
     *
     * @param prev - the previous multiple value
     * @param next - the current multiple value
     */
    multipleChanged(prev, next) {
        super.multipleChanged(prev, next);
        this.proxy.multiple = next;
    }
    /**
     * @internal
     */
    ignoreClickHandler(e) {
        e.stopPropagation();
    }
    /**
     * @internal
     */
    clearClickHandler(e) {
        this.open = false;
        this.clearSelect();
        this.updateValue(true);
        e.stopPropagation();
    }
    /**
     * @internal
     */
    updateDisplayValue() {
        const placeholderOption = this.getPlaceholderOption();
        if (placeholderOption
            && this.firstSelectedOption === placeholderOption) {
            this.displayPlaceholder = true;
        }
        else {
            this.displayPlaceholder = false;
        }
        Observable.notify(this, 'displayValue');
    }
    /**
     * Handle content changes on the control input.
     *
     * @param e - the input event
     * @internal
     */
    inputHandler(e) {
        this.filter = this.filterInput?.value ?? '';
        if (this.filterMode === FilterMode.standard) {
            this.filterOptions();
            const enabledOptions = this.filteredOptions.filter(o => !o.disabled);
            let activeOptionIndex = this.filter !== ''
                ? (this.openActiveIndex ?? this.selectedIndex)
                : this.selectedIndex;
            if (enabledOptions.length > 0
                && !enabledOptions.find(o => o === this.options[activeOptionIndex])) {
                activeOptionIndex = this.options.indexOf(enabledOptions[0]);
            }
            else if (enabledOptions.length === 0) {
                activeOptionIndex = -1;
            }
            this.setActiveOption(activeOptionIndex);
        }
        if (this.filterMode !== FilterMode.none) {
            this.emitFilterInputEvent();
        }
        if (e.inputType.includes('deleteContent') || this.filter.length === 0) {
            return true;
        }
        e.stopPropagation();
        return true;
    }
    /**
     * @internal
     */
    focusoutHandler(e) {
        super.focusoutHandler(e);
        if (!this.open) {
            return true;
        }
        this.open = false;
        const focusTarget = e.relatedTarget;
        if (this.isSameNode(focusTarget)) {
            this.focus();
            return true;
        }
        return true;
    }
    /**
     * @internal
     */
    keydownHandler(e) {
        const initialSelectedIndex = this.selectedIndex;
        const key = e.key;
        if (e.ctrlKey || e.shiftKey) {
            return true;
        }
        if (key !== keyArrowDown && key !== keyArrowUp) {
            super.keydownHandler(e);
        }
        let currentActiveIndex = this.openActiveIndex ?? this.selectedIndex;
        let commitValueThenClose = false;
        switch (key) {
            case keyArrowDown: {
                const selectedOption = this.options[this.selectedIndex];
                if (this.open
                    && isListOption(selectedOption)
                    && !isOptionOrGroupVisible(selectedOption)) {
                    if (this.openActiveIndex === this.selectedIndex) {
                        this.selectFirstOption();
                    }
                    else {
                        this.selectNextOption();
                    }
                }
                else {
                    super.keydownHandler(e);
                }
                // update currentActiveIndex again as dependent state may have changed
                currentActiveIndex = this.openActiveIndex ?? this.selectedIndex;
                break;
            }
            case keyArrowUp: {
                const selectedOption = this.options[this.selectedIndex];
                if (this.open
                    && isListOption(selectedOption)
                    && !isOptionOrGroupVisible(selectedOption)) {
                    if (this.openActiveIndex === this.selectedIndex) {
                        this.selectLastOption();
                    }
                    else {
                        this.selectPreviousOption();
                    }
                }
                else {
                    super.keydownHandler(e);
                }
                // update currentActiveIndex again as dependent state may have changed
                currentActiveIndex = this.openActiveIndex ?? this.selectedIndex;
                break;
            }
            case keySpace: {
                // when dropdown is open allow user to enter a space for filter text
                if (this.open && this.filterMode !== FilterMode.none) {
                    break;
                }
                e.preventDefault();
                if (this.collapsible && this.typeAheadExpired) {
                    this.open = !this.open;
                }
                if (!this.open) {
                    this.focus();
                }
                break;
            }
            case keyHome:
            case keyEnd: {
                e.preventDefault();
                break;
            }
            case keyEnter: {
                e.preventDefault();
                if (this.filteredOptions.length === 0
                    || this.filteredOptions.every(o => o.disabled)) {
                    return false;
                }
                if (this.open) {
                    commitValueThenClose = true;
                }
                else {
                    this.open = true;
                }
                if (commitValueThenClose) {
                    this.focus();
                }
                break;
            }
            case keyEscape: {
                if (!this.open) {
                    if (this.clearable) {
                        this.clearSelect();
                        this.updateValue(true);
                        return true;
                    }
                    break;
                }
                if (this.collapsible && this.open) {
                    e.preventDefault();
                    this.open = false;
                }
                currentActiveIndex = this.selectedIndex;
                this.focus();
                break;
            }
            default: {
                break;
            }
        }
        if (!this.open || commitValueThenClose) {
            if (this.selectedIndex !== currentActiveIndex) {
                this.selectedIndex = currentActiveIndex;
            }
            if (initialSelectedIndex !== this.selectedIndex) {
                this.updateValue(true);
            }
        }
        if (commitValueThenClose) {
            this.open = false;
        }
        return !(key === keyArrowDown || key === keyArrowUp);
    }
    /**
     * Updates the proxy value when the selected index changes.
     *
     * @param prev - the previous selected index
     * @param next - the next selected index
     *
     * @internal
     */
    selectedIndexChanged(_, __) {
        // Don't call super.selectedIndexChanged as this will disallow disabled options
        // from being valid initial selected values. Our setDefaultSelectedOption
        // implementation handles skipping non-selected disabled options for the initial
        // selected value.
        this.setSelectedOptions();
        if (this.open) {
            this.setActiveOption(this.selectedIndex);
        }
        this.updateValue();
    }
    /**
     * @internal
     * Fork of Listbox implementation, so that the selectedIndex is not changed while the dropdown
     * is open.
     */
    typeaheadBufferChanged(_, __) {
        if (this.$fastController.isConnected) {
            const typeaheadMatches = this.getTypeaheadMatches();
            if (typeaheadMatches.length > 0) {
                const activeOptionIndex = this.options.indexOf(typeaheadMatches[0]);
                if (!(this.open && this.filterMode !== FilterMode.none)) {
                    this.setActiveOption(activeOptionIndex);
                }
            }
            this.typeaheadExpired = false;
        }
    }
    /**
     * Synchronize the `aria-disabled` property when the `disabled` property changes.
     *
     * @param prev - The previous disabled value
     * @param next - The next disabled value
     *
     * @internal
     */
    disabledChanged(prev, next) {
        if (super.disabledChanged) {
            super.disabledChanged(prev, next);
        }
        this.ariaDisabled = this.disabled ? 'true' : 'false';
    }
    /**
     * Reset the element to its first selectable option when its parent form is reset.
     *
     * @internal
     */
    formResetCallback() {
        this.setProxyOptions();
        // Call the base class's implementation setDefaultSelectedOption instead of the select's
        // override, in order to reset the selectedIndex without using the value property.
        super.setDefaultSelectedOption();
        if (this.selectedIndex === -1) {
            this.selectedIndex = 0;
        }
    }
    /**
     * @internal
     */
    selectNextOption() {
        // don't call super.selectNextOption as that relies on side-effecty
        // behavior to not select disabled option (which no longer works)
        const startIndex = this.openActiveIndex ?? this.selectedIndex;
        for (let i = startIndex + 1; i < this.options.length; i++) {
            const listOption = this.options[i];
            if (isListOption(listOption) && isOptionSelectable(listOption)) {
                this.setActiveOption(i);
                break;
            }
        }
    }
    /**
     * @internal
     */
    selectPreviousOption() {
        // don't call super.selectPreviousOption as that relies on side-effecty
        // behavior to not select disabled option (which no longer works)
        const startIndex = this.openActiveIndex ?? this.selectedIndex;
        for (let i = startIndex - 1; i >= 0; i--) {
            const listOption = this.options[i];
            if (isListOption(listOption) && isOptionSelectable(listOption)) {
                this.setActiveOption(i);
                break;
            }
        }
    }
    /**
     * @internal
     */
    selectFirstOption() {
        const newActiveOptionIndex = this.options.findIndex(o => isListOption(o) && isOptionSelectable(o));
        this.setActiveOption(newActiveOptionIndex);
    }
    /**
     * @internal
     */
    selectLastOption() {
        const newActiveOptionIndex = findLastIndex(this.options, o => isListOption(o) && isOptionSelectable(o));
        this.setActiveOption(newActiveOptionIndex);
    }
    /**
     * @internal
     */
    registerOption(option) {
        if (this.options.includes(option)) {
            return;
        }
        // Adding an option to the end, ultimately, isn't the correct
        // thing to do, as this will mean the option's index in the options,
        // at least temporarily, does not match the DOM order. However, it
        // is expected that a successive run of `slottedOptionsChanged` will
        // correct this order issue. See 'https://github.com/ni/nimble/issues/1915'
        // for more info.
        this.options.push(option);
    }
    setSelectedOptions() {
        // Prevents parent classes from resetting selectedIndex to a positive
        // value while filtering, which can result in a disabled option being
        // selected.
        if (this.open && this.selectedIndex === -1) {
            return;
        }
        super.setSelectedOptions();
    }
    focusAndScrollOptionIntoView() {
        if (this.open) {
            super.focusAndScrollOptionIntoView();
            window.requestAnimationFrame(() => {
                this.filterInput?.focus();
            });
        }
    }
    getTypeaheadMatches() {
        const matches = super.getTypeaheadMatches();
        // Don't allow placeholder to be matched
        return matches.filter(o => !o.hidden && !o.disabled);
    }
    positionChanged(_, next) {
        this.positionAttribute = next;
        this.setPositioning();
    }
    /**
     * Updates the proxy's size property when the size attribute changes.
     *
     * @param prev - the previous size
     * @param next - the current size
     *
     * @override
     * @internal
     */
    sizeChanged(prev, next) {
        super.sizeChanged(prev, next);
        this.proxy.size = next;
    }
    openChanged() {
        if (!this.collapsible) {
            return;
        }
        if (this.open) {
            this.initializeOpenState();
            return;
        }
        const activeOption = this.options[this.openActiveIndex ?? this.selectedIndex];
        if (isListOption(activeOption)) {
            activeOption.activeOption = false;
        }
        this.openActiveIndex = undefined;
        this.filter = '';
        if (this.filterInput) {
            this.filterInput.value = '';
        }
        if (this.filterMode !== FilterMode.none) {
            this.emitFilterInputEvent();
        }
        this.ariaControls = '';
        this.ariaExpanded = 'false';
    }
    /**
     * Updates the selectedness of each option when the list of selected options changes.
     *
     * @param prev - the previous list of selected options
     * @param next - the current list of selected options
     *
     * @override
     * @internal
     */
    selectedOptionsChanged(_prev, next) {
        // don't call super.selectedOptionsChanged so we don't filter out hidden elements
        // when updating 'selected' state (copied relevant super implementation)
        this.options?.forEach((o, i) => {
            const notifier = Observable.getNotifier(o);
            notifier.unsubscribe(this, 'selected');
            o.selected = next.includes(o);
            notifier.subscribe(this, 'selected');
            const proxyOption = this.proxy?.options.item(i);
            if (proxyOption) {
                proxyOption.selected = o.selected;
            }
        });
    }
    /**
     * Sets the selected index to match the first option with the selected attribute, or
     * the first selectable option.
     *
     * @override
     * @internal
     */
    setDefaultSelectedOption() {
        const options = this.options
            ?? Array.from(this.children).filter(o => isListOption(o));
        const optionIsSelected = (option) => {
            return option.hasAttribute('selected') || option.selected;
        };
        let selectedIndex = -1;
        let firstValidOptionIndex = -1;
        let placeholderIndex = -1;
        for (let i = 0; i < options?.length; i++) {
            const option = options[i];
            if (optionIsSelected(option) || option.value === this.value) {
                selectedIndex = i;
                break;
            }
            else if (placeholderIndex === -1 && isOptionPlaceholder(option)) {
                placeholderIndex = i;
            }
            else if (firstValidOptionIndex === -1
                && isOptionSelectable(option)) {
                firstValidOptionIndex = i;
            }
        }
        if (selectedIndex !== -1) {
            this.selectedIndex = selectedIndex;
        }
        else if (placeholderIndex !== -1) {
            this.selectedIndex = placeholderIndex;
        }
        else if (firstValidOptionIndex !== -1) {
            this.selectedIndex = firstValidOptionIndex;
        }
        else {
            this.selectedIndex = 0;
        }
    }
    getSlottedOptions(slottedElements) {
        const options = [];
        slottedElements?.forEach(el => {
            if (isListOption(el)) {
                options.push(el);
            }
            else if (isListOptionGroup(el)) {
                options.push(...this.getGroupOptions(el));
            }
        });
        return options;
    }
    setActiveOption(newActiveIndex) {
        const activeOption = this.options[newActiveIndex];
        if (this.open) {
            if (isListOption(activeOption)) {
                activeOption.activeOption = true;
            }
            const previousActiveIndex = this.openActiveIndex ?? this.selectedIndex;
            const previousActiveOption = this.options[previousActiveIndex];
            if (previousActiveIndex !== newActiveIndex
                && isListOption(previousActiveOption)) {
                previousActiveOption.activeOption = false;
            }
            this.openActiveIndex = newActiveIndex;
            this.focusAndScrollActiveOptionIntoView();
        }
        else {
            this.selectedIndex = newActiveIndex;
        }
        this.ariaActiveDescendant = activeOption?.id ?? '';
    }
    focusAndScrollActiveOptionIntoView() {
        const optionToFocus = this.options[this.openActiveIndex ?? this.selectedIndex];
        // Copied from FAST: To ensure that the browser handles both `focus()` and
        // `scrollIntoView()`, the timing here needs to guarantee that they happen on
        // different frames. Since this function is typically called from the `openChanged`
        // observer, `DOM.queueUpdate` causes the calls to be grouped into the same frame.
        // To prevent this, `requestAnimationFrame` is used instead of `DOM.queueUpdate`.
        if (optionToFocus !== undefined && this.contains(optionToFocus)) {
            optionToFocus.focus();
            requestAnimationFrame(() => {
                optionToFocus.scrollIntoView({ block: 'nearest' });
            });
        }
    }
    getPlaceholderOption() {
        return this.options.find(o => o.hidden && o.disabled);
    }
    setPositioning() {
        if (!this.$fastController.isConnected) {
            // Don't call setPositioning() until we're connected,
            // since this.forcedPosition isn't initialized yet.
            return;
        }
        const currentBox = this.getBoundingClientRect();
        const viewportHeight = window.innerHeight;
        const availableBottom = viewportHeight - currentBox.bottom;
        if (this.forcedPosition) {
            this.position = this.positionAttribute;
        }
        else if (currentBox.top > availableBottom) {
            this.position = SelectPosition.above;
        }
        else {
            this.position = SelectPosition.below;
        }
        this.positionAttribute = this.forcedPosition
            ? this.positionAttribute
            : this.position;
        this.availableViewportHeight = this.position === SelectPosition.above
            ? Math.trunc(currentBox.top)
            : Math.trunc(availableBottom);
    }
    updateAdjacentSeparatorState(element) {
        const previousElement = this.getPreviousVisibleOptionOrGroup(element);
        const nextElement = this.getNextVisibleOptionOrGroup(element);
        if (isOptionOrGroupVisible(element)) {
            const topSeparatorVisible = isListOption(previousElement);
            this.setTopSeparatorState(element, topSeparatorVisible);
            const bottomSeparatorVisible = nextElement !== null;
            this.setBottomSeparatorState(element, bottomSeparatorVisible);
            this.setBottomSeparatorState(previousElement, true);
        }
        else {
            const nextTopSeparatorVisible = isListOption(previousElement);
            this.setTopSeparatorState(nextElement, nextTopSeparatorVisible);
            const previousBottomSeparatorVisible = nextElement !== null;
            this.setBottomSeparatorState(previousElement, previousBottomSeparatorVisible);
        }
    }
    setTopSeparatorState(element, visible) {
        if (isListOptionGroup(element)) {
            element.topSeparatorVisible = visible;
        }
    }
    setBottomSeparatorState(element, visible) {
        if (isListOptionGroup(element)) {
            element.bottomSeparatorVisible = visible;
        }
    }
    getPreviousVisibleOptionOrGroup(element) {
        let previousElement = element.previousElementSibling;
        while (previousElement) {
            if ((isListOption(previousElement)
                || isListOptionGroup(previousElement))
                && isOptionOrGroupVisible(previousElement)) {
                return previousElement;
            }
            previousElement = previousElement.previousElementSibling;
        }
        return null;
    }
    getNextVisibleOptionOrGroup(element) {
        let nextElement = element.nextElementSibling;
        while (nextElement) {
            if ((isListOption(nextElement) || isListOptionGroup(nextElement))
                && isOptionOrGroupVisible(nextElement)) {
                return nextElement;
            }
            nextElement = nextElement.nextElementSibling;
        }
        return null;
    }
    isOptionHiddenOrFilteredOut(option) {
        if (option.hidden) {
            return true;
        }
        return this.filterMode === FilterMode.standard
            ? !this.filterMatchesText(option.text)
            : false;
    }
    filterMatchesText(text) {
        const filter = this.filter.toLowerCase();
        const normalizedFilter = diacriticInsensitiveStringNormalizer(filter);
        return diacriticInsensitiveStringNormalizer(text).includes(normalizedFilter);
    }
    /**
     * Filter available options by text value.
     *
     * @public
     */
    filterOptions() {
        if (!this.$fastController.isConnected) {
            return;
        }
        const filteredOptions = [];
        for (const element of this.slottedOptions) {
            if (isListOptionGroup(element)) {
                if (element.hidden) {
                    continue; // no need to process hidden groups
                }
                const groupOptions = this.getGroupOptions(element);
                const groupMatchesFilter = this.filterMatchesText(element.labelContent);
                groupOptions.forEach(option => {
                    option.visuallyHidden = groupMatchesFilter
                        ? false
                        : this.isOptionHiddenOrFilteredOut(option);
                    if (!option.visuallyHidden) {
                        filteredOptions.push(option);
                    }
                });
            }
            else if (isListOption(element)) {
                element.visuallyHidden = this.isOptionHiddenOrFilteredOut(element);
                if (!element.visuallyHidden) {
                    filteredOptions.push(element);
                }
            }
        }
        this.filteredOptions = filteredOptions;
    }
    getGroupOptions(group) {
        return Array.from(group.children)
            .filter(el => isListOption(el))
            .map(el => {
            if (group.hidden && isListOption(el)) {
                el.visuallyHidden = true;
            }
            return el;
        });
    }
    /**
     * Sets the value and display value to match the first selected option.
     *
     * @param shouldEmit - if true, the input and change events will be emitted
     *
     * @internal
     */
    updateValue(shouldEmit) {
        if (this.$fastController.isConnected) {
            this.value = this.firstSelectedOption?.value ?? '';
            this.observeSelectedOptionTextContent();
        }
        if (shouldEmit) {
            this.$emit('input');
            this.$emit('change', this, {
                bubbles: true,
                composed: undefined
            });
        }
    }
    clearSelect() {
        const placeholder = this.getPlaceholderOption();
        this.selectedIndex = placeholder
            ? this.options.indexOf(placeholder)
            : -1;
    }
    /**
     * Resets and fills the proxy to match the component's options.
     *
     * @internal
     */
    setProxyOptions() {
        if (this.proxy instanceof HTMLSelectElement && this.options !== null) {
            this.proxy.options.length = 0;
            this.options.forEach(option => {
                const proxyOption = option.proxy
                    ?? (option instanceof HTMLOptionElement
                        ? option.cloneNode()
                        : null);
                if (proxyOption !== null) {
                    this.proxy.options.add(proxyOption);
                }
            });
        }
    }
    filterChanged() {
        this.filterOptions();
    }
    emitFilterInputEvent() {
        const eventDetail = {
            filterText: this.filter
        };
        this.$emit('filter-input', eventDetail, { bubbles: true });
    }
    initializeOpenState() {
        this.setActiveOption(this.selectedIndex);
        this.ariaControls = this.listboxId;
        this.ariaExpanded = 'true';
        this.setPositioning();
        this.focusAndScrollOptionIntoView();
    }
    observeSelectedOptionTextContent() {
        this.selectedOptionObserver?.disconnect();
        if (this.selectedIndex === -1) {
            return;
        }
        const selectedOption = this.firstSelectedOption;
        if (selectedOption !== null) {
            this.selectedOptionObserver?.observe(selectedOption, {
                characterData: true,
                subtree: true,
                childList: true
            });
        }
    }
}
__decorate([
    attr
], Select.prototype, "appearance", void 0);
__decorate([
    attr({ attribute: 'appearance-readonly', mode: 'boolean' })
], Select.prototype, "appearanceReadOnly", void 0);
__decorate([
    attr({ attribute: 'full-bleed', mode: 'boolean' })
], Select.prototype, "fullBleed", void 0);
__decorate([
    attr({ attribute: 'position' })
], Select.prototype, "positionAttribute", void 0);
__decorate([
    attr({ attribute: 'filter-mode' })
], Select.prototype, "filterMode", void 0);
__decorate([
    attr({ attribute: 'clearable', mode: 'boolean' })
], Select.prototype, "clearable", void 0);
__decorate([
    attr({ attribute: 'loading-visible', mode: 'boolean' })
], Select.prototype, "loadingVisible", void 0);
__decorate([
    observable
], Select.prototype, "displayPlaceholder", void 0);
__decorate([
    attr({ attribute: 'open', mode: 'boolean' })
], Select.prototype, "open", void 0);
__decorate([
    observable
], Select.prototype, "position", void 0);
__decorate([
    observable
], Select.prototype, "control", void 0);
__decorate([
    observable
], Select.prototype, "scrollableRegion", void 0);
__decorate([
    observable
], Select.prototype, "filterInput", void 0);
__decorate([
    observable
], Select.prototype, "anchoredRegion", void 0);
__decorate([
    observable
], Select.prototype, "hasOverflow", void 0);
__decorate([
    observable
], Select.prototype, "filteredOptions", void 0);
__decorate([
    observable
], Select.prototype, "filter", void 0);
__decorate([
    observable
], Select.prototype, "availableViewportHeight", void 0);
__decorate([
    volatile
], Select.prototype, "collapsible", null);
__decorate([
    volatile
], Select.prototype, "labelContent", null);
__decorate([
    volatile
], Select.prototype, "displayValue", null);
const nimbleSelect = Select.compose({
    baseName: 'select',
    baseClass: FoundationSelect,
    template,
    styles,
    indicator: arrowExpanderDown16X16.data,
    end: html `
        <${iconExclamationMarkTag}
            severity="error"
            class="error-icon"
        ></${iconExclamationMarkTag}>
        ${errorTextTemplate}
    `
});
applyMixins(Select, StartEnd, DelegatesARIASelect);
DesignSystem.getOrCreate().withPrefix('nimble').register(nimbleSelect());
export const selectTag = 'nimble-select';
//# sourceMappingURL=index.js.map