/*!
 * Custom elements for Nanopore-Digital Web applications
 */
import { Host, } from "@stencil/core";
import { h, transformTag } from "../../utils/renderer";
import { getActiveElement } from "../../utils/active-element";
import { getDirectChildren } from "../../utils/dom";
import { getTextContent } from "../../utils/slot";
import { getTabbableElements } from "../../utils/tabbable";
/**
 * Menus provide a list of options for the user to choose from.
 *
 * @slot - The menu's content; `nano-option`, `nano-nav-item`, `a`, `nano-content-links` elements as well as `hr` and more.
 *
 * @part base - The base element of the menu.
 *
 * @version 1.0.0
 * @status stable
 */
export class Menu {
    ignoreMouseEvents = false;
    ignoreMouseTimeout;
    menu;
    typeToSelect = '';
    typeToSelectTimeout;
    el;
    // Public API
    /** get the focus state of the menu @readonly */
    get hasFocus() {
        return this._hasFocus;
    }
    _hasFocus = false;
    /** changes the role of the underlying control (required for accessibility semantics).
     * By default this will be set via the frequency of element type (e.g. `<button>` vs `<a>`) */
    type;
    // we'll use this internally if it's not set externally
    _type;
    /** an accessible label */
    label;
    // Events
    /** Emitted when the menu gains focus. */
    nanoFocus;
    /** Emitted when the menu loses focus. */
    nanoBlur;
    /** Emitted when a menu item is selected. */
    nanoSelect;
    // Public Methods
    /** Sets focus on the menu. */
    async setFocus() {
        if (this.menu?.focus) {
            this.menu.focus({ preventScroll: true });
        }
    }
    /** Removes focus from the menu. */
    async removeFocus() {
        if (this.menu?.blur)
            this.menu.blur();
    }
    /** Sets the current active item */
    async showActiveElement() {
        if (this.selectedItem)
            this.selectedItem.scrollIntoView({ block: 'nearest' });
    }
    /** Removes any active item's state */
    async resetActiveItem() {
        this.getItems
            .filter((i) => i.tagName.toLowerCase() === transformTag('nano-option'))
            .map((i) => i.setAttribute('tabindex', '-1'));
    }
    // Private methods
    get getItems() {
        let items;
        if (this._type === 'listbox') {
            items = getDirectChildren(this.el, `${transformTag('nano-nav-item')}, ${transformTag('nano-option')}`, 'slot');
        }
        else {
            const opened = this.el.querySelectorAll('[open]');
            items = getTabbableElements(opened[opened.length - 1] || this.el);
        }
        return items.filter((el) => !el.disabled && !el.hidden);
    }
    get activeItem() {
        const activeElement = getActiveElement();
        return this.getItems.find((i) => i.getAttribute('tabindex') === '0' ||
            i === activeElement ||
            i === document.activeElement ||
            i.classList.contains('has-focus'));
    }
    get selectedItem() {
        return this.getItems.find((i) => i.selected);
    }
    async setActiveFocusItem(item, focus = true, preventScroll = false) {
        const items = this.getItems;
        const activeItem = !item
            ? items.find((i) => i.selected) || items[0]
            : item;
        items
            .filter((i) => i.tagName.toLowerCase() === transformTag('nano-option'))
            .map((i) => i.setAttribute('tabindex', i === activeItem ? '0' : '-1'));
        if (!focus)
            return;
        if (activeItem) {
            if (preventScroll) {
                activeItem.focus({ preventScroll: true });
            }
            else if (activeItem.setFocus) {
                activeItem.setFocus();
            }
            else {
                activeItem.focus();
            }
        }
        else if (!this.hasFocus) {
            this.menu.focus({ preventScroll });
        }
    }
    // Event Handlers
    handleBlur(e) {
        if (!this.hasFocus)
            return;
        const kev = e;
        let found;
        if (kev.key) {
            if (kev.key !== 'Tab') {
                return;
            }
            found =
                getActiveElement() &&
                    getActiveElement().closest(this.el.tagName.toLowerCase()) === this.el;
        }
        else {
            found = !!e.composedPath().find((el) => el === this.el);
        }
        if (!found) {
            this.resetActiveItem();
            this._hasFocus = false;
            this.nanoBlur.emit();
        }
    }
    handleFocus = () => {
        this._hasFocus = true;
        this.setActiveFocusItem(this.selectedItem || this.getItems[0]);
        this.nanoFocus.emit();
    };
    handleClick = (event) => {
        const target = event.target;
        const item = target.closest(transformTag('nano-nav-item'));
        if (item && !item.disabled) {
            this.nanoSelect.emit(item);
        }
    };
    handleKeyDown = (event) => {
        // When keying through the menu, if the mouse happens to be hovering over a menu item and the menu scrolls, the
        // mouseout/mouseover event will fire causing the selection to be different than what the user expects. This gives
        // us a way to temporarily ignore mouse events while the user is interacting with a keyboard.
        clearTimeout(this.ignoreMouseTimeout);
        this.ignoreMouseTimeout = setTimeout(() => (this.ignoreMouseEvents = false), 500);
        this.ignoreMouseEvents = true;
        // Make a selection when pressing enter
        switch (event.key) {
            case ' ':
                if (this.activeItem) {
                    event.stopPropagation();
                    if (this.activeItem.nodeName !== 'BUTTON') {
                        this.activeItem.click();
                    }
                }
                break;
            case 'ArrowDown':
            case 'ArrowUp':
            case 'PageDown':
            case 'PageUp':
            case 'Home':
            case 'End': {
                const items = this.getItems;
                const selectedItem = this.activeItem;
                let index = items.indexOf(selectedItem);
                if (items.length) {
                    event.preventDefault();
                    if (event.key === 'ArrowDown') {
                        index++;
                    }
                    else if (event.key === 'ArrowUp') {
                        index--;
                    }
                    else if (event.key === 'Home' || event.key === 'PageUp') {
                        index = 0;
                    }
                    else if (event.key === 'End' || event.key === 'PageDown') {
                        index = items.length - 1;
                    }
                    if (index < 0)
                        index = items.length - 1;
                    if (index > items.length - 1)
                        index = 0;
                    this.setActiveFocusItem(items[index]);
                    if (items[index])
                        items[index].scrollIntoView({ block: 'nearest' });
                    return;
                }
                break;
            }
        }
        // Handle type-to-search behavior when non-control characters are entered
        if (event.key === ' ' || /^[a-z0-9]+$/i.test(event.key)) {
            clearTimeout(this.typeToSelectTimeout);
            this.typeToSelectTimeout = setTimeout(() => (this.typeToSelect = ''), 750);
            this.typeToSelect += event.key;
            const items = this.getItems;
            for (const item of items) {
                const label = getTextContent(item).toLowerCase().trim();
                if (label.substring(0, this.typeToSelect.length) === this.typeToSelect) {
                    this.setActiveFocusItem(item);
                    break;
                }
            }
        }
    };
    handleMouseOver = (event) => {
        const target = event.target;
        const item = target.closest(transformTag('nano-nav-item')) ||
            target.closest(transformTag('nano-option')) ||
            target.closest('a');
        if (item && !this.ignoreMouseEvents) {
            this.setActiveFocusItem(item, true, true);
        }
    };
    handleSlotContent = () => {
        this._type = this.type || null;
        if (!this._type) {
            let linkCount, btnCount = 0;
            getTabbableElements(this.el).forEach((item) => {
                if (item.tagName === 'A' || item.role === 'link') {
                    linkCount++;
                }
                else {
                    btnCount++;
                }
            });
            this._type = linkCount > btnCount ? 'nav' : 'menu';
        }
        // a nav role can have any type of children
        if (this._type === 'nav')
            return;
        // any direct children of a role="menu|listbox" need an explicit role, other a role of 'none'
        getDirectChildren(this.el, `*:not(${transformTag('nano-nav-item')}, a, ${transformTag('nano-option')}, ${transformTag('nano-dropdown')})`, true).forEach((el) => {
            if (this.getItems.indexOf(el) < 0) {
                el.setAttribute('role', 'none');
            }
        });
        this.el
            .querySelectorAll(`a, ${transformTag('nano-dropdown')}`)
            .forEach((el) => {
            if (el.tagName === 'A') {
                // links need to be set to role="menuitem" to be valid
                el.setAttribute('role', 'menuitem');
            }
            else {
                // nested dropdowns need to be set to role="group" to be valid
                el.setAttribute('role', 'group');
            }
        });
    };
    // Component Lifecycle
    componentWillLoad() {
        this.handleSlotContent();
    }
    render() {
        return (h(Host, { key: '40403e7a4f942f8fa8594edc9ceb24f33aba6979', role: this._type === 'listbox' ? 'group' : this._type, class: "nano-menu" }, h("div", { key: '561716ed34aa038cebacb31dc3b8562829f062e4', onClick: this.handleClick, onKeyDown: this.handleKeyDown, onMouseOver: this.handleMouseOver, onFocus: this.handleFocus, "aria-label": this.label ? this.label : undefined, ref: (el) => (this.menu = el), part: "base", class: {
                menu: true,
                ['menu--' + this._type]: true,
                'menu--has-focus': this.hasFocus,
            }, tabIndex: -1, role: "group" }, h("slot", { key: 'e36d4071433a6e7ddd700aee284868856cbd82ff', onSlotchange: this.handleSlotContent }))));
    }
    static get is() { return "nano-menu"; }
    static get encapsulation() { return "shadow"; }
    static get originalStyleUrls() {
        return {
            "$": ["menu.scss"]
        };
    }
    static get styleUrls() {
        return {
            "$": ["menu.css"]
        };
    }
    static get properties() {
        return {
            "hasFocus": {
                "type": "boolean",
                "attribute": "has-focus",
                "mutable": false,
                "complexType": {
                    "original": "boolean",
                    "resolved": "boolean",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [{
                            "name": "readonly",
                            "text": undefined
                        }],
                    "text": "get the focus state of the menu"
                },
                "getter": true,
                "setter": false,
                "reflect": false,
                "defaultValue": "false"
            },
            "type": {
                "type": "string",
                "attribute": "type",
                "mutable": false,
                "complexType": {
                    "original": "'menu' | 'listbox' | 'nav'",
                    "resolved": "\"listbox\" | \"menu\" | \"nav\"",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "changes the role of the underlying control (required for accessibility semantics).\nBy default this will be set via the frequency of element type (e.g. `<button>` vs `<a>`)"
                },
                "getter": false,
                "setter": false,
                "reflect": false
            },
            "label": {
                "type": "string",
                "attribute": "label",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "an accessible label"
                },
                "getter": false,
                "setter": false,
                "reflect": false
            }
        };
    }
    static get states() {
        return {
            "_type": {}
        };
    }
    static get events() {
        return [{
                "method": "nanoFocus",
                "name": "nanoFocus",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted when the menu gains focus."
                },
                "complexType": {
                    "original": "any",
                    "resolved": "any",
                    "references": {}
                }
            }, {
                "method": "nanoBlur",
                "name": "nanoBlur",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted when the menu loses focus."
                },
                "complexType": {
                    "original": "any",
                    "resolved": "any",
                    "references": {}
                }
            }, {
                "method": "nanoSelect",
                "name": "nanoSelect",
                "bubbles": true,
                "cancelable": true,
                "composed": true,
                "docs": {
                    "tags": [],
                    "text": "Emitted when a menu item is selected."
                },
                "complexType": {
                    "original": "HTMLNanoOptionElement | HTMLNanoNavItemElement",
                    "resolved": "HTMLNanoNavItemElement | HTMLNanoOptionElement",
                    "references": {
                        "HTMLNanoOptionElement": {
                            "location": "global",
                            "id": "global::HTMLNanoOptionElement"
                        },
                        "HTMLNanoNavItemElement": {
                            "location": "global",
                            "id": "global::HTMLNanoNavItemElement"
                        }
                    }
                }
            }];
    }
    static get methods() {
        return {
            "setFocus": {
                "complexType": {
                    "signature": "() => Promise<void>",
                    "parameters": [],
                    "references": {
                        "Promise": {
                            "location": "global",
                            "id": "global::Promise"
                        }
                    },
                    "return": "Promise<void>"
                },
                "docs": {
                    "text": "Sets focus on the menu.",
                    "tags": []
                }
            },
            "removeFocus": {
                "complexType": {
                    "signature": "() => Promise<void>",
                    "parameters": [],
                    "references": {
                        "Promise": {
                            "location": "global",
                            "id": "global::Promise"
                        }
                    },
                    "return": "Promise<void>"
                },
                "docs": {
                    "text": "Removes focus from the menu.",
                    "tags": []
                }
            },
            "showActiveElement": {
                "complexType": {
                    "signature": "() => Promise<void>",
                    "parameters": [],
                    "references": {
                        "Promise": {
                            "location": "global",
                            "id": "global::Promise"
                        }
                    },
                    "return": "Promise<void>"
                },
                "docs": {
                    "text": "Sets the current active item",
                    "tags": []
                }
            },
            "resetActiveItem": {
                "complexType": {
                    "signature": "() => Promise<void>",
                    "parameters": [],
                    "references": {
                        "Promise": {
                            "location": "global",
                            "id": "global::Promise"
                        }
                    },
                    "return": "Promise<void>"
                },
                "docs": {
                    "text": "Removes any active item's state",
                    "tags": []
                }
            }
        };
    }
    static get elementRef() { return "el"; }
    static get listeners() {
        return [{
                "name": "mousedown",
                "method": "handleBlur",
                "target": "body",
                "capture": false,
                "passive": true
            }, {
                "name": "keydown",
                "method": "handleBlur",
                "target": undefined,
                "capture": false,
                "passive": false
            }];
    }
}
