'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

const index = require('./index-b7293b2f.js');

const dropdownListboxCss = ":host{--button-background:#fff;--button-border:none;--button-focus-box-shadow:inset 0 0 0 2px #1b2c56;--button-font-size:0.875em;--button-max-height:3em;--button-min-height:3em;--button-padding:1em 3em 1em 1em;--listbox-max-height:80vmin;--listbox-max-width:16.875rem;--list-expanded-box-shadow:inset 0 0 0 2px #1b2c56;--list-expanded-background:#ffffff;--svg-top:1.25em;display:inline-flex;position:relative;width:100%;max-width:16.875rem;max-width:var(--listbox-max-width)}button{flex:1 1 100%;height:100%;min-height:3em;min-height:var(--button-min-height);max-height:3em;max-height:var(--button-max-height);padding:1em 3em 1em 1em;padding:var(--button-padding);transition:box-shadow 0.2s ease-out;border:none;border:var(--button-border);background:#fff;background:var(--button-background);color:inherit;font-family:inherit;font-size:0.875em;font-size:var(--button-font-size);line-height:1;text-align:left;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}button:focus,button:hover{outline:none;box-shadow:0 0 0 2px #1b2c56;box-shadow:var(--button-focus-box-shadow)}button>svg{position:absolute;top:1.25em;top:var(--svg-top);right:1em;width:0.875em;margin-left:1rem;transition:transform 0.1s ease-out}button[aria-expanded]>svg{transform:rotate(180deg)}button+ul{position:absolute;right:0;left:0;max-height:0;margin:0;padding:0.5em 0;transition:opacity 0.1s ease-out;background:#ffffff;background:var(--list-expanded-background);list-style:none;opacity:0;overflow:hidden;visibility:hidden}button[aria-expanded]+ul{max-height:80vmin;max-height:var(--listbox-max-height);transform:translateY(var(--listbox-offset));box-shadow:inset 0 0 0 2px #1b2c56;box-shadow:var(--list-expanded-box-shadow);opacity:1;overflow:auto;visibility:visible;z-index:10}:host .visually-hidden:not(:focus):not(:active){position:absolute;width:1px;height:1px;white-space:nowrap;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%)}";

const DropdownListbox = class {
  constructor(hostRef) {
    index.registerInstance(this, hostRef);
    this.slotHasChanged = index.createEvent(this, "slotHasChanged", 0);
    this.dropdownListboxValueChange = index.createEvent(this, "dropdownListboxValueChange", 7);
    this.dropdownClosed = index.createEvent(this, "dropdownClosed", 7);
    this.options = this.getOptions();
    this.activeDescendant = this.findActiveDescendant();
    this.firstItem = this.options[0];
    this.lastItem = this.options[this.options.length - 1];
    this.lastSelectedValue = this.parseValueFromOption(this.activeDescendant);
    /*
     * This allows you to open and close the dropdown. It could also act as a hook to control on when you would take an action for value change. E.g., Both click and keyboard navigation change the activeDescendant (and thus the value). You are listening to dropdownListboxValueChange to dispatch a fetch event. When a user navigates the dropdown options by keyboard, you would fire a fetch event per arrow down. You could read the value of this prop to ensure that the dropdown is closed before firing your fetch.
     * */
    this.isExpanded = false;
    /*
     * This labels a visually-hidden element that expresses why this listbox exists.
     * */
    this.label = "Select an Option";
    /*
     *  Should an external element act as a label for this listbox? Pass in the ID Reference of that element and do not pass in a `label` prop.
     * */
    this.labelRef = "";
    /*
     *  Get or set the current value.
     * */
    this.value = this.parseValueFromOption(this.activeDescendant);
  }
  handleOptionChange() {
    this.initOptions();
  }
  handleActiveDescendantChange(newOption) {
    if (newOption) {
      this.initActiveDescendant();
      this.value = this.parseValueFromOption(newOption);
    }
    else {
      this.activeDescendant = this.parseOptionFromValue(this.value);
    }
  }
  // If the value is updated because of an activeDescendant change, this will be triggered second. This would include an async-loaded <listbox-option> that triggers 'slotchange'.
  handleValueChange(newValue) {
    this.dropdownListboxValueChange.emit(this.value);
    // Does the new value not equal the current option's value? This must be a programmatic value change, so set the activeDescendant to the option that matches the value.
    if (this.parseValueFromOption(this.activeDescendant) !== newValue) {
      this.activeDescendant = this.parseOptionFromValue(newValue);
    }
  }
  // If the dropdown is closed, this emits an event with the selected value when the dropdown was closed.
  handleExpandedChange(newValue) {
    if (!newValue && this.value && this.lastSelectedValue !== this.value) {
      this.lastSelectedValue = this.value;
      this.dropdownClosed.emit(this.value);
    }
  }
  connectedCallback() {
    this.initOptions();
    this.initActiveDescendant();
  }
  componentDidLoad() {
    var _a;
    (_a = this.host.shadowRoot.querySelector("slot")) === null || _a === void 0 ? void 0 : _a.addEventListener("slotchange", function () {
      this.slotHasChanged.emit(this.getOptions());
    }.bind(this));
    window.requestAnimationFrame(() => {
      this.host.style.setProperty("--listbox-offset", `${this.host.clientHeight}px`);
    });
  }
  componentDidUnload() {
    var _a;
    (_a = this.host.shadowRoot.querySelector("slot")) === null || _a === void 0 ? void 0 : _a.removeEventListener("slotchange", function () {
      this.slotHasChanged.emit(this.getOptions());
    }.bind(this));
  }
  // Based on https://www.w3.org/TR/wai-aria-practices-1.1/examples/listbox/listbox-collapsible.html
  handleKeyDown(ev) {
    switch (ev.key) {
      case "Enter":
        if (this.isExpanded) {
          this.isExpanded = !this.isExpanded;
          window.requestAnimationFrame(() => this.button.focus());
        }
        else {
          ev.preventDefault();
          this.isExpanded = !this.isExpanded;
          window.requestAnimationFrame(() => this.activeDescendant.focus());
        }
        break;
      case "Escape":
        this.isExpanded = false;
        window.requestAnimationFrame(() => this.button.focus());
        break;
      case "ArrowDown":
        ev.preventDefault();
        this.handleUpDownKey(1);
        break;
      case "ArrowUp":
        ev.preventDefault();
        this.handleUpDownKey(-1);
        break;
      case "Home":
        this.handleHomeEndKey(this.firstItem);
        break;
      case "End":
        this.handleHomeEndKey(this.lastItem);
        break;
    }
  }
  handleSlotChange(event) {
    this.options = event.detail;
    this.activeDescendant = this.findActiveDescendant();
  }
  // A click outside of the host should close the dropdown.
  handleWindowClick({ target }) {
    if (!this.host.contains(target)) {
      this.isExpanded = false;
    }
  }
  handleUpDownKey(direction) {
    this.isExpanded = true;
    let newActiveIndex = Number(this.activeDescendant.id) + direction;
    let arrayUpperBound = this.options.length - 1;
    if (newActiveIndex <= arrayUpperBound && newActiveIndex >= 0) {
      this.activeDescendant = this.options[newActiveIndex];
    }
    window.requestAnimationFrame(() => this.activeDescendant.focus());
  }
  handleHomeEndKey(element) {
    if (this.isExpanded) {
      this.activeDescendant = element;
      window.requestAnimationFrame(() => this.activeDescendant.focus());
    }
  }
  handleOptionClick({ target, currentTarget }) {
    // If the click is on the UL to which this event listener is attached, do nothing.
    if (target === currentTarget) {
      return;
    }
    // On Edge, the target is the child <li>, so use the closest <lu-listbox-option>. Closest can return self on other browsers.
    this.activeDescendant = target.closest("lu-listbox-option");
    this.isExpanded = !this.isExpanded;
  }
  handleListboxClick() {
    this.isExpanded = !this.isExpanded;
  }
  handleOutsideClick() {
    this.isExpanded = false;
  }
  // ! Preserve until documentation.
  // getSlottedElements({ flatten = false } = {}) {
  //   return this.host.shadowRoot.querySelector("slot")?.assignedElements({
  //     flatten
  //   })
  // }
  // ! Document host.querySelectorAll vs slot.getSlottedElements.
  // ? Slots and their assigned elements are, apparently, only readable after componentDidRender.
  // ? Querying by role did not work (a reflected prop) in time, but querying by tag name did.
  getOptions() {
    return Array.from(this.host.querySelectorAll("lu-listbox-option"));
  }
  initOptions() {
    this.options.forEach((option, index) => {
      option.id = `${index}`;
    });
  }
  parseOptionFromValue(value) {
    return this.options.find((option) => this.parseValueFromOption(option) === value);
  }
  // option?.getAttribute("value") would not read the attribute in time for initial load, but accessing the property directly did.
  parseValueFromOption(option) {
    return (option === null || option === void 0 ? void 0 : option.value) || (option === null || option === void 0 ? void 0 : option.textContent.trim());
  }
  initActiveDescendant() {
    var _a;
    this.options.forEach((option) => option.removeAttribute("aria-selected"));
    (_a = this.activeDescendant) === null || _a === void 0 ? void 0 : _a.setAttribute("aria-selected", "true");
  }
  // * Order of Operation: the value property then the first element.
  findActiveDescendant() {
    return (this.options.find((option) => this.value === this.parseValueFromOption(option)) || this.options[0]);
  }
  render() {
    var _a, _b;
    return (index.h(index.Host, { role: "none" }, !this.labelRef && (index.h("span", { class: "visually-hidden", id: "label" }, this.label)), index.h("button", { id: "button", ref: (element) => (this.button = element), "aria-expanded": this.isExpanded ? "true" : null, "aria-haspopup": "listbox", "aria-labelledby": `${!this.labelRef ? "label" : this.labelRef} button`, type: "button", onClick: () => this.handleListboxClick() }, (_a = this.activeDescendant) === null || _a === void 0 ? void 0 :
      _a.textContent, index.h("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 12 8" }, index.h("path", { fill: "none", "fill-rule": "evenodd", stroke: "#111", "stroke-linecap": "round", "stroke-width": "2", d: "M.971 1.014l5.026 4.972 5.032-4.972" }))), index.h("ul", { tabIndex: -1, role: "listbox", "aria-labelledby": `${!this.labelRef ? "label" : this.labelRef} button`, "aria-activedescendant": (_b = this.activeDescendant) === null || _b === void 0 ? void 0 : _b.id, onClick: (event) => this.handleOptionClick(event) }, index.h("slot", null))));
  }
  get host() { return index.getElement(this); }
  static get watchers() { return {
    "options": ["handleOptionChange"],
    "activeDescendant": ["handleActiveDescendantChange"],
    "value": ["handleValueChange"],
    "isExpanded": ["handleExpandedChange"]
  }; }
};
DropdownListbox.style = dropdownListboxCss;

exports.lu_dropdown_listbox = DropdownListbox;

//# sourceMappingURL=lu-dropdown-listbox.cjs.entry.js.map