function containFocus(rootElement) {
  const focusableElements = getFocusableElements(rootElement);
  // Initial focus goes to first element with autofocus, or the root element
  const initialFocus = focusableElements.find(e => e.hasAttribute('autofocus')) || rootElement;
  /** @type {HTMLElement} */
  let tabDetectionElement;
  /** @type {MutationObserver} */
  let rootElementMutationObserver;

  // If root element will receive focus, it should have a tabindex of -1.
  // This makes it focusable through js, but it won't appear in the tab order
  if (initialFocus === rootElement) {
    rootElement.tabIndex = -1;
    rootElement.style.setProperty('outline', 'none');
  }

  // Focus first focusable element
  initialFocus.focus();

  /**
   * Ensures focus stays inside root element on tab
   * @param {KeyboardEvent} e
   */
  function handleKeydown(e) {
    if (e.keyCode === keyCodes.tab) {
      rotateFocus(rootElement, e);
    }
  }

  function createHelpersDetectingTabDirection() {
    tabDetectionElement = document.createElement('div');
    tabDetectionElement.style.display = 'none';
    tabDetectionElement.setAttribute('data-is-tab-detection-element', '');
    rootElement.insertBefore(tabDetectionElement, rootElement.children[0]);

    rootElementMutationObserver = new MutationObserver(mutationsList => {
      for (const mutation of mutationsList) {
        if (mutation.type === 'childList') {
          const tabDetectionElIsMissing = !Array.from(rootElement.children).find(el =>
            el.hasAttribute('data-is-tab-detection-element'),
          );
          const foundTabDetectionElInMutations = Array.from(mutation.addedNodes).find(
            /** @param {Node} el */ el =>
              el instanceof HTMLElement && el.hasAttribute('data-is-tab-detection-element'),
          );
          // Prevent infinite loop by detecting that mutation event is not from adding the tab detection el
          if (tabDetectionElIsMissing && !foundTabDetectionElInMutations) {
            rootElementMutationObserver.disconnect();
            createHelpersDetectingTabDirection();
          }
        }
      }
    });
    rootElementMutationObserver.observe(rootElement, { childList: true });
  }

  function isForwardTabInWindow() {
    const compareMask = tabDetectionElement.compareDocumentPosition(
      /** @type {Element} */ (document.activeElement),
    );
    return compareMask === Node.DOCUMENT_POSITION_PRECEDING;
  }

  /**
   * @param {Object} opts
   * @param {boolean} [opts.resetToRoot]
   * @desc When we simulate a modal dialog, we need to restore the focus to the first or last
   * element of the rootElement
   */
  function setFocusInRootElement({ resetToRoot = false } = {}) {
    if (deepContains(rootElement, /** @type {HTMLElement} */ (getDeepActiveElement()))) {
      return;
    }

    let nextActive;
    if (resetToRoot) {
      nextActive = rootElement;
    } else {
      nextActive = focusableElements[isForwardTabInWindow() ? 0 : focusableElements.length - 1];
    }

    if (nextActive) {
      nextActive.focus();
    }
  }

  function handleFocusin() {
    window.removeEventListener('focusin', handleFocusin);
    setFocusInRootElement();
  }

  function handleFocusout() {
    /**
     * There is a moment in time between focusout and focusin (when focus shifts)
     * where the activeElement is reset to body first. So we use an async task to check
     * a little bit later for activeElement, so we don't get a false positive.
     *
     * We used to check for focusin event for this, however,
     * it can happen that focusout happens, but focusin never does, e.g. click outside but no focusable
     * element is found to focus. If this happens, we should take the focus back to the rootElement.
     */
    setTimeout(() => {
      if (!deepContains(rootElement, /** @type {HTMLElement} */ (getDeepActiveElement()))) {
        setFocusInRootElement({ resetToRoot: true });
      }
    });

    window.addEventListener('focusin', handleFocusin);
  }

  function disconnect() {
    window.removeEventListener('keydown', handleKeydown);
    window.removeEventListener('focusin', handleFocusin);
    window.removeEventListener('focusout', handleFocusout);
    // Guard this, since we also disconnect if we notice a missing tab
    // detection element. We reinsert it, so it's okay to not fail here.
    rootElementMutationObserver.disconnect();
    if (Array.from(rootElement.children).includes(tabDetectionElement)) {
      rootElement.removeChild(tabDetectionElement);
    }
    rootElement.style.removeProperty('outline');
  }

  window.addEventListener('keydown', handleKeydown);
  window.addEventListener('focusout', handleFocusout);
  createHelpersDetectingTabDirection();

  return { disconnect };
}
Contains focus within given root element. When focus is on the last focusable
element inside the root element, the next focus will be redirected to the first
focusable element.

@param {HTMLElement} rootElement The element to contain focus within
@returns {{ disconnect: () => void }} handler with a disconnect callback