{"version":3,"file":"accordion-elements-section.min.js","mappings":"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","sources":["webpack://lange/./lib/dom-ready.ts","webpack://lange/./sections/accordion-elements/accordion/index.ts","webpack://lange/./sections/accordion-elements/accordion-item/index.ts"],"sourcesContent":["/**\n * Releases the handle returned from the ready() function.\n *\n * This is idempotent. Additional calls are pointless but harmless.\n */\nexport type ReleaseHandle = () => void;\n\n/**\n * A subset of events that pertain to document ready state.\n */\ntype LoadingEventMap = Pick<DocumentEventMap, 'DOMContentLoaded' | 'load'>;\n\n/**\n * Calls the provided callback when the document is ready. This works both when the document is\n * already ready and when it is not, so callers do not have to care. This exists because it ensures\n * that each caller consistently expresses its dependency on document readiness, and to save a bit\n * of boilerplate.\n *\n * A document is considered ready when it has been parsed. This corresponds to the ready state of\n * 'interactive'. When the document has been fully loaded, the ready state is 'complete'. The\n * callback runs either way, so callers do not have to check which one they are in.\n *\n * Ready means the html is parsed and the DOM is there to query. It does not mean the page's other\n * scripts have run. Deferred scripts and module scripts run after the ready state turns\n * 'interactive' and before DOMContentLoaded fires, so a callback here can run before them. When a\n * caller needs some other script to have run already, this is the wrong tool, and it should wait on\n * whatever that script produces instead.\n *\n * That remedy has a catch of its own when the calling bundle is loaded with async. An async bundle\n * can execute after DOMContentLoaded has already fired, so a plain listener for that event never\n * runs and the work silently never happens. Nothing here can detect that, because by the time this\n * file executes the event is long gone. Catching it needs something registered at parse time,\n * before any bundle loads.\n *\n * There is a second trap right next to that one, and it applies only when waiting for\n * DOMContentLoaded. The ready state stays 'interactive' after DOMContentLoaded has fired, all the\n * way until 'complete', so the ready state cannot tell you whether DOMContentLoaded is still coming\n * or has already gone. Waiting for load is not exposed to this, because the ready state turns\n * 'complete' and load fires in the same task, so seeing 'complete' means load has fired too.\n *\n * When the document is already ready, the callback is invoked via setTimeout. This way, if the\n * callback throws, the error surfaces to the global error handler in a way that is similar to what\n * happens when an event listener throws.\n *\n * We might one day want to move this to an NPM library.\n *\n * Beware of a quirk here. setTimeout yields, and the busier the frontend, the longer it takes to\n * fire. Page load is exactly when the frontend is busiest. The timeout is a floor and not a\n * guarantee, so there is no real bound on how long that yield lasts. What this means is that using\n * this library can introduce more delay than you might expect. Compare code that runs on async load\n * and checks ready state and invokes immediately to code that runs after setTimeout. The former\n * runs in the same tick, the latter runs in \"some later tick\". That later tick can be surprisingly\n * much later. That can cause initial paint issues.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event\n * @see https://html.spec.whatwg.org/multipage/parsing.html#the-end\n */\nexport function ready(callback: (event?: Event) => void,\n  type: keyof LoadingEventMap = 'DOMContentLoaded') {\n  if (document.readyState === 'complete' || (type === 'DOMContentLoaded' &&\n    document.readyState === 'interactive')) {\n    const timer = setTimeout(callback, 0);\n    const handle: ReleaseHandle = () => clearTimeout(timer);\n    return handle;\n  } else {\n    addEventListener(type, callback);\n    const handle: ReleaseHandle = () => removeEventListener(type, callback);\n    return handle;\n  }\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { ready } from '../../../lib/dom-ready.js';\nimport styles from './index.scss';\n\n/**\n * Renders accordion wrapper, it handles basic events for accordion items. This element uses\n * accordion-item elements to render each item.\n */\nclass AccordionElement extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * Whether expanding an item closes whatever else is open. When exclusive, only one item is open\n     * at a time. When not exclusive the items are independent and expanding one leaves the others\n     * alone. Anything other than 'false', the attribute being absent included, is exclusive.\n     */\n    exclusive: 'false' | 'true';\n\n    /**\n     * The view type of the accordion. This is used to determine which styles to apply.\n     */\n    view: 'pdp-faq';\n  };\n\n  private onItemExpandedBound = this.onItemExpanded.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n    this.addEventListener('accordion-item-expanded', this.onItemExpandedBound);\n    this.addEventListener('accordion-item-collapsed', onItemClosed);\n  }\n\n  public disconnectedCallback() {\n    this.removeEventListener('accordion-item-expanded', this.onItemExpandedBound);\n    this.removeEventListener('accordion-item-collapsed', onItemClosed);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style><slot></slot>`;\n    }\n  }\n\n  private onItemExpanded(event: HTMLElementEventMap['accordion-item-expanded']) {\n    // We prevent propagation here because the custom event is exclusively consumed by this class\n    event.stopPropagation();\n\n    // Narrow the type of target from EventTarget to AccordionItem. This runtime check is preferred\n    // over an unsafe hard cast. In practice, the only element that fires the event is an accordion\n    // item element, so this early return should never actually occur.\n\n    if (!isAccordionItem(event.target)) {\n      return;\n    }\n\n    // Collapse the other items so only the expanded one stays open. Every non-target item is set to\n    // an explicit false because the collapse styling is keyed on data-open=\"false\", not on the mere\n    // absence of data-open=\"true\". This is also why we cannot treat the attribute as a boolean and\n    // just delete it (until we update the css). The query is scoped to this accordion so items\n    // belonging to other accordion elements are left untouched.\n    //\n    // When this accordion is not exclusive we skip all of that and let the items open and close on\n    // their own. We read the attribute here rather than caching it so that changing it later takes\n    // effect.\n\n    if (this.dataset.exclusive !== 'false') {\n      const items = this.querySelectorAll('accordion-item');\n      for (const item of items) {\n        if (item !== event.target) {\n          item.dataset.open = 'false';\n        }\n      }\n    }\n\n    event.target.dataset.open = 'true';\n  }\n}\n\n/**\n * This function is not a method of AccordionElement because it never uses `this`.\n */\nfunction onItemClosed(event: HTMLElementEventMap['accordion-item-collapsed']) {\n  // We prevent propagation here because the custom event is exclusively consumed by this class\n\n  event.stopPropagation();\n\n  // onItemClosed is bound to the accordion element, so currentTarget is the accordion element, not\n  // the accordion item element that was closed, so target is the correct property to use here.\n\n  if (isAccordionItem(event.target)) {\n    event.target.dataset.open = 'false';\n  }\n}\n\ntype AccordionItem = HTMLElementTagNameMap['accordion-item'];\n\n/**\n * Narrows the event target to the dispatching accordion-item. The instanceof check makes localName\n * safely accessible, and localName proves the element is specifically an accordion-item.\n */\nfunction isAccordionItem(value: EventTarget | null): value is AccordionItem {\n  return value instanceof HTMLElement && value.localName === 'accordion-item';\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'accordion-element': AccordionElement;\n  }\n}\n\nfunction onReady() {\n  if (!customElements.get('accordion-element')) {\n    customElements.define('accordion-element', AccordionElement);\n  }\n}\n\nready(onReady);\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { ready } from '../../../lib/dom-ready.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders single accordion item, it uses two slot elements to handle title and body of an accordion\n * item. This element is related to the accordion-element.\n */\nclass AccordionItem extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return [\n      'data-open'\n    ];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Whether this item is in the expanded state. When the data-open attribute is absent the\n     * dataset value is undefined, which render treats the same as 'false'. The item starts\n     * collapsed unless data-open=\"true\" is explicitly set.\n     */\n    open: 'false' | 'true';\n\n    /**\n     * The view type of the accordion item. This is used to determine which styles to apply.\n     */\n    view: 'pdp-details';\n  };\n\n  private onClickBound = this.onClick.bind(this);\n\n  private onTransitionEndBound = this.onTransitionEnd.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n    this.addEventListener('click', this.onClickBound);\n\n    const body = this.shadowRoot.querySelector<HTMLElement>('#body');\n    body?.addEventListener('transitionend', this.onTransitionEndBound);\n\n    this.render();\n\n    // An item that starts out open never animates, so there is no animation to keep the scrollbar\n    // out of and we can let it scroll right away.\n\n    if (this.dataset.open === 'true') {\n      body.dataset.scrollable = 'true';\n    }\n  }\n\n  public disconnectedCallback() {\n    this.removeEventListener('click', this.onClickBound);\n\n    const body = this.shadowRoot.querySelector<HTMLElement>('#body');\n    body?.removeEventListener('transitionend', this.onTransitionEndBound);\n  }\n\n  public attributeChangedCallback(name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    // isConnected is checked because there is no need to render while detached. Once connected the\n    // connected callback will trigger the render.\n    //\n    // The old value is compared to the new value because attribute changes occur any time the\n    // attribute is set, including when the value has not actually changed. When the value has not\n    // changed, there is no need to re-render.\n\n    if (this.isConnected && name === 'data-open' && oldValue !== newValue) {\n      this.render();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private render() {\n    const label = this.shadowRoot.querySelector('button');\n    const body = this.shadowRoot.querySelector<HTMLElement>('#body');\n\n    // The body animates its max height whenever the item opens or closes, and while that number is\n    // somewhere in between, the content is taller than the box we are showing it in, so the browser\n    // puts a scrollbar there for the length of the animation. We stop the body from scrolling for\n    // the whole animation and let it scroll again once it has settled open.\n\n    delete body?.dataset.scrollable;\n\n    if (this.dataset.open === 'true') {\n      label?.setAttribute('aria-expanded', 'true');\n      body?.removeAttribute('aria-hidden');\n    } else {\n      label?.setAttribute('aria-expanded', 'false');\n      body?.setAttribute('aria-hidden', 'true');\n    }\n  }\n\n  /**\n   * Keep the capital C in this method's name. The all-lowercase spelling is the native event-\n   * handler property inherited from HTMLElement, so naming this method that way would override the\n   * platform handler instead of defining our own delegated listener.\n   */\n  private onClick(event: MouseEvent) {\n    // Use event delegation to handle clicks on the custom element and any child elements within the\n    // button label\n    if (event.target instanceof Element &&\n      event.target.matches('accordion-item, span[slot=\"label\"]')) {\n      this.toggleAccordion();\n    }\n  }\n\n  /**\n   * Keep the capital E in this method's name, for the same reason as onClick above.\n   */\n  private onTransitionEnd(event: TransitionEvent) {\n    if (event.propertyName === 'max-height' && this.dataset.open === 'true') {\n      const body = this.shadowRoot.querySelector<HTMLElement>('#body');\n      body.dataset.scrollable = 'true';\n    }\n  }\n\n  private toggleAccordion() {\n    const eventType = this.dataset.open === 'true' ?\n      'accordion-item-collapsed' :\n      'accordion-item-expanded';\n\n    // We intentionally omit composed here. This event is private to the accordion: it is dispatched\n    // on this host, which is a light-DOM child of its parent accordion-element, and that parent is\n    // the only consumer (it calls stopPropagation). Because both elements live in the same tree,\n    // the event only needs to bubble; it never has to cross a shadow boundary. Setting composed\n    // would only let it escape into an ancestor shadow tree, which no one would ever consume. By\n    // convention we prefer to narrow the blast radius of events to only what needs to hear them.\n\n    const changeEvent = new CustomEvent(eventType, { bubbles: true });\n    this.dispatchEvent(changeEvent);\n  }\n}\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    'accordion-item-collapsed': CustomEvent;\n    'accordion-item-expanded': CustomEvent;\n  }\n\n  interface HTMLElementTagNameMap {\n    'accordion-item': AccordionItem;\n  }\n}\n\nfunction onReady() {\n  if (!customElements.get('accordion-item')) {\n    customElements.define('accordion-item', AccordionItem);\n  }\n}\n\nready(onReady);\n"],"names":["ready","callback","type","document","readyState","timer","setTimeout","clearTimeout","addEventListener","removeEventListener","AccordionElement","HTMLElement","onItemExpandedBound","this","onItemExpanded","bind","connectedCallback","initializeShadowRoot","onItemClosed","disconnectedCallback","shadowRoot","attachShadow","mode","innerHTML","event","stopPropagation","isAccordionItem","target","dataset","exclusive","items","querySelectorAll","item","open","value","localName","customElements","get","define","AccordionItem","observedAttributes","onClickBound","onClick","onTransitionEndBound","onTransitionEnd","body","querySelector","render","scrollable","attributeChangedCallback","name","oldValue","newValue","isConnected","label","setAttribute","removeAttribute","Element","matches","toggleAccordion","propertyName","eventType","changeEvent","CustomEvent","bubbles","dispatchEvent"],"sourceRoot":""}