import { MDCFloatingLabel } from "@material/floating-label";
import { MDCSelectHelperText } from "@material/select/helper-text";
import { h, } from "@stencil/core";
import { isMobileDevice } from "../../util/device";
import { ENTER, SPACE } from "../../util/keycodes";
import { isMultiple } from "../../util/multiple";
import { createRandomString } from "../../util/random-string";
import { SelectTemplate, triggerIconColorWarning } from "./select.template";
/**
 * @exampleComponent limel-example-select-basic
 * @exampleComponent limel-example-select-with-icons
 * @exampleComponent limel-example-select-with-separators
 * @exampleComponent limel-example-select-with-secondary-text
 * @exampleComponent limel-example-select-multiple
 * @exampleComponent limel-example-select-multiple-icons
 * @exampleComponent limel-example-select-with-empty-option
 * @exampleComponent limel-example-select-preselected
 * @exampleComponent limel-example-select-change-options
 * @exampleComponent limel-example-select-dialog
 */
export class Select {
    resetHasChanged() {
        this.hasChanged = false;
    }
    constructor() {
        /**
         * Set to `true` to make the field disabled.
         * and visually shows that the `select` component is editable but disabled.
         * This tells the users that if certain requirements are met,
         * the component may become interactable.
         */
        this.disabled = false;
        /**
         * Set to `true` to make the field read-only.
         * This visualizes the component slightly differently.
         * But shows no visual sign indicating that the component is disabled
         * or can ever become interactable.
         */
        this.readonly = false;
        /**
         * True if the control requires a value.
         */
        this.required = false;
        /**
         * List of options.
         */
        this.options = [];
        /**
         * Set to `true` to allow multiple values to be selected.
         */
        this.multiple = false;
        this.menuOpen = false;
        this.hasChanged = false;
        this.checkValid = false;
        this.handleMenuChange = this.handleMenuChange.bind(this);
        this.handleNativeChange = this.handleNativeChange.bind(this);
        this.handleMenuTriggerKeyPress =
            this.handleMenuTriggerKeyPress.bind(this);
        this.openMenu = this.openMenu.bind(this);
        this.closeMenu = this.closeMenu.bind(this);
        this.portalId = createRandomString();
    }
    connectedCallback() {
        this.initialize();
    }
    componentWillLoad() {
        this.isMobileDevice = isMobileDevice();
        // It should not be possible to render the native select for consumers, but we still want to make it testable.
        // We can set this attribute in tests to force rendering of the native select
        if (Object.hasOwn(this.host.dataset, 'native')) {
            this.isMobileDevice = true;
        }
    }
    componentDidLoad() {
        this.initialize();
        triggerIconColorWarning(this.getOptionsExcludingSeparators());
        this.updatePortalAnchor();
    }
    initialize() {
        let element;
        element = this.host.shadowRoot.querySelector('.mdc-floating-label');
        if (!element) {
            return;
        }
        this.mdcFloatingLabel = new MDCFloatingLabel(element);
        element = this.host.shadowRoot.querySelector('.mdc-select-helper-text');
        if (element) {
            this.mdcSelectHelperText = new MDCSelectHelperText(element);
        }
    }
    disconnectedCallback() {
        this.cancelPendingFocus();
        if (this.mdcFloatingLabel) {
            this.mdcFloatingLabel.destroy();
        }
        if (this.mdcSelectHelperText) {
            this.mdcSelectHelperText.destroy();
        }
    }
    componentDidUpdate() {
        if (this.menuOpen) {
            this.setMenuFocus();
        }
    }
    render() {
        const dropdownZIndex = getComputedStyle(this.host).getPropertyValue('--dropdown-z-index');
        return (h(SelectTemplate, { key: '10e53df1cc7d3e629da0e5b18a96045e4f8e6224', id: this.portalId, disabled: this.disabled || this.readonly, readonly: this.readonly, required: this.required, invalid: this.invalid, label: this.label, helperText: this.helperText, value: this.value, options: this.options, onMenuChange: this.handleMenuChange, onNativeChange: this.handleNativeChange, onTriggerPress: this.handleMenuTriggerKeyPress, multiple: this.multiple, isOpen: this.menuOpen, open: this.openMenu, close: this.closeMenu, checkValid: this.checkValid, native: this.isMobileDevice && !this.multiple, dropdownZIndex: dropdownZIndex, anchor: this.getAnchorElement() }));
    }
    watchOpen(newValue, oldValue) {
        if (this.checkValid) {
            return;
        }
        // Menu was closed for the first time
        if (!newValue && oldValue) {
            this.checkValid = true;
        }
    }
    setMenuFocus() {
        if (this.isMobileDevice) {
            return;
        }
        this.cancelPendingFocus();
        this.focusTimeoutId = setTimeout(() => {
            this.focusTimeoutId = undefined;
            if (!this.menuOpen) {
                return;
            }
            const list = document.querySelector(`#${this.portalId} limel-menu-surface limel-list`);
            if (!list) {
                return;
            }
            this.focusObserver = new IntersectionObserver((entries) => {
                const entry = entries[0];
                if (!(entry === null || entry === void 0 ? void 0 : entry.isIntersecting)) {
                    return;
                }
                this.focusObserver.disconnect();
                this.focusObserver = undefined;
                if (!this.menuOpen) {
                    return;
                }
                this.focusFirstMenuItem(list);
            });
            this.focusObserver.observe(list);
        }, 0);
    }
    cancelPendingFocus() {
        if (this.focusTimeoutId !== undefined) {
            clearTimeout(this.focusTimeoutId);
            this.focusTimeoutId = undefined;
        }
        if (this.focusObserver) {
            this.focusObserver.disconnect();
            this.focusObserver = undefined;
        }
    }
    focusFirstMenuItem(list) {
        var _a;
        const firstItem = (_a = list === null || list === void 0 ? void 0 : list.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('[tabindex]');
        if (firstItem) {
            firstItem.focus({ preventScroll: true });
        }
    }
    setTriggerFocus() {
        const trigger = this.host.shadowRoot.querySelector('.limel-select-trigger');
        trigger.focus();
    }
    // During the first render(), the shadow DOM isn't populated yet, so
    // querySelector('.limel-select-trigger') returns null and we fall back
    // to this.host. componentDidLoad() calls updatePortalAnchor() to
    // imperatively refresh the anchor once the shadow DOM is available.
    getAnchorElement() {
        var _a;
        return ((_a = this.host.shadowRoot.querySelector('.limel-select-trigger')) !== null && _a !== void 0 ? _a : this.host);
    }
    updatePortalAnchor() {
        const portal = this.host.shadowRoot.querySelector('limel-portal');
        if (portal) {
            portal.anchor = this.getAnchorElement();
        }
    }
    handleMenuChange(event) {
        var _a, _b;
        event.stopPropagation();
        if (isMultiple(event.detail)) {
            const selector = `#${this.portalId} limel-menu-surface`;
            const menuSurface = (_b = (_a = document
                .querySelector(selector)) === null || _a === void 0 ? void 0 : _a.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('.mdc-menu-surface');
            const scrollPosition = (menuSurface === null || menuSurface === void 0 ? void 0 : menuSurface.scrollTop) || 0;
            const listItems = event.detail;
            const options = listItems.map((item) => item.value);
            this.change.emit(options);
            // Using a single requestAnimationFrame or setTimeout doesn't
            // work. Using two nested `requestAnimationFrame` worked most of
            // the time, but not always. Using `setTimeout` inside the
            // `requestAnimationFrame` seems to work consistently. /Ads
            requestAnimationFrame(() => {
                setTimeout(() => {
                    menuSurface.scrollTop = scrollPosition;
                });
            });
            return;
        }
        if (!event.detail.selected) {
            return;
        }
        const listItem = event.detail;
        const option = listItem.value;
        if (option.disabled) {
            return;
        }
        this.change.emit(option);
        this.menuOpen = false;
        this.cancelPendingFocus();
        this.setTriggerFocus();
    }
    openMenu() {
        const autoSelectOption = this.getFirstNativeAutoSelectOption();
        if (autoSelectOption) {
            this.hasChanged = true;
            this.change.emit(autoSelectOption);
        }
        this.menuOpen = true;
    }
    getFirstNativeAutoSelectOption() {
        if (this.hasChanged || !this.isMobileDevice || this.multiple) {
            return undefined;
        }
        const options = this.getOptionsExcludingSeparators();
        // Also treat it as "no value" when the current value doesn't match
        // any available option (e.g. an empty option that was filtered out
        // by a required field).
        const currentValue = this.value;
        const hasMatchingValue = currentValue &&
            !Array.isArray(currentValue) &&
            options.some((o) => o.value === currentValue.value);
        if (hasMatchingValue) {
            return undefined;
        }
        if (options.length > 0 && !options[0].value) {
            return undefined;
        }
        return options.find((o) => !o.disabled && o.value);
    }
    closeMenu() {
        this.menuOpen = false;
        this.cancelPendingFocus();
        this.setTriggerFocus();
    }
    handleMenuTriggerKeyPress(event) {
        const isEnter = event.key === ENTER;
        const isSpace = event.key === SPACE;
        if (!this.menuOpen && (isSpace || isEnter)) {
            event.stopPropagation();
            event.preventDefault();
            this.menuOpen = true;
        }
    }
    handleNativeChange(event) {
        event.stopPropagation();
        const element = this.host.shadowRoot.querySelector('select.limel-select__native-control');
        const options = Array.apply(null, element.options) // eslint-disable-line prefer-spread
            .filter((optionElement) => {
            return !!optionElement.selected;
        })
            .map((optionElement) => {
            return this.getOptionsExcludingSeparators().find((o) => o.value === optionElement.value);
        });
        if (this.multiple) {
            this.change.emit(options);
            return;
        }
        this.change.emit(options[0]);
        this.menuOpen = false;
    }
    getOptionsExcludingSeparators() {
        return this.options.filter((option) => !('separator' in option));
    }
    static get is() { return "limel-select"; }
    static get encapsulation() { return "shadow"; }
    static get originalStyleUrls() {
        return {
            "$": ["select.scss"]
        };
    }
    static get styleUrls() {
        return {
            "$": ["select.css"]
        };
    }
    static get properties() {
        return {
            "disabled": {
                "type": "boolean",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Set to `true` to make the field disabled.\nand visually shows that the `select` component is editable but disabled.\nThis tells the users that if certain requirements are met,\nthe component may become interactable."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "attribute": "disabled",
                "defaultValue": "false"
            },
            "readonly": {
                "type": "boolean",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Set to `true` to make the field read-only.\nThis visualizes the component slightly differently.\nBut shows no visual sign indicating that the component is disabled\nor can ever become interactable."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "attribute": "readonly",
                "defaultValue": "false"
            },
            "invalid": {
                "type": "boolean",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Set to `true` to indicate that the current value of the select is\ninvalid."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "attribute": "invalid"
            },
            "required": {
                "type": "boolean",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "True if the control requires a value."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "attribute": "required",
                "defaultValue": "false"
            },
            "label": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Text to display next to the select."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "attribute": "label"
            },
            "helperText": {
                "type": "string",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Optional helper text to display below the input field when it has focus."
                },
                "getter": false,
                "setter": false,
                "reflect": true,
                "attribute": "helper-text"
            },
            "value": {
                "type": "unknown",
                "mutable": false,
                "complexType": {
                    "original": "Option | Option[]",
                    "resolved": "Option<string> | Option<string>[]",
                    "references": {
                        "Option": {
                            "location": "import",
                            "path": "../select/option.types",
                            "id": "src/components/select/option.types.ts::Option",
                            "referenceLocation": "Option"
                        }
                    }
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Currently selected value or values.\nIf `multiple` is `true`, this must be an array. Otherwise it must be a\nsingle value."
                },
                "getter": false,
                "setter": false
            },
            "options": {
                "type": "unknown",
                "mutable": false,
                "complexType": {
                    "original": "Array<Option | ListSeparator>",
                    "resolved": "(ListSeparator | Option<string>)[]",
                    "references": {
                        "Array": {
                            "location": "global",
                            "id": "global::Array"
                        },
                        "Option": {
                            "location": "import",
                            "path": "../select/option.types",
                            "id": "src/components/select/option.types.ts::Option",
                            "referenceLocation": "Option"
                        },
                        "ListSeparator": {
                            "location": "import",
                            "path": "../list-item/list-item.types",
                            "id": "src/components/list-item/list-item.types.ts::ListSeparator",
                            "referenceLocation": "ListSeparator"
                        }
                    }
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "List of options."
                },
                "getter": false,
                "setter": false,
                "defaultValue": "[]"
            },
            "multiple": {
                "type": "boolean",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "Set to `true` to allow multiple values to be selected."
                },
                "getter": false,
                "setter": false,
                "reflect": false,
                "attribute": "multiple",
                "defaultValue": "false"
            }
        };
    }
    static get states() {
        return {
            "menuOpen": {}
        };
    }
    static get events() {
        return [{
                "method": "change",
                "name": "change",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted when the value is changed."
                },
                "complexType": {
                    "original": "Option | Option[]",
                    "resolved": "Option<string> | Option<string>[]",
                    "references": {
                        "Option": {
                            "location": "import",
                            "path": "../select/option.types",
                            "id": "src/components/select/option.types.ts::Option",
                            "referenceLocation": "Option"
                        }
                    }
                }
            }];
    }
    static get elementRef() { return "host"; }
    static get watchers() {
        return [{
                "propName": "value",
                "methodName": "resetHasChanged"
            }, {
                "propName": "options",
                "methodName": "resetHasChanged"
            }, {
                "propName": "menuOpen",
                "methodName": "watchOpen"
            }];
    }
}
