{"version":3,"file":"modal-element-snippet.min.js","mappings":"wZAoBA,MAAMA,qBAAqBC,YACzB,6BAAkBC,GAChB,MAAO,CAAC,mBAAoB,mBAAoB,YAAa,aAC/D,CA+CQC,eAAiBC,KAAKC,UAAUC,KAAKF,MACrCG,0BAA4BH,KAAKI,qBAAqBF,KAAKF,MAC3DK,6BAA+BL,KAAKM,wBAAwBJ,KAAKF,MAElE,iBAAAO,GACLP,KAAKQ,uBAEAR,KAAKS,aAAa,UACrBT,KAAKU,KAAO,UAGTV,KAAKS,aAAa,eACrBT,KAAKW,aAAa,aAAc,QAGdX,KAAKY,WAAWC,cAAc,gBACtCC,iBAAiB,QAASd,KAAKG,2BAE3CH,KAAKc,iBAAiB,QAASd,KAAKD,gBAEpCC,KAAKe,cACLf,KAAKgB,kBACP,CAEO,oBAAAC,GACejB,KAAKY,WAAWC,cAAc,gBACtCK,oBAAoB,QAASlB,KAAKG,2BAC9CH,KAAKkB,oBAAoB,QAASlB,KAAKD,gBACvCmB,oBAAoB,0BAA2BlB,KAAKK,6BACtD,CAEO,wBAAAc,CAAyBC,EAAcC,EAAkBC,GAC9DtB,KAAKQ,uBAEDR,KAAKuB,aAAwB,cAATH,GAAwBC,IAAaC,IAC1C,SAAbA,EACFtB,KAAKwB,QAAQC,WAAa,OACJ,SAAbJ,UACFrB,KAAKwB,QAAQC,YAIpBzB,KAAKuB,aAAwB,qBAATH,GAA+BC,IAAaC,IACjD,SAAbA,EACFtB,KAAK0B,OACiB,SAAbL,GACTrB,KAAK2B,SAIL3B,KAAKuB,aAAwB,eAATH,GAAyBC,IAAaC,GAC5DtB,KAAKe,cAGHf,KAAKuB,aAAwB,qBAATH,GAA+BC,IAAaC,GAClEtB,KAAKgB,kBAET,CAEQ,oBAAAR,GACDR,KAAKY,aACRZ,KAAK4B,aAAa,CAAEC,KAAM,SAC1B7B,KAAKY,WAAWkB,UAAY,ulCAEhC,CAEQ,WAAAf,GACN,MAAMgB,EAAQ/B,KAAKY,WAAWC,cAAc,MAC5CkB,EAAMC,YAAchC,KAAKwB,QAAQO,OAAS,GAC1CA,EAAME,QAAUjC,KAAKwB,QAAQO,MAIzB/B,KAAKwB,QAAQO,OACf/B,KAAKW,aAAa,aAAcX,KAAKwB,QAAQO,MAEjD,CAEQ,gBAAAf,GACN,MAAMkB,EAAclC,KAAKY,WAAWC,cAAc,gBAC9Cb,KAAKwB,QAAQW,WACfD,EAAYV,QAAQY,MAAQpC,KAAKwB,QAAQW,kBAElCD,EAAYV,QAAQY,KAE/B,CAEQ,IAAAV,GACNR,oBAAoB,0BAA2BlB,KAAKK,8BACpDS,iBAAiB,0BAA2Bd,KAAKK,8BAEjD,MAAMgC,EAAY,IAAIC,MAAM,uBAAwB,CAAEC,SAAS,EAAMC,UAAU,IAC/ExC,KAAKyC,cAAcJ,GAECrC,KAAKY,WAAWC,cAAc,gBACtC6B,MAAM,CAAEC,cAAc,GACpC,CAEQ,KAAAhB,GACNT,oBAAoB,0BAA2BlB,KAAKK,8BAEpD,MAAMuC,EAAa,IAAIN,MAAM,wBAAyB,CAAEC,SAAS,EAAMC,UAAU,IACjFxC,KAAKyC,cAAcG,EACrB,CAEQ,SAAA3C,CAAU4C,GAChB,GAAIA,EAAMC,kBAAkBC,QAAS,CACnC,MAAMC,EAASH,EAAMC,OAAOG,QAAQ,sBAChCD,GAAUhD,KAAKkD,SAASF,WACnBhD,KAAKwB,QAAQE,IAExB,CACF,CAEQ,oBAAAtB,CAAqB+C,UACpBnD,KAAKwB,QAAQE,IACtB,CAEQ,uBAAApB,CAAwB6C,UACvBnD,KAAKwB,QAAQE,IACtB,EASG0B,eAAeC,IAAI,kBACtBD,eAAeE,OAAO,gBAAiB1D","sources":["webpack://lange/./snippets/modal-element/index.ts"],"sourcesContent":["import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * The centered box our own modals open in, with the close button in its corner. A modal renders\n * `<modal-element>` around its content and passes the heading text in `data-title`. It stays hidden\n * until the code using it sets `data-open=\"true\"`, and it closes when that attribute goes away,\n * whether the code using it removes it or the element does on its own. A click on anything inside\n * the modal marked `data-modal-close`, such as a \"Continue shopping\" button, closes it too.\n *\n * The element shows and hides itself off `data-modal-state` rather than `data-open`. For now it\n * sets that attribute itself whenever `data-open` changes. Later the modal manager sets it instead.\n *\n * It exists so each modal stops building its own box, close button, dialog role, and open and close\n * handling. It doesn't own the page behind it. On open it dispatches `modal-open-requested`, and on\n * close `modal-close-requested`, and the modal manager takes care of the backdrop, scroll locking,\n * making the rest of the page inert, and putting focus back on the opener. It closes itself on the\n * manager's `modal-dismiss-requested`, which covers a backdrop click and the escape key.\n */\nclass ModalElement extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-close-label', 'data-modal-state', 'data-open', 'data-title'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Present when the content should fill the whole modal edge to edge, with no box and no\n     * heading, and the close button drawn over its top right corner. The value is ignored. Only the\n     * stylesheet reads it.\n     */\n    bare: string;\n\n    /**\n     * The accessible name of the close button. The close button's own default applies when this is\n     * left off.\n     */\n    closeLabel: string;\n\n    /**\n     * Whether the modal is shown, as \"open\". Anything else, or no value at all, means closed.\n     *\n     * A change only takes effect while the element is on the page. A change made while it is\n     * detached is ignored, and connecting it later does not pick it up. Markup that arrives with\n     * the attribute already set still works.\n     */\n    modalState: string;\n\n    /**\n     * Whether the modal is open, as \"true\". Anything else, or no value at all, means closed.\n     *\n     * A change only takes effect while the element is on the page. A change made while it is\n     * detached is ignored, and connecting it later does not pick it up. Markup that arrives with\n     * the attribute already set still works.\n     */\n    open: string;\n\n    /**\n     * Present when the content should scroll on its own under a header that stays put, for a modal\n     * tall enough to scroll. The value is ignored. Only the stylesheet reads it.\n     */\n    scrollBody: string;\n\n    /**\n     * The text of the modal's heading, which also becomes its accessible name. A modal with no\n     * title shows no heading and names itself with its own aria-label.\n     */\n    title: string;\n  };\n\n  private onClickedBound = this.onClicked.bind(this);\n  private onCloseButtonClickedBound = this.onCloseButtonClicked.bind(this);\n  private onModalDismissRequestedBound = this.onModalDismissRequested.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    if (!this.hasAttribute('role')) {\n      this.role = 'dialog';\n    }\n\n    if (!this.hasAttribute('aria-modal')) {\n      this.setAttribute('aria-modal', 'true');\n    }\n\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton.addEventListener('click', this.onCloseButtonClickedBound);\n\n    this.addEventListener('click', this.onClickedBound);\n\n    this.renderTitle();\n    this.renderCloseLabel();\n  }\n\n  public disconnectedCallback() {\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton.removeEventListener('click', this.onCloseButtonClickedBound);\n    this.removeEventListener('click', this.onClickedBound);\n    removeEventListener('modal-dismiss-requested', this.onModalDismissRequestedBound);\n  }\n\n  public attributeChangedCallback(name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && name === 'data-open' && oldValue !== newValue) {\n      if (newValue === 'true') {\n        this.dataset.modalState = 'open';\n      } else if (oldValue === 'true') {\n        delete this.dataset.modalState;\n      }\n    }\n\n    if (this.isConnected && name === 'data-modal-state' && oldValue !== newValue) {\n      if (newValue === 'open') {\n        this.open();\n      } else if (oldValue === 'open') {\n        this.close();\n      }\n    }\n\n    if (this.isConnected && name === 'data-title' && oldValue !== newValue) {\n      this.renderTitle();\n    }\n\n    if (this.isConnected && name === 'data-close-label' && oldValue !== newValue) {\n      this.renderCloseLabel();\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 renderTitle() {\n    const title = this.shadowRoot.querySelector('h2');\n    title.textContent = this.dataset.title || '';\n    title.hidden = !this.dataset.title;\n\n    // The dialog role sits on the host, and an aria-labelledby there can't point at the heading\n    // inside our shadow root, so we name the dialog with the same text directly.\n    if (this.dataset.title) {\n      this.setAttribute('aria-label', this.dataset.title);\n    }\n  }\n\n  private renderCloseLabel() {\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    if (this.dataset.closeLabel) {\n      closeButton.dataset.label = this.dataset.closeLabel;\n    } else {\n      delete closeButton.dataset.label;\n    }\n  }\n\n  private open() {\n    removeEventListener('modal-dismiss-requested', this.onModalDismissRequestedBound);\n    addEventListener('modal-dismiss-requested', this.onModalDismissRequestedBound);\n\n    const showEvent = new Event('modal-open-requested', { bubbles: true, composed: true });\n    this.dispatchEvent(showEvent);\n\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton.focus({ focusVisible: true });\n  }\n\n  private close() {\n    removeEventListener('modal-dismiss-requested', this.onModalDismissRequestedBound);\n\n    const closeEvent = new Event('modal-close-requested', { bubbles: true, composed: true });\n    this.dispatchEvent(closeEvent);\n  }\n\n  private onClicked(event: MouseEvent) {\n    if (event.target instanceof Element) {\n      const closer = event.target.closest('[data-modal-close]');\n      if (closer && this.contains(closer)) {\n        delete this.dataset.open;\n      }\n    }\n  }\n\n  private onCloseButtonClicked(_event: MouseEvent) {\n    delete this.dataset.open;\n  }\n\n  private onModalDismissRequested(_event: WindowEventMap['modal-dismiss-requested']) {\n    delete this.dataset.open;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'modal-element': ModalElement;\n  }\n}\n\nif (!customElements.get('modal-element')) {\n  customElements.define('modal-element', ModalElement);\n}\n"],"names":["ModalElement","HTMLElement","observedAttributes","onClickedBound","this","onClicked","bind","onCloseButtonClickedBound","onCloseButtonClicked","onModalDismissRequestedBound","onModalDismissRequested","connectedCallback","initializeShadowRoot","hasAttribute","role","setAttribute","shadowRoot","querySelector","addEventListener","renderTitle","renderCloseLabel","disconnectedCallback","removeEventListener","attributeChangedCallback","name","oldValue","newValue","isConnected","dataset","modalState","open","close","attachShadow","mode","innerHTML","title","textContent","hidden","closeButton","closeLabel","label","showEvent","Event","bubbles","composed","dispatchEvent","focus","focusVisible","closeEvent","event","target","Element","closer","closest","contains","_event","customElements","get","define"],"sourceRoot":""}