{"version":3,"file":"modal-manager-section.min.js","mappings":"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","sources":["webpack://lange/./sections/modal-manager/index.ts"],"sourcesContent":["// Entry point for the modal-manager section. This is modeled after the native dialog element and\n// its show-modal behavior: a top-layer modal over an inert background, a backdrop that dismisses on\n// click or escape, and focus returned to the opener on close. We reimplement those semantics in\n// userland rather than using the native element because we do not yet have broad enough support for\n// dialog across the browsers our visitors use.\n//\n// Each modal event gets exactly one listener here, and that listener runs every step in a fixed\n// order. If each step had its own listener, the order would come down to which listener happened to\n// register first, and some of the steps care about order. For example, on close the background has\n// to stop being inert before focus goes back to the opener.\n\n/**\n * The currently active modal dialog element, or null when no modal dialog is open.\n *\n * Modality is a singleton: at most one modal owns the screen at any instant, so a single reference\n * is enough to track it. This is only used for diagnostics. Nothing else reads it, so it being\n * wrong can never strand the page.\n */\nlet activeModal: HTMLElement | null = null;\n\n/**\n * A stack of elements to restore keyboard focus to as modals close.\n *\n * A stack rather than a single slot because openers accumulate across a sequence of modals: when\n * one modal hands off to another it closes before the next opens, and a modal's opener can itself\n * be an element inside a modal that has since closed. Keeping the history rather than clearing it\n * on each restore lets focus fall back to an earlier opener still on the page instead of being\n * lost.\n *\n * We do not use WeakRef here because the support is not yet widespread enough (iOS > 14.4). It\n * should not be a big problem as only a limited number of elements will be stored here at any given\n * time.\n */\nconst focusStack: HTMLElement[] = [];\n\nfunction onModalOpenRequested(event: GlobalEventHandlersEventMap['modal-open-requested']) {\n  traceOpen(event.target);\n  raiseBackdrop();\n  inertBackground(event.target);\n  lockScroll();\n  hideAttentive();\n  hideGladly();\n}\n\n/**\n * Undo everything set up on open. The teardown is idempotent and does not depend on which modal\n * closed, so it always runs in full, even when the close does not match the modal we think is\n * active.\n */\nfunction onModalCloseRequested(event: GlobalEventHandlersEventMap['modal-close-requested']) {\n  // The diagnostics run first so the mismatch report captures the page as it was when the close\n  // came in, before any of the teardown below changes it.\n  traceClose(event.target);\n\n  // Focus cannot land on an element still inside an inert subtree, so the background has to be made\n  // interactive again before focus goes back to the opener.\n  clearInertBackground();\n  restoreFocus();\n\n  lowerBackdrop();\n  unlockScroll();\n  showAttentive();\n  showGladly();\n}\n\n/**\n * Record an element that wants keyboard focus returned to it once an open modal is closed. This is\n * announced by whatever is about to open a modal (for example, the cart drawer's open button). We\n * only remember the element here. Restoring focus to it happens in {@link restoreFocus}, which runs\n * when a modal close is requested.\n */\nfunction onFocusStackPushRequested(\n  event: GlobalEventHandlersEventMap['focus-stack-push-requested']) {\n  const target = event.composedPath()[0];\n  if (target instanceof HTMLElement) {\n    focusStack.push(target);\n  }\n}\n\n/**\n * Raise the backdrop and install the dismiss affordances. The listener cleanup makes this\n * idempotent, so repeated raises, such as during a modal-to-modal handoff where the incoming modal\n * opens while the backdrop is already shown, cannot accumulate duplicate listeners.\n */\nfunction raiseBackdrop() {\n  const backdrop = document.querySelector<HTMLElement>('#modal-backdrop');\n\n  // just in case of a bug/inconsistency we clean up any previous listeners\n  backdrop.removeEventListener('click', onBackdropClicked);\n  document.removeEventListener('keyup', onKeyUp);\n\n  backdrop.addEventListener('click', onBackdropClicked);\n  document.addEventListener('keyup', onKeyUp);\n\n  backdrop.classList.add('active');\n}\n\n/**\n * Lower the backdrop and remove the dismiss affordances. Safe to call when the backdrop is already\n * hidden, since removing an absent listener or class is a no-op.\n */\nfunction lowerBackdrop() {\n  const backdrop = document.querySelector<HTMLElement>('#modal-backdrop');\n  backdrop.removeEventListener('click', onBackdropClicked);\n\n  document.removeEventListener('keyup', onKeyUp);\n\n  backdrop.classList.remove('active');\n}\n\nfunction onBackdropClicked(event: MouseEvent) {\n  requestModalDismiss(event);\n}\n\n/**\n * Requests dismissal when escape is pressed. This is bound to the document rather than a specific\n * element so it fires wherever focus happens to be while a modal is open, since focus could be on\n * any control inside the modal or nowhere in particular.\n */\nfunction onKeyUp(event: KeyboardEvent) {\n  if (event.code === 'Escape') {\n    requestModalDismiss(event);\n  }\n}\n\n/**\n * Translate a raw dismiss input (e.g. a backdrop click or an escape key press) into the single\n * dismiss broadcast that open modals listen for. Centralizing the bridge here keeps the originating\n * input from mattering to anything downstream. The originating event is just for logging.\n *\n * Dismissal is centralized here rather than duplicated per modal. That is why this code does not\n * know or care which modal is open, it only announces that a dismiss was requested. Not every modal\n * opts into the broadcast (some intentionally do not close on backdrop or escape), so a dismiss\n * does not always result in a close.\n */\nfunction requestModalDismiss(event: Event) {\n  console.log('Modal dismiss requested via', event.type);\n\n  const dismissEvent = new Event('modal-dismiss-requested');\n  dispatchEvent(dismissEvent);\n}\n\n/**\n * Make all the Shopify sections (including the header and the footer, but excluding the section\n * containing the modal dialog) inert. This is a relatively safe and widely supported way of\n * disabling the active background elements while a modal window is shown. They have to be disabled\n * to avoid a11y problems with keyboard browsing. It stands in for the native dialog element's top\n * layer, which makes everything outside the open dialog inert automatically.\n *\n * The skip link is inerted separately below rather than folded into the sweep above: it renders as\n * a direct child of <body>, before any section markup, so it never matches `.shopify-section` and\n * would otherwise stay reachable behind the modal.\n */\nfunction inertBackground(modal: EventTarget) {\n  if (modal instanceof HTMLElement) {\n    const section = globalClosest(modal, '.shopify-section');\n    const sections = document.querySelectorAll<HTMLElement>('.shopify-section');\n    for (const otherSection of sections) {\n      if (otherSection !== section && !otherSection.classList.contains('a11y-always-active')) {\n        otherSection.inert = true;\n      }\n    }\n\n    const skipLink = document.querySelector('.skip-link');\n    if (skipLink instanceof HTMLElement) {\n      skipLink.inert = true;\n    }\n  }\n}\n\nfunction clearInertBackground() {\n  const sections = document.querySelectorAll<HTMLElement>('.shopify-section');\n  for (const section of sections) {\n    section.inert = false;\n  }\n\n  const skipLink = document.querySelector('.skip-link');\n  if (skipLink instanceof HTMLElement) {\n    skipLink.inert = false;\n  }\n}\n\n/**\n * Return true if the element is focusable, i.e. it exists in the DOM and is visible. The visibility\n * check uses the visibility API (if available) and also checks if the element is within the\n * viewport horizontally (our drawers are not really hidden but moved off-screen horizontally). If\n * the element is off-screen vertically - we can still focus it and the browser will scroll it into\n * view.\n */\nfunction isElementFocusable(element: HTMLElement) {\n  if (!element) {\n    return false;\n  }\n\n  // the visibility API may not be available\n  if (element.checkVisibility) {\n    const visibility = element.checkVisibility({\n      opacityProperty: true,\n      visibilityProperty: true\n    });\n\n    if (!visibility) {\n      return false;\n    }\n  }\n\n  const rect = element.getBoundingClientRect();\n  const isInViewport = rect.left < window.innerWidth && rect.right > 0;\n  return isInViewport;\n}\n\n/**\n * Restore focus to the last element in the focus stack that is still focusable. This is the end of\n * the focus-restoration chain: elements arrive on the stack via onFocusStackPushRequested, and this\n * runs when a modal close is requested to return focus to whichever element opened the modal.\n */\nfunction restoreFocus() {\n  for (let i = focusStack.length - 1; i >= 0; i--) {\n    const element = focusStack[i];\n\n    if (isElementFocusable(element)) {\n      element.focus({ focusVisible: true });\n      // return and keep the element on the stack\n      return;\n    }\n\n    // Element no longer available or visible - remove it from the stack and try the next one\n    focusStack.pop();\n  }\n\n  // No element can be focused - do nothing\n}\n\n/**\n * Finds an ancestor matching a selector, crossing shadow root boundaries that the platform\n * closest() stops at. Modal elements and their openers live inside custom-element shadow roots, so\n * callers that need the light-DOM section wrapper around them have to walk out through those roots.\n */\nfunction globalClosest(element: Element, selector: string) {\n  if (!element) {\n    return;\n  }\n\n  const closest = element.closest(selector);\n\n  if (closest) {\n    return closest;\n  }\n\n  const root = element.getRootNode();\n  // under some circumstances getRootNode() may return the document, the element itself etc we are\n  // only interested in cases where the root is a custom element's ShadowRoot\n  if (isShadowRoot(root)) {\n    return globalClosest(root.host, selector);\n  }\n}\n\nfunction isShadowRoot(value: any): value is ShadowRoot {\n  return value instanceof ShadowRoot;\n}\n\n/**\n * The lock is a flag on the document element that a style rule reacts to by suppressing scroll.\n * Suppressing the scrollbar reclaims its width, which would otherwise let the page shift sideways\n * as it appears and disappears, so before locking we measure the current scrollbar width and expose\n * it as a custom property the same style rule pads by. The width is only measured, never cleared,\n * because the padding it feeds only applies while the lock flag is present. Unlocking removes just\n * the flag.\n *\n * The measurement is skipped unless it comes out to a whole number, guarding against fractional or\n * bogus readings during layout churn that would otherwise pad by a nonsensical amount.\n */\nfunction lockScroll() {\n  const scrollbarWidth = innerWidth - document.documentElement.clientWidth;\n  if (Number.isInteger(scrollbarWidth)) {\n    document.documentElement.style.setProperty('--scrollbar-width', scrollbarWidth + 'px');\n  }\n\n  document.documentElement.dataset.scrollLocked = 'true';\n}\n\nfunction unlockScroll() {\n  delete document.documentElement.dataset.scrollLocked;\n}\n\n/**\n * Attentive renders its creatives inside a single container it injects at the document level, and\n * that content lives in a cross-origin iframe whose subtree we cannot reach. So rather than drive\n * Attentive's own API, annotate the document element and let a style rule force the whole container\n * out of view. Keeping the flag on the root means it survives Attentive re-rendering its subtree\n * and needs no lookup of the widget.\n *\n * Attentive renders more than one kind of creative into that container over a visitor's session.\n * One is a game overlay that toggles between a maximized and a minimized state, which are a single\n * element differing only by style properties rather than separate or added and removed elements.\n * Another, live now, is a bar that docks along the bottom of the viewport carrying a sign-up or\n * offer unit in its own cross-origin iframe. Because the flag forces the whole container out of\n * view by its stable identifier, every creative it renders is hidden regardless of type or internal\n * state, without this code needing to know or care which one is showing.\n *\n * This deliberately avoids Attentive's own hide and show API. The hide call would clear the\n * creatives, but the show call is not a visibility restore, it re-runs creative eligibility and\n * increments a frequency counter, so calling it on every modal close would re-trigger sign-up units\n * and pollute Attentive's own frequency data. Annotating the root instead only switches visibility.\n * It does not close an open creative, so if Attentive's full screen popup is open underneath a\n * modal, its scroll lock and its aria-hidden on the rest of the page stay on until the modal closes\n * and the popup shows again. That is fine here because every modal closes, and it is why a hide\n * that can last a whole visit has to leave the full screen popup visible.\n *\n * Expect a harmless focus-trap error in the console while a modal hides Attentive. Collapsing the\n * overlay collapses its sign-up creative, which renders in a cross-origin iframe, and that creative\n * tries to trap focus inside itself. With the iframe collapsed there is nothing focusable to trap,\n * so its focus-trap throws during activation, before it engages, so focus is never actually\n * trapped. The throw is inside a cross-origin iframe, so it never reaches our error reporting and\n * cannot be caught or muted from here. Leaving the overlay focusable instead would trap a keyboard\n * user inside the invisible iframe, which is worse.\n */\nfunction hideAttentive() {\n  document.documentElement.dataset.attentiveHidden = 'true';\n}\n\nfunction showAttentive() {\n  delete document.documentElement.dataset.attentiveHidden;\n}\n\n/**\n * Gladly offers no public method to hide its chat launcher, and it tears down and rebuilds its own\n * subtree as it opens and closes. In addition, Gladly routinely fails to load, and it does not load\n * in a reliable timeframe, and its API is generally sparse and undocumented. So rather than touch\n * Gladly's own elements or use its abysmal API, just annotate the document element and let a style\n * rule react. Keeping the flag on the root means it survives Gladly re-rendering and needs no\n * lookup of the widget. This works regardless of whether Gladly is loaded on the current page,\n * because it never searches for the element. If Gladly is not loaded, no harm; if it is, the style\n * rule takes effect.\n *\n * Opening or closing the Gladly chat does not announce a modal, so this never runs in response to\n * Gladly's own panel and there is no circular path where opening Gladly would hide itself. This was\n * verified at runtime against the live widget, not just the theme source, because Gladly is\n * injected from outside the theme and the source alone cannot rule it out.\n */\nfunction hideGladly() {\n  document.documentElement.dataset.gladlyHidden = 'true';\n}\n\nfunction showGladly() {\n  delete document.documentElement.dataset.gladlyHidden;\n}\n\n/**\n * Track the newly opened modal as the active one and trace the lifecycle as a breadcrumb.\n */\nfunction traceOpen(modal: EventTarget) {\n  if (modal instanceof HTMLElement) {\n    // Log level on purpose: the remote logger records log lines as breadcrumbs rather than\n    // capturing them as events the way it does warnings, so a later error carries a record of which\n    // modals opened and closed and in what order, at no cost of its own. Recording what was active\n    // before this modal is what reveals a modal opening over an already-open one.\n\n    console.log('Modal opening (%s was active, %s opening)', toDescriptor(activeModal),\n      toDescriptor(modal));\n\n    activeModal = modal;\n  }\n}\n\n/**\n * Trace the close as a breadcrumb, check it against the tracked modal, and clear the tracking.\n */\nfunction traceClose(modal: EventTarget) {\n  if (modal instanceof HTMLElement) {\n    console.log('Modal closing (%s was active, closing %s)', toDescriptor(activeModal),\n      toDescriptor(modal));\n\n    checkModalMismatch(modal, activeModal);\n\n    activeModal = null;\n  }\n}\n\n/**\n * A loosely-typed snapshot of a modal element's identity for diagnostics. We capture descriptive\n * fields rather than the element itself so the logged value is a plain serializable object and so\n * we never retain a reference to a possibly-detached element.\n */\ninterface ElementDescriptor {\n  classes: string | null;\n  id: string | null;\n  sectionId: string | null;\n  tag: string;\n}\n\n/**\n * The error logged when a close request does not match the active modal. Everything beyond the\n * standard message is a non-standard property that the logging integration copies into the captured\n * event as extra context. The mismatchType is the most useful field: it separates a redundant close\n * with nothing open from a genuine case of a different modal still being tracked.\n */\ninterface ModalMismatchError extends Error {\n  activeModal: ElementDescriptor | null;\n  backdropActive: boolean | null;\n  closingModal: ElementDescriptor | null;\n  focusStack: (ElementDescriptor | null)[];\n  focusStackDepth: number;\n  inertSectionCount: number;\n  mismatchType: 'different-modal-active' | 'no-active-modal';\n  scrollLocked: boolean;\n}\n\n/**\n * Given an element, produce a short descriptor string useful for logging and debugging.\n */\nfunction toDescriptor(element: Element | null) {\n  if (element instanceof Element) {\n    const tag = element.tagName.toLowerCase();\n    if (element.id) {\n      return `${tag}#${element.id}`;\n    }\n\n    return tag;\n  }\n\n  return null;\n}\n\nfunction describeElement(element: HTMLElement | null) {\n  if (element instanceof HTMLElement) {\n    const section = globalClosest(element, '.shopify-section');\n\n    return {\n      tag: element.tagName.toLowerCase(),\n      id: element.id || null,\n      classes: element.className || null,\n      sectionId: section?.id || null\n    };\n  }\n\n  return null;\n}\n\n/**\n * Check whether a close request targets the modal currently tracked as active, and warn with a rich\n * diagnostic when it does not.\n *\n * A mismatch is usually benign rather than a real fault. Features swap one modal for another as a\n * sequential handoff, the outgoing modal requesting close before the incoming one requests show, so\n * the reference normally points at exactly one modal through a transition rather than holding two.\n * The common mismatch is a redundant close of an already-closed modal (a double click on a close\n * control, or a backdrop click landing together with an escape press), which arrives with no modal\n * active. A mismatch naming a different, still-tracked modal would instead mean two were open at\n * once, which the handoff model is not supposed to allow.\n *\n * Both shapes have reported from production. The first is the redundant close: a cart drawer\n * closing while already closed, with nothing recorded as active and the page already fully torn\n * down. That was the dominant source and is now guarded at its origin so it no longer reaches the\n * handler, and the same guard covers a rapid repeat dismissal whose second activation sees an\n * already-closed drawer and bails before announcing anything. The second is genuinely two modals\n * open at once, which earlier data had suggested never happened. It does: opening the cart drawer\n * does not close an already-open mega-menu panel, so both stay open, each with its own dismiss\n * listener, and a single dismiss closes both back to back, the menu while the cart is still tracked\n * active, then the cart once the menu's teardown has cleared tracking. So the first lands as a\n * different-modal mismatch and the second as a nothing-active one. The end state is still correct:\n * the dismiss broadcast closes everything and the page tears down clean, which is the recovery that\n * broadcast exists to provide, so these warnings are its cost rather than a stranded state. Closing\n * the menu as the cart opens would remove this source the way the guard removed the redundant\n * close, but that handoff is not yet in place.\n *\n * This check is purely diagnostic and never gates the close. Even on a genuine mismatch the rest of\n * the teardown must still run, which clears inert, hides the backdrop, unlocks scroll, and restores\n * focus, because that teardown is idempotent and skipping it can strand the page with a stuck\n * backdrop or sections left inert. Until the handoff above exists, the singleton active-modal\n * reference plus that idempotent teardown is the safety net for both shapes.\n *\n * A decorated error is logged rather than a bare message because from the message alone we cannot\n * tell those two cases apart, and the remote logging integration copies non-standard error\n * properties into the captured event, so every bit of state that might help diagnose a given\n * occurrence after the fact is attached. Building the diagnostic only reads existing state and\n * mutates nothing, but it is wrapped so a failure here can never stop the teardown from running.\n */\nfunction checkModalMismatch(closingModal: HTMLElement, currentActiveModal: HTMLElement | null) {\n  // Building the diagnostic reads live DOM and focus-stack state, so despite looking like it cannot\n  // fail, a read here throwing must never abort the teardown. Fall back to a bare message if it\n  // does.\n\n  if (closingModal !== currentActiveModal) {\n    try {\n      console.warn(createModalMismatchError(closingModal, currentActiveModal));\n    } catch (error) {\n      console.warn('Modal close requested for a modal that is not active');\n      console.warn(error);\n    }\n  }\n}\n\nfunction createModalMismatchError(closingModal: HTMLElement,\n  currentActiveModal: HTMLElement | null) {\n  const backdrop = document.querySelector('#modal-backdrop');\n\n  const message = 'Modal close requested for a modal that is not active';\n  const error = <ModalMismatchError> new Error(message);\n  error.mismatchType = currentActiveModal ? 'different-modal-active' : 'no-active-modal';\n  error.closingModal = describeElement(closingModal);\n  error.activeModal = describeElement(currentActiveModal);\n  error.focusStackDepth = focusStack.length;\n  error.focusStack = focusStack.map(element => describeElement(element));\n  error.inertSectionCount = document.querySelectorAll('.shopify-section[inert]').length;\n  error.backdropActive = backdrop ? backdrop.classList.contains('active') : null;\n  error.scrollLocked = document.documentElement.dataset.scrollLocked === 'true';\n  return error;\n}\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    /**\n     * Requests dismissal of the open modal in response to a light-dismiss gesture - a click on the\n     * backdrop or an escape key press. Carries no payload; listeners close themselves in response.\n     * This is the user's upstream \"please dismiss\" intent, separate from the close request a modal\n     * dispatches afterward to ask the manager to run its teardown.\n     *\n     * It is deliberately a global broadcast to every open modal rather than a message aimed at the\n     * single modal the manager believes is active, and that naivety is the point. A modal only\n     * listens for this while it is open, so one dismiss closes everything that is currently open\n     * regardless of whether the manager's record of the active modal is correct. That makes it a\n     * safety net for the cases where modal state has gone bad: stale tracking, two modals somehow\n     * open at once, or otherwise inconsistent state. Because modals open and close as a sequential\n     * handoff rather than as truly nested dialogs, there is never a legitimate reason for a dismiss\n     * to spare an open modal, so closing all of them is always the intended outcome.\n     *\n     * Do not narrow this to a message aimed only at the active modal. That would tie dismissal to\n     * the correctness of the very bookkeeping that is least trustworthy here, and would throw away\n     * the recovery behavior that lets a single dismiss clean up an inconsistent state.\n     */\n    'modal-dismiss-requested': Event;\n  }\n}\n\naddEventListener('focus-stack-push-requested', onFocusStackPushRequested);\naddEventListener('modal-open-requested', onModalOpenRequested);\naddEventListener('modal-close-requested', onModalCloseRequested);\n"],"names":["activeModal","focusStack","onBackdropClicked","event","requestModalDismiss","onKeyUp","code","console","log","type","dismissEvent","Event","dispatchEvent","isElementFocusable","element","checkVisibility","opacityProperty","visibilityProperty","rect","getBoundingClientRect","left","window","innerWidth","right","globalClosest","selector","closest","root","getRootNode","ShadowRoot","host","toDescriptor","Element","tag","tagName","toLowerCase","id","describeElement","HTMLElement","section","classes","className","sectionId","addEventListener","target","composedPath","push","modal","backdrop","document","querySelector","removeEventListener","classList","add","raiseBackdrop","sections","querySelectorAll","otherSection","contains","inert","skipLink","inertBackground","scrollbarWidth","documentElement","clientWidth","Number","isInteger","style","setProperty","dataset","scrollLocked","lockScroll","attentiveHidden","gladlyHidden","closingModal","currentActiveModal","warn","error","Error","mismatchType","focusStackDepth","length","map","inertSectionCount","backdropActive","createModalMismatchError","checkModalMismatch","clearInertBackground","i","focus","focusVisible","pop","restoreFocus","remove","lowerBackdrop"],"sourceRoot":""}