{"version":3,"file":"modal-element-snippet.min.js","mappings":"wZAoBA,MAAMA,qBAAqBC,YACzB,6BAAkBC,GAChB,MAAO,CAAC,mBAAoB,mBAAoB,YAAa,aAC/D,CAuCQC,eAAiBC,KAAKC,UAAUC,KAAKF,MACrCG,0BAA4BH,KAAKI,qBAAqBF,KAAKF,MAC3DK,6BAA+BL,KAAKM,wBAAwBJ,KAAKF,MAElE,iBAAAO,GACLP,KAAKQ,uBAEAR,KAAKS,aAAa,SACrBT,KAAKU,aAAa,OAAQ,UAGvBV,KAAKS,aAAa,eACrBT,KAAKU,aAAa,aAAc,QAGdV,KAAKW,WAAWC,cAAc,gBACtCC,iBAAiB,QAASb,KAAKG,2BAE3CH,KAAKa,iBAAiB,QAASb,KAAKD,gBAEpCC,KAAKc,cACLd,KAAKe,kBACP,CAEO,oBAAAC,GACehB,KAAKW,WAAWC,cAAc,gBACtCK,oBAAoB,QAASjB,KAAKG,2BAC9CH,KAAKiB,oBAAoB,QAASjB,KAAKD,gBACvCkB,oBAAoB,0BAA2BjB,KAAKK,6BACtD,CAEO,wBAAAa,CAAyBC,EAAcC,EAAkBC,GAC9DrB,KAAKQ,uBAEDR,KAAKsB,aAAwB,cAATH,GAAwBC,IAAaC,IAC1C,SAAbA,EACFrB,KAAKuB,QAAQC,WAAa,OACJ,SAAbJ,UACFpB,KAAKuB,QAAQC,YAIpBxB,KAAKsB,aAAwB,qBAATH,GAA+BC,IAAaC,IACjD,SAAbA,EACFrB,KAAKyB,OACiB,SAAbL,GACTpB,KAAK0B,SAIL1B,KAAKsB,aAAwB,eAATH,GAAyBC,IAAaC,GAC5DrB,KAAKc,cAGHd,KAAKsB,aAAwB,qBAATH,GAA+BC,IAAaC,GAClErB,KAAKe,kBAET,CAEQ,oBAAAP,GACDR,KAAKW,aACRX,KAAK2B,aAAa,CAAEC,KAAM,SAC1B5B,KAAKW,WAAWkB,UAAY,ulCAEhC,CAEQ,WAAAf,GACN,MAAMgB,EAAQ9B,KAAKW,WAAWC,cAAc,MAC5CkB,EAAMC,YAAc/B,KAAKuB,QAAQO,OAAS,GAC1CA,EAAME,QAAUhC,KAAKuB,QAAQO,MAIzB9B,KAAKuB,QAAQO,OACf9B,KAAKU,aAAa,aAAcV,KAAKuB,QAAQO,MAEjD,CAEQ,gBAAAf,GACN,MAAMkB,EAAcjC,KAAKW,WAAWC,cAAc,gBAC9CZ,KAAKuB,QAAQW,WACfD,EAAYV,QAAQY,MAAQnC,KAAKuB,QAAQW,kBAElCD,EAAYV,QAAQY,KAE/B,CAEQ,IAAAV,GACNR,oBAAoB,0BAA2BjB,KAAKK,8BACpDQ,iBAAiB,0BAA2Bb,KAAKK,8BAEjD,MAAM+B,EAAY,IAAIC,MAAM,uBAAwB,CAAEC,SAAS,EAAMC,UAAU,IAC/EvC,KAAKwC,cAAcJ,GAECpC,KAAKW,WAAWC,cAAc,gBACtC6B,MAAM,CAAEC,cAAc,GACpC,CAEQ,KAAAhB,GACNT,oBAAoB,0BAA2BjB,KAAKK,8BAEpD,MAAMsC,EAAa,IAAIN,MAAM,wBAAyB,CAAEC,SAAS,EAAMC,UAAU,IACjFvC,KAAKwC,cAAcG,EACrB,CAEQ,SAAA1C,CAAU2C,GAChB,GAAIA,EAAMC,kBAAkBC,QAAS,CACnC,MAAMC,EAASH,EAAMC,OAAOG,QAAQ,sBAChCD,GAAU/C,KAAKiD,SAASF,WACnB/C,KAAKuB,QAAQE,IAExB,CACF,CAEQ,oBAAArB,CAAqB8C,UACpBlD,KAAKuB,QAAQE,IACtB,CAEQ,uBAAAnB,CAAwB4C,UACvBlD,KAAKuB,QAAQE,IACtB,EASG0B,eAAeC,IAAI,kBACtBD,eAAeE,OAAO,gBAAiBzD","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    modalState: string;\n\n    /**\n     * Whether the modal is open, as \"true\". Anything else, or no value at all, means closed.\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.setAttribute('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","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":""}