{"version":3,"file":"useFocusBoundary.mjs","names":[],"sources":["../../../../package/common/hooks/useFocusBoundary.ts"],"sourcesContent":["import type { RefObject } from 'react';\n\nconst FOCUSABLE_SELECTOR = [\n  'a[href]',\n  'area[href]',\n  'button:not([disabled])',\n  'input:not([disabled])',\n  'select:not([disabled])',\n  'textarea:not([disabled])',\n  '[tabindex]:not([tabindex=\"-1\"])',\n  '[contenteditable=\"true\"]',\n].join(',');\n\n/** Tabbable descendants of `root`, in DOM order. */\nexport function tabbablesIn(root: ParentNode | null): HTMLElement[] {\n  if (!root) return [];\n  return (\n    Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))\n      // A button or a link can be taken out of the tab order too, which the\n      // selector above only catches for elements whose focusability comes from\n      // the attribute. A roving tabindex, where one item of a list is the tab\n      // stop and the rest are reachable by arrow key, otherwise reads as a list\n      // of tab stops, and the Tab boundary lands on the wrong entry.\n      .filter((el) => el.getAttribute('tabindex') !== '-1')\n  );\n}\n\nfunction isTabbable(element: HTMLElement): boolean {\n  return element.matches(FOCUSABLE_SELECTOR) && element.getAttribute('tabindex') !== '-1';\n}\n\nexport interface UseFocusBoundaryArgs {\n  /** The trigger element focus returns to on Escape / Shift+Tab out of the content's top. */\n  triggerRef: RefObject<HTMLElement | null>;\n  /** The portaled content root — tabbables inside it define the Tab boundary. */\n  contentRef: RefObject<HTMLElement | null>;\n}\n\n/**\n * Focus helpers for content portaled to the end of the DOM (e.g. via BodyEnd), where Tab can't\n * reach it in visual document order on its own. Used by Popover and by Menu, which both need to\n * hand focus back to the trigger on Escape / Shift+Tab out of the top, and continue focus\n * naturally past the trigger when Tab exits the bottom.\n */\nexport function useFocusBoundary({ triggerRef, contentRef }: UseFocusBoundaryArgs) {\n  function firstTriggerFocusable(): HTMLElement | null {\n    const trigger = triggerRef.current;\n    // The trigger itself where it takes focus, as an element handed to Popover's `anchor` can;\n    // else the first element inside it that does, as inside Popover's and Menu's own wrappers.\n    if (trigger && isTabbable(trigger)) return trigger;\n    return tabbablesIn(trigger)[0] ?? trigger;\n  }\n\n  function restoreFocusToTrigger() {\n    firstTriggerFocusable()?.focus();\n  }\n\n  // Move focus to the tabbable element that follows the trigger in the document, skipping the\n  // portaled content — the \"natural next\" stop when Tab exits the bottom of the content.\n  function focusAfterTrigger() {\n    const trig = firstTriggerFocusable();\n    // Inside a Modal the next stop is the Modal's, wrapping as its trap does: past its last\n    // element lies the page behind it, and the focus stayed on the panel being removed.\n    const modal = trig?.closest<HTMLElement>('[aria-modal=\"true\"]');\n    const all = tabbablesIn(modal ?? document.body).filter((el) => !contentRef.current?.contains(el));\n    const idx = trig ? all.indexOf(trig) : -1;\n    if (idx < 0) return;\n    (all[idx + 1] ?? (modal ? all[0] : undefined))?.focus();\n  }\n\n  return { firstTriggerFocusable, restoreFocusToTrigger, focusAfterTrigger };\n}\n"],"mappings":"aAEA,IAAM,EAAqB,CACzB,UACA,aACA,yBACA,wBACA,yBACA,2BACA,kCACA,0BACF,CAAC,CAAC,KAAK,GAAG,EAGV,SAAgB,EAAY,EAAwC,CAElE,OADK,EAEH,MAAM,KAAK,EAAK,iBAA8B,CAAkB,CAAC,CAAC,CAM/D,OAAQ,GAAO,EAAG,aAAa,UAAU,IAAM,IAAI,EARtC,CAAC,CAUrB,CAEA,SAAS,EAAW,EAA+B,CACjD,OAAO,EAAQ,QAAQ,CAAkB,GAAK,EAAQ,aAAa,UAAU,IAAM,IACrF,CAeA,SAAgB,EAAiB,CAAE,aAAY,cAAoC,CACjF,SAAS,GAA4C,CACnD,IAAM,EAAU,EAAW,QAI3B,OADI,GAAW,EAAW,CAAO,EAAU,EACpC,EAAY,CAAO,CAAC,CAAC,IAAM,CACpC,CAEA,SAAS,GAAwB,CAC/B,EAAsB,CAAC,EAAE,MAAM,CACjC,CAIA,SAAS,GAAoB,CAC3B,IAAM,EAAO,EAAsB,EAG7B,EAAQ,GAAM,QAAqB,qBAAqB,EACxD,EAAM,EAAY,GAAS,SAAS,IAAI,CAAC,CAAC,OAAQ,GAAO,CAAC,EAAW,SAAS,SAAS,CAAE,CAAC,EAC1F,EAAM,EAAO,EAAI,QAAQ,CAAI,EAAI,GACnC,EAAM,IACT,EAAI,EAAM,KAAO,EAAQ,EAAI,GAAK,IAAA,IAAA,EAAa,MAAM,CACxD,CAEA,MAAO,CAAE,wBAAuB,wBAAuB,mBAAkB,CAC3E"}