text: /**
   * Implementation based on: https://github.com/PolymerElements/iron-overlay-behavior/blob/master/iron-focusables-helper.html
   * The original implementation does not work for non-Polymer web components,
   * and contains several bugs on IE11.
   */
  
  import { isVisible } from './is-visible.js';
  import { sortByTabIndex } from './sort-by-tabindex.js';
  
  // IE11 supports matches as 'msMatchesSelector'
  const matchesFunc = 'matches' in Element.prototype ? 'matches' : 'msMatchesSelector';
  
  /**
   * @param {HTMLElement} element
   * @returns {boolean} Whether the element matches
   */
  function isFocusable(element) {
    // Elements that cannot be focused if they have [disabled] attribute.
    if (element[matchesFunc]('input, select, textarea, button, object')) {
      return element[matchesFunc](':not([disabled])');
    }
  
    // Elements that can be focused even if they have [disabled] attribute.
    return element[matchesFunc]('a[href], area[href], iframe, [tabindex], [contentEditable]');
  }
  
  /**
   * @param {HTMLElement} element
   * @returns {Number}
   */
  function getTabindex(element) {
    if (isFocusable(element)) {
      return Number(element.getAttribute('tabindex') || 0);
    }
    return -1;
  }
  
  /**
   * @param {HTMLElement} element
   */
  function getChildNodes(element) {
    if (element.localName === 'slot') {
      /** @type {HTMLSlotElement} */
      const slot = element;
      return slot.assignedNodes();
    }
  
    const { children } = element.shadowRoot || element;
    // On IE11, SVGElement.prototype.children is undefined
    return children || [];
  }
  
  /**
   * @param {Node} node
   * @returns {boolean}
   */
  function isVisibleElement(node) {
    if (node.nodeType !== Node.ELEMENT_NODE) {
      return false;
    }
  
    // A slot is not visible, but it's children might so we need
    // to treat is as such.
    if (node.localName === 'slot') {
      return true;
    }
  
    return isVisible(/** @type {HTMLElement} */ (node));
  }
  
  /**
   * Recursive function that traverses the children of the target node and finds
   * elements that can receive focus. Mutates the nodes property for performance.
   *
   * @param {Node} node
   * @param {HTMLElement[]} nodes
   * @returns {boolean} whether the returned node list should be sorted. This happens when
   *                    there is an element with tabindex > 0
   */
  function collectFocusableElements(node, nodes) {
    // If not an element or not visible, no need to explore children.
    if (!isVisibleElement(node)) {
      return false;
    }
  
    /** @type {HTMLElement} */
    const element = node;
  
    const tabIndex = getTabindex(element);
    let needsSort = tabIndex > 0;
    if (tabIndex >= 0) {
      nodes.push(element);
    }
  
    const childNodes = getChildNodes(element);
    for (let i = 0; i < childNodes.length; i += 1) {
      needsSort = collectFocusableElements(childNodes[i], nodes) || needsSort;
    }
    return needsSort;
  }
  
  /**
   * @param {Node} node
   * @returns {HTMLElement[]}
   */
  export function getFocusableElements(node) {
    /** @type {HTMLElement[]} */
    const nodes = [];
  
    const needsSort = collectFocusableElements(node, nodes);
    return needsSort ? sortByTabIndex(nodes) : nodes;
  }
  