Simple Gamepad Navigation

Navigate (almost) any accessible website with a controller.

Od 07.09.2025.. Pogledajte najnovija verzija.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Simple Gamepad Navigation
// @namespace    https://miniwangdali.github.io/simple-gamepad-navigation/
// @version      0.0.1
// @author       [email protected]
// @description  Navigate (almost) any accessible website with a controller.
// @license      MIT
// @icon         https://raw.githubusercontent.com/miniwangdali/SimpleGamepadNavigation/refs/heads/main/packages/monkey-script/asset/icon.svg
// @match        *://*/*
// @grant        window.close
// ==/UserScript==

(function () {
  'use strict';

  var NavigationDirection;
  (function(NavigationDirection2) {
    NavigationDirection2["Up"] = "up";
    NavigationDirection2["Down"] = "down";
    NavigationDirection2["Left"] = "left";
    NavigationDirection2["Right"] = "right";
  })(NavigationDirection || (NavigationDirection = {}));
  var ScrollDirection;
  (function(ScrollDirection2) {
    ScrollDirection2["Vertical"] = "vertical";
    ScrollDirection2["Horizontal"] = "horizontal";
  })(ScrollDirection || (ScrollDirection = {}));
  const isSliderElement = (element) => {
    return !!element && element instanceof HTMLInputElement && (element.type === "range" || element.role === "slider");
  };
  const isTabListElement = (element) => {
    return element.role === "tablist";
  };
  const getTabListElementOfTarget = (target) => {
    let el = target;
    while (el) {
      if (el.previousSibling instanceof Element && isTabListElement(el.previousSibling)) {
        return el.previousSibling;
      }
      if (el.nextSibling instanceof Element && isTabListElement(el.nextSibling)) {
        return el.nextSibling;
      }
      if (isTabListElement(el)) {
        return el;
      }
      el = el.parentElement;
    }
    return el;
  };
  const getTabItemsOfTabList = (tabList) => {
    return tabList.querySelectorAll('[role="tab"]');
  };
  function debounce(func, debounceMs, { signal, edges } = {}) {
    let pendingThis = void 0;
    let pendingArgs = null;
    const leading = edges != null && edges.includes("leading");
    const trailing = edges == null || edges.includes("trailing");
    const invoke = () => {
      if (pendingArgs !== null) {
        func.apply(pendingThis, pendingArgs);
        pendingThis = void 0;
        pendingArgs = null;
      }
    };
    const onTimerEnd = () => {
      if (trailing) {
        invoke();
      }
      cancel();
    };
    let timeoutId = null;
    const schedule = () => {
      if (timeoutId != null) {
        clearTimeout(timeoutId);
      }
      timeoutId = setTimeout(() => {
        timeoutId = null;
        onTimerEnd();
      }, debounceMs);
    };
    const cancelTimer = () => {
      if (timeoutId !== null) {
        clearTimeout(timeoutId);
        timeoutId = null;
      }
    };
    const cancel = () => {
      cancelTimer();
      pendingThis = void 0;
      pendingArgs = null;
    };
    const flush = () => {
      invoke();
    };
    const debounced = function(...args) {
      if (signal?.aborted) {
        return;
      }
      pendingThis = this;
      pendingArgs = args;
      const isFirstCall = timeoutId == null;
      schedule();
      if (leading && isFirstCall) {
        invoke();
      }
    };
    debounced.schedule = schedule;
    debounced.cancel = cancel;
    debounced.flush = flush;
    signal?.addEventListener("abort", cancel, { once: true });
    return debounced;
  }
  function throttle(func, throttleMs, { signal, edges = ["leading", "trailing"] } = {}) {
    let pendingAt = null;
    const debounced = debounce(func, throttleMs, { signal, edges });
    const throttled = function(...args) {
      if (pendingAt == null) {
        pendingAt = Date.now();
      } else {
        if (Date.now() - pendingAt >= throttleMs) {
          pendingAt = Date.now();
          debounced.cancel();
        }
      }
      debounced(...args);
    };
    throttled.cancel = debounced.cancel;
    throttled.flush = debounced.flush;
    return throttled;
  }
  const calculateDistanceOfTwoPoints = (pointA, pointB) => {
    const dx = pointB.x - pointA.x;
    const dy = pointB.y - pointA.y;
    return Math.sqrt(dx * dx + dy * dy);
  };
  const DISTANCE_BUFFER = 2;
  const getDistanceOfTwoElements = (sourceElementRect, targetElementRect, side) => {
    switch (side) {
      case "top": {
        const sourceTopMiddlePoint = {
          x: (sourceElementRect.left + sourceElementRect.right) / 2,
          y: sourceElementRect.top
        };
        const targetBottomMiddlePoint = {
          x: (targetElementRect.left + targetElementRect.right) / 2,
          y: targetElementRect.bottom
        };
        let distance = calculateDistanceOfTwoPoints(sourceTopMiddlePoint, targetBottomMiddlePoint);
        if (sourceTopMiddlePoint.y < targetBottomMiddlePoint.y - DISTANCE_BUFFER) {
          distance = Infinity;
        }
        return distance;
      }
      case "bottom": {
        const sourceBottomMiddlePoint = {
          x: (sourceElementRect.left + sourceElementRect.right) / 2,
          y: sourceElementRect.bottom
        };
        const targetTopMiddlePoint = {
          x: (targetElementRect.left + targetElementRect.right) / 2,
          y: targetElementRect.top
        };
        let distance = calculateDistanceOfTwoPoints(sourceBottomMiddlePoint, targetTopMiddlePoint);
        if (sourceBottomMiddlePoint.y > targetTopMiddlePoint.y + DISTANCE_BUFFER) {
          distance = Infinity;
        }
        return distance;
      }
      case "left": {
        const sourceLeftMiddlePoint = {
          x: sourceElementRect.left,
          y: (sourceElementRect.top + sourceElementRect.bottom) / 2
        };
        const targetRightMiddlePoint = {
          x: targetElementRect.right,
          y: (targetElementRect.top + targetElementRect.bottom) / 2
        };
        let distance = calculateDistanceOfTwoPoints(sourceLeftMiddlePoint, targetRightMiddlePoint);
        if (sourceLeftMiddlePoint.x < targetRightMiddlePoint.x - DISTANCE_BUFFER) {
          distance = Infinity;
        }
        return distance;
      }
      case "right": {
        const sourceRightMiddlePoint = {
          x: sourceElementRect.right,
          y: (sourceElementRect.top + sourceElementRect.bottom) / 2
        };
        const targetLeftMiddlePoint = {
          x: targetElementRect.left,
          y: (targetElementRect.top + targetElementRect.bottom) / 2
        };
        let distance = calculateDistanceOfTwoPoints(sourceRightMiddlePoint, targetLeftMiddlePoint);
        if (sourceRightMiddlePoint.x > targetLeftMiddlePoint.x + DISTANCE_BUFFER) {
          distance = Infinity;
        }
        return distance;
      }
    }
  };
  const isElementInRect = (elementRect, containerRect) => {
    return elementRect.top >= containerRect.top - DISTANCE_BUFFER && elementRect.bottom <= containerRect.bottom + DISTANCE_BUFFER && elementRect.left >= containerRect.left - DISTANCE_BUFFER && elementRect.right <= containerRect.right + DISTANCE_BUFFER;
  };
  const isDialogElement = (element) => {
    return element instanceof HTMLDialogElement || element.role === "dialog" || element.getAttribute("aria-modal") === "true";
  };
  const getDialogElementOfTarget = (target) => {
    let el = target;
    while (el) {
      if (isDialogElement(el)) {
        return el;
      }
      el = el.parentElement;
    }
    return el;
  };
  const INPUT_ROLES = ["textbox", "searchbox", "combobox", "slider", "spinbutton"];
  const INTERACTABLE_ROLES = ["button", "link", "checkbox", "radio", "slider", "tab", ...INPUT_ROLES];
  const isContentEditable = (element) => {
    return element.hasAttribute("contenteditable") && element.getAttribute("contenteditable") !== "false";
  };
  const hasInputRole = (element) => {
    return element.hasAttribute("role") && INPUT_ROLES.includes(element.getAttribute("role"));
  };
  const hasInteractableRole = (element) => {
    return element.hasAttribute("role") && INTERACTABLE_ROLES.includes(element.getAttribute("role"));
  };
  const isInputElement = (element) => {
    return element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || hasInputRole(element) || isContentEditable(element);
  };
  const isVisibleElement = (element) => {
    if (!(element instanceof HTMLElement))
      return false;
    const rect = element.getBoundingClientRect();
    if (rect.width === 0 && rect.height === 0 && rect.x === 0 && rect.y === 0)
      return false;
    const style = getComputedStyle(element);
    return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
  };
  const isInteractableElement = (element) => {
    return element instanceof HTMLButtonElement || element instanceof HTMLAnchorElement || isInputElement(element) || hasInteractableRole(element);
  };
  const getInteractableElements = (restriction) => {
    const treeWalker = document.createTreeWalker(restriction?.restrictedRootElement || document.body, NodeFilter.SHOW_ELEMENT);
    const interactableElements = [];
    while (treeWalker.nextNode()) {
      const currentNode = treeWalker.currentNode;
      if ((currentNode instanceof Element || currentNode.nodeType === Node.ELEMENT_NODE) && isInteractableElement(currentNode)) {
        const interactableRect = currentNode.getBoundingClientRect();
        const inRestrictedRect = !restriction?.restrictedRect || isElementInRect(interactableRect, restriction.restrictedRect);
        if (inRestrictedRect && isVisibleElement(currentNode)) {
          interactableElements.push(currentNode);
        }
      }
    }
    return interactableElements;
  };
  const findNewInteractableElement = (currentElement, direction) => {
    const currentRect = currentElement.getBoundingClientRect();
    const restrictedRootElement = getDialogElementOfTarget(currentElement);
    const allInteractableElements = getInteractableElements({ restrictedRootElement });
    let candidate = void 0;
    let candidateDistance = Infinity;
    for (const element of allInteractableElements) {
      if (element === currentElement)
        continue;
      const elementRect = element.getBoundingClientRect();
      let distance = 0;
      switch (direction) {
        case "up":
          distance = getDistanceOfTwoElements(currentRect, elementRect, "top");
          break;
        case "down":
          distance = getDistanceOfTwoElements(currentRect, elementRect, "bottom");
          break;
        case "left":
          distance = getDistanceOfTwoElements(currentRect, elementRect, "left");
          break;
        case "right":
          distance = getDistanceOfTwoElements(currentRect, elementRect, "right");
          break;
      }
      if (distance >= 0 && distance < candidateDistance) {
        candidate = element;
        candidateDistance = distance;
      }
    }
    return candidate;
  };
  const getNearestScrollContainer = (element) => {
    let el = element;
    while (el) {
      if (el instanceof HTMLElement) {
        const style = getComputedStyle(el);
        const overflowY = style.overflowY;
        const isScrollableY = (overflowY === "auto" || overflowY === "scroll") && el.scrollHeight > el.clientHeight;
        const overflowX = style.overflowX;
        const isScrollableX = (overflowX === "auto" || overflowX === "scroll") && el.scrollWidth > el.clientWidth;
        if (isScrollableY || isScrollableX) {
          return el;
        }
      }
      el = el.parentElement;
      if (el === document.documentElement) {
        break;
      }
    }
    return el instanceof HTMLElement ? el : null;
  };
  const canScroll = (scrollContainer, direction, speed) => {
    switch (direction) {
      case ScrollDirection.Vertical: {
        const reverse = getComputedStyle(scrollContainer).flexDirection === "column-reverse";
        if (speed >= 0) {
          return reverse ? scrollContainer.scrollTop < 0 : scrollContainer.scrollTop + scrollContainer.clientHeight < scrollContainer.scrollHeight;
        } else if (speed < 0) {
          return reverse ? -scrollContainer.scrollTop + scrollContainer.clientHeight < scrollContainer.scrollHeight : scrollContainer.scrollTop > 0;
        }
        break;
      }
      case ScrollDirection.Horizontal: {
        const reverse = getComputedStyle(scrollContainer).flexDirection === "row-reverse";
        if (speed >= 0) {
          return reverse ? scrollContainer.scrollLeft < 0 : scrollContainer.scrollLeft + scrollContainer.clientWidth < scrollContainer.scrollWidth;
        } else if (speed < 0) {
          return reverse ? -scrollContainer.scrollLeft + scrollContainer.clientWidth < scrollContainer.scrollWidth : scrollContainer.scrollLeft > 0;
        }
        break;
      }
    }
    return false;
  };
  const isElementPositionedUnRelatedToScrollContainer = (element, scrollContainer) => {
    let el = element;
    let unrelatedPosition = false;
    const treeWalker = document.createTreeWalker(scrollContainer, NodeFilter.SHOW_ELEMENT);
    const elementsInScrollContainer = new Set();
    while (treeWalker.nextNode()) {
      elementsInScrollContainer.add(treeWalker.currentNode);
    }
    while (el && elementsInScrollContainer.has(el)) {
      if (el === scrollContainer) {
        return false;
      }
      const position = getComputedStyle(el).position;
      if (position === "absolute" || position === "fixed") {
        unrelatedPosition = true;
      }
      el = el.offsetParent;
    }
    return unrelatedPosition;
  };
  const THROTTLE_DELAY$1 = 250;
  const scroll = (direction, speed, originalPosition, immediate) => {
    if (document.activeElement) {
      const scrollContainer = getNearestScrollContainer(document.activeElement);
      if (scrollContainer) {
        originalPosition.x = scrollContainer.scrollLeft;
        originalPosition.y = scrollContainer.scrollTop;
        const style = getComputedStyle(scrollContainer);
        const overflowY = style.overflowY;
        const isScrollableY = (overflowY === "auto" || overflowY === "scroll" || scrollContainer === document.documentElement) && scrollContainer.scrollHeight > scrollContainer.clientHeight;
        const overflowX = style.overflowX;
        const isScrollableX = (overflowX === "auto" || overflowX === "scroll" || scrollContainer === document.documentElement) && scrollContainer.scrollWidth > scrollContainer.clientWidth;
        const scrollAmount = scrollContainer.clientHeight * speed;
        switch (direction) {
          case ScrollDirection.Vertical:
            if (isScrollableY) {
              scrollContainer.scrollBy({ top: scrollAmount, behavior: immediate ? "auto" : "smooth" });
            }
            break;
          case ScrollDirection.Horizontal:
            if (isScrollableX) {
              scrollContainer.scrollBy({ left: scrollAmount, behavior: immediate ? "auto" : "smooth" });
            }
            break;
        }
        return true;
      }
    }
    return false;
  };
  const throttledScroll = throttle((direction, speed, state, immediate) => {
    state.result = scroll(direction, speed, state.originalPosition, immediate);
  }, THROTTLE_DELAY$1, { edges: ["leading"] });
  const THROTTLE_DELAY = 250;
  const focusInteractableElement = (target) => {
    if (target instanceof HTMLElement) {
      document.activeElement.blur();
      target.focus();
      target.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
      return true;
    }
    return false;
  };
  const focusNextInteractableElement = (current, direction) => {
    let nextElement = findNewInteractableElement(current, direction);
    const scrollContainer = getNearestScrollContainer(current);
    const scrollDirection = direction === NavigationDirection.Up || direction === NavigationDirection.Down ? ScrollDirection.Vertical : ScrollDirection.Horizontal;
    const speed = direction === NavigationDirection.Up || direction === NavigationDirection.Left ? -0.5 : 0.5;
    while (true) {
      if (scrollContainer && nextElement && canScroll(scrollContainer, scrollDirection, speed) && isElementPositionedUnRelatedToScrollContainer(nextElement, scrollContainer)) {
        const originalPosition = { x: 0, y: 0 };
        throttledScroll(scrollDirection, speed, { result: false, originalPosition }, true);
        const newNextElement = findNewInteractableElement(current, direction);
        if (newNextElement === nextElement) {
          scrollContainer.scrollTo(originalPosition.x, originalPosition.y);
          break;
        }
        nextElement = newNextElement;
        continue;
      }
      break;
    }
    if (nextElement instanceof HTMLElement) {
      return focusInteractableElement(nextElement);
    }
    return false;
  };
  const navigate = (direction) => {
    const viewportRect = new DOMRect(0, 0, window.innerWidth, window.innerHeight);
    if (document.activeElement && document.activeElement !== document.body && isElementInRect(document.activeElement.getBoundingClientRect(), viewportRect)) {
      return focusNextInteractableElement(document.activeElement, direction);
    } else {
      const allInteractableElements = getInteractableElements({ restrictedRect: viewportRect });
      let candidate;
      let candidateRect;
      switch (direction) {
        case NavigationDirection.Up:
          candidateRect = new DOMRect(Infinity, 0, 0, 0);
          break;
        case NavigationDirection.Right:
        case NavigationDirection.Down:
          candidateRect = new DOMRect(Infinity, Infinity, 0, 0);
          break;
        case NavigationDirection.Left:
          candidateRect = new DOMRect(0, Infinity, 0, 0);
          break;
      }
      for (const element of allInteractableElements) {
        const elementRect = element.getBoundingClientRect();
        switch (direction) {
          case NavigationDirection.Up:
            if (elementRect.bottom > candidateRect.bottom || elementRect.bottom === candidateRect.bottom && elementRect.left < candidateRect.left) {
              candidate = element;
              candidateRect = elementRect;
            }
            break;
          case NavigationDirection.Right:
            if (elementRect.left < candidateRect.left || elementRect.left === candidateRect.left && elementRect.top < candidateRect.top) {
              candidate = element;
              candidateRect = elementRect;
            }
            break;
          case NavigationDirection.Down:
            if (elementRect.top < candidateRect.top || elementRect.top === candidateRect.top && elementRect.left < candidateRect.left) {
              candidate = element;
              candidateRect = elementRect;
            }
            break;
          case NavigationDirection.Left:
            if (elementRect.right > candidateRect.right || elementRect.right === candidateRect.right && elementRect.top < candidateRect.top) {
              candidate = element;
              candidateRect = elementRect;
            }
            break;
        }
      }
      if (candidate) {
        return focusInteractableElement(candidate);
      }
    }
    return false;
  };
  const throttledNavigate = throttle((direction, state) => {
    state.result = navigate(direction);
  }, THROTTLE_DELAY, { edges: ["leading"] });
  const LEFT_THUMBSTICK_DEFAULT_THRESHOLD = 0.6;
  const RIGHT_THUMBSTICK_DEFAULT_THRESHOLD = 0.4;
  var XboxButton;
  (function(XboxButton2) {
    XboxButton2[XboxButton2["A"] = 0] = "A";
    XboxButton2[XboxButton2["B"] = 1] = "B";
    XboxButton2[XboxButton2["X"] = 2] = "X";
    XboxButton2[XboxButton2["Y"] = 3] = "Y";
    XboxButton2[XboxButton2["LeftBumper"] = 4] = "LeftBumper";
    XboxButton2[XboxButton2["RightBumper"] = 5] = "RightBumper";
    XboxButton2[XboxButton2["LeftTrigger"] = 6] = "LeftTrigger";
    XboxButton2[XboxButton2["RightTrigger"] = 7] = "RightTrigger";
    XboxButton2[XboxButton2["View"] = 8] = "View";
    XboxButton2[XboxButton2["Menu"] = 9] = "Menu";
    XboxButton2[XboxButton2["LeftThumbStick"] = 10] = "LeftThumbStick";
    XboxButton2[XboxButton2["RightThumbStick"] = 11] = "RightThumbStick";
    XboxButton2[XboxButton2["DpadUp"] = 12] = "DpadUp";
    XboxButton2[XboxButton2["DpadDown"] = 13] = "DpadDown";
    XboxButton2[XboxButton2["DpadLeft"] = 14] = "DpadLeft";
    XboxButton2[XboxButton2["DpadRight"] = 15] = "DpadRight";
    XboxButton2[XboxButton2["Nexus"] = 16] = "Nexus";
  })(XboxButton || (XboxButton = {}));
  const buttonKeyMap = {
    [XboxButton.A]: "Enter",
    [XboxButton.B]: "Escape",
    [XboxButton.LeftTrigger]: "Shift",
    [XboxButton.RightTrigger]: "Control",
    [XboxButton.DpadUp]: "ArrowUp",
    [XboxButton.DpadDown]: "ArrowDown",
    [XboxButton.DpadLeft]: "ArrowLeft",
    [XboxButton.DpadRight]: "ArrowRight"
  };
  class XboxStandardController {
    constructor() {
      Object.defineProperty(this, "pressedKeys", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: new Set()
      });
      Object.defineProperty(this, "workingTabListElement", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: null
      });
      Object.defineProperty(this, "workingOnSlider", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: false
      });
      Object.defineProperty(this, "getValidGamepad", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: (gamepadId) => {
          const gamepads = GamepadManager.getInstance().getGamepads();
          if (!gamepads.has(gamepadId)) {
            return null;
          }
          return gamepads.get(gamepadId);
        }
      });
      Object.defineProperty(this, "checkNavigation", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: (gamepadId) => {
          const gamepad = this.getValidGamepad(gamepadId);
          if (!gamepad) {
            return false;
          }
          const x = gamepad.axes[0];
          const y = gamepad.axes[1];
          const resultState = { result: false };
          if (x > LEFT_THUMBSTICK_DEFAULT_THRESHOLD) {
            throttledNavigate(NavigationDirection.Right, resultState);
          } else if (x < -LEFT_THUMBSTICK_DEFAULT_THRESHOLD) {
            throttledNavigate(NavigationDirection.Left, resultState);
          }
          if (y > RIGHT_THUMBSTICK_DEFAULT_THRESHOLD) {
            throttledNavigate(NavigationDirection.Down, resultState);
          } else if (y < -RIGHT_THUMBSTICK_DEFAULT_THRESHOLD) {
            throttledNavigate(NavigationDirection.Up, resultState);
          }
          const dpadUp = gamepad.buttons[XboxButton.DpadUp];
          const dpadDown = gamepad.buttons[XboxButton.DpadDown];
          const dpadLeft = gamepad.buttons[XboxButton.DpadLeft];
          const dpadRight = gamepad.buttons[XboxButton.DpadRight];
          if (!this.workingOnSlider) {
            if (dpadUp.pressed) {
              throttledNavigate(NavigationDirection.Up, resultState);
            }
            if (dpadDown.pressed) {
              throttledNavigate(NavigationDirection.Down, resultState);
            }
            if (dpadLeft.pressed) {
              throttledNavigate(NavigationDirection.Left, resultState);
            }
            if (dpadRight.pressed) {
              throttledNavigate(NavigationDirection.Right, resultState);
            }
          }
          return resultState.result;
        }
      });
      Object.defineProperty(this, "checkScrolling", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: (gamepadId) => {
          const gamepad = this.getValidGamepad(gamepadId);
          if (!gamepad) {
            return false;
          }
          const x = gamepad.axes[2];
          const y = gamepad.axes[3];
          const resultState = { result: false, originalPosition: { x: 0, y: 0 } };
          if (x > RIGHT_THUMBSTICK_DEFAULT_THRESHOLD || x < -RIGHT_THUMBSTICK_DEFAULT_THRESHOLD) {
            throttledScroll(ScrollDirection.Horizontal, x, resultState);
          }
          if (y > RIGHT_THUMBSTICK_DEFAULT_THRESHOLD || y < -RIGHT_THUMBSTICK_DEFAULT_THRESHOLD) {
            throttledScroll(ScrollDirection.Vertical, y, resultState);
          }
          return resultState.result;
        }
      });
      Object.defineProperty(this, "checkButtonKeyEvent", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: (buttonIndex, gamepad) => {
          const button = gamepad.buttons[buttonIndex];
          if (button.pressed) {
            const hasPressedKey = this.pressedKeys.has(buttonIndex);
            if (!hasPressedKey) {
              this.pressedKeys.add(buttonIndex);
            }
            const eventTarget = document.activeElement || document.body;
            if (!hasPressedKey) {
              if (eventTarget instanceof HTMLAnchorElement && buttonIndex === XboxButton.A) {
                eventTarget.click();
              }
            }
            const enterKeyDownEvent = new KeyboardEvent("keydown", { key: buttonKeyMap[buttonIndex], bubbles: true });
            eventTarget.dispatchEvent(enterKeyDownEvent);
          } else if (this.pressedKeys.has(buttonIndex)) {
            this.pressedKeys.delete(buttonIndex);
            if (buttonIndex === XboxButton.A && isSliderElement(document.activeElement)) {
              this.workingOnSlider = true;
            }
            if (this.workingOnSlider && buttonIndex === XboxButton.B) {
              this.workingOnSlider = false;
            }
            const eventTarget = document.activeElement || document.body;
            const enterKeyUpEvent = new KeyboardEvent("keyup", { key: buttonKeyMap[buttonIndex], bubbles: true });
            eventTarget.dispatchEvent(enterKeyUpEvent);
          }
        }
      });
      Object.defineProperty(this, "checkMenuButton", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: (gamepadId) => {
          const gamepad = this.getValidGamepad(gamepadId);
          if (!gamepad) {
            return;
          }
          const menuButton = gamepad.buttons[XboxButton.Menu];
          if (menuButton.pressed) {
            if (!this.pressedKeys.has(XboxButton.Menu)) {
              this.pressedKeys.add(XboxButton.Menu);
              const eventTarget = document.activeElement || document.body;
              const eventInit = {
                bubbles: true,
                cancelable: true,
                button: 2,
                buttons: 2,
                view: window
              };
              eventTarget.dispatchEvent(new MouseEvent("mousedown", eventInit));
              eventTarget.dispatchEvent(new MouseEvent("contextmenu", eventInit));
            }
          } else if (this.pressedKeys.has(XboxButton.Menu)) {
            this.pressedKeys.delete(XboxButton.Menu);
            const eventTarget = document.activeElement || document.body;
            const eventInit = {
              bubbles: true,
              cancelable: true,
              button: 2,
              buttons: 0,
              view: window
            };
            eventTarget.dispatchEvent(new MouseEvent("mouseup", eventInit));
          }
        }
      });
      Object.defineProperty(this, "checkViewButton", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: (gamepadId) => {
          const gamepad = this.getValidGamepad(gamepadId);
          if (!gamepad) {
            return;
          }
          const viewButton = gamepad.buttons[XboxButton.View];
          if (viewButton.pressed) {
            if (!this.pressedKeys.has(XboxButton.View)) {
              this.pressedKeys.add(XboxButton.View);
            }
          } else if (this.pressedKeys.has(XboxButton.View)) {
            this.pressedKeys.delete(XboxButton.View);
            if (this.pressedKeys.has(XboxButton.LeftTrigger)) {
              window.close();
            }
          }
        }
      });
      Object.defineProperty(this, "checkButton", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: (gamepadId) => {
          const gamepad = this.getValidGamepad(gamepadId);
          if (!gamepad) {
            return;
          }
          this.checkButtonKeyEvent(XboxButton.A, gamepad);
          this.checkButtonKeyEvent(XboxButton.B, gamepad);
          this.checkButtonKeyEvent(XboxButton.LeftTrigger, gamepad);
          this.checkButtonKeyEvent(XboxButton.RightTrigger, gamepad);
          if (this.workingOnSlider) {
            this.checkButtonKeyEvent(XboxButton.DpadLeft, gamepad);
            this.checkButtonKeyEvent(XboxButton.DpadRight, gamepad);
            this.checkButtonKeyEvent(XboxButton.DpadUp, gamepad);
            this.checkButtonKeyEvent(XboxButton.DpadDown, gamepad);
          }
          this.checkMenuButton(gamepadId);
          this.checkViewButton(gamepadId);
        }
      });
      Object.defineProperty(this, "checkBumper", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: (bumper, gamepad) => {
          const button = gamepad.buttons[bumper];
          if (button.pressed) {
            if (!this.pressedKeys.has(bumper)) {
              this.pressedKeys.add(bumper);
            }
          } else if (this.pressedKeys.has(bumper)) {
            this.pressedKeys.delete(bumper);
            if (this.pressedKeys.has(XboxButton.RightTrigger)) {
              if (bumper === XboxButton.LeftBumper) {
                history.back();
              } else {
                history.forward();
              }
              return true;
            }
            const tabList = this.workingTabListElement || (document.activeElement ? getTabListElementOfTarget(document.activeElement) : null);
            if (tabList) {
              this.workingTabListElement = tabList;
              const tabItems = getTabItemsOfTabList(tabList);
              const selectedTabIndex = Array.from(tabItems).findIndex((tab) => tab.getAttribute("aria-selected") === "true");
              let nextTabItem = 0;
              if (bumper === XboxButton.LeftBumper) {
                nextTabItem = Math.max(0, selectedTabIndex - 1);
              } else {
                nextTabItem = Math.min(tabItems.length - 1, selectedTabIndex + 1);
              }
              tabItems.item(nextTabItem).click();
              tabItems.item(nextTabItem).focus();
              return nextTabItem !== selectedTabIndex;
            }
          }
          return false;
        }
      });
      Object.defineProperty(this, "checkBumpers", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: (gamepadId) => {
          const gamepad = this.getValidGamepad(gamepadId);
          if (!gamepad) {
            return;
          }
          let result = false;
          result || (result = this.checkBumper(XboxButton.LeftBumper, gamepad));
          result || (result = this.checkBumper(XboxButton.RightBumper, gamepad));
          return result;
        }
      });
    }
    checkInput(gamepad) {
      const navigated = this.checkNavigation(gamepad.index);
      this.checkScrolling(gamepad.index);
      this.checkButton(gamepad.index);
      if (navigated) {
        this.workingTabListElement = null;
        this.workingOnSlider = false;
      }
      this.checkBumpers(gamepad.index);
    }
  }
  const defaultAdapter = new XboxStandardController();
  class GamepadManager {
    static getInstance() {
      if (!GamepadManager.instance) {
        GamepadManager.instance = new GamepadManager();
      }
      return GamepadManager.instance;
    }
    constructor() {
      Object.defineProperty(this, "gamepads", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: new Map()
      });
      Object.defineProperty(this, "pollingHandle", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: null
      });
      Object.defineProperty(this, "gamepadConnectedListener", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: (e) => {
          const gamepad = navigator.getGamepads()[e.gamepad.index];
          if (gamepad && (gamepad.mapping === "standard" || gamepad.mapping === "xr-standard") && !this.gamepads.has(gamepad.index)) {
            this.addGamepad(gamepad);
          }
        }
      });
      Object.defineProperty(this, "gamepadDisconnectedListener", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: (e) => {
          if (this.gamepads.has(e.gamepad.index)) {
            this.removeGamepad(e.gamepad);
          }
        }
      });
      Object.defineProperty(this, "addGamepad", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: (gamepad) => {
          this.gamepads.set(gamepad.index, gamepad);
        }
      });
      Object.defineProperty(this, "removeGamepad", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: (gamepad) => {
          this.gamepads.delete(gamepad.index);
        }
      });
      Object.defineProperty(this, "updateGamepad", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: (gamepad) => {
          this.gamepads.set(gamepad.index, gamepad);
        }
      });
      Object.defineProperty(this, "startPolling", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: () => {
          const gamepads = navigator.getGamepads().filter((gp) => this.gamepads.has(gp?.index ?? -1));
          for (const gamepad of gamepads) {
            this.updateGamepad(gamepad);
            const adapter = defaultAdapter;
            adapter.checkInput(gamepad);
          }
          this.pollingHandle = requestAnimationFrame(this.startPolling);
        }
      });
      Object.defineProperty(this, "stopPolling", {
        enumerable: true,
        configurable: true,
        writable: true,
        value: () => {
          if (this.pollingHandle !== null) {
            cancelAnimationFrame(this.pollingHandle);
            this.pollingHandle = null;
          }
        }
      });
      this.initialize();
    }
    initialize() {
      window.addEventListener("gamepadconnected", this.gamepadConnectedListener);
      window.addEventListener("gamepaddisconnected", this.gamepadDisconnectedListener);
      this.startPolling();
    }
    getGamepads() {
      return this.gamepads;
    }
    cleanUp() {
      this.gamepads.clear();
      this.stopPolling();
      window.removeEventListener("gamepadconnected", this.gamepadConnectedListener);
      window.removeEventListener("gamepaddisconnected", this.gamepadDisconnectedListener);
    }
  }
  const tag = "[SimpleGamepadNavigation]";
  let gamepadManager;
  const initializeGamepadNavigation = () => {
    const globalThis = window;
    if (globalThis.SimpleGamepadNavigation?.initialized) {
      console.info(`${tag} Already initialized.`);
      return;
    }
    window.addEventListener("load", () => {
      gamepadManager = GamepadManager.getInstance();
    });
    window.addEventListener("beforeunload", () => {
      gamepadManager.cleanUp();
    });
    if (!globalThis.SimpleGamepadNavigation) {
      globalThis.SimpleGamepadNavigation = { initialized: true };
    } else {
      globalThis.SimpleGamepadNavigation.initialized = true;
    }
    console.info(`${tag} Initialized.`);
  };
  initializeGamepadNavigation();

})();