{"version":3,"file":"modal-element-snippet.min.js","mappings":"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","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. Content\n * marked `slot=\"footer\"` renders below the rest, and with `data-scroll-body` it stays put under the\n * part that scrolls.\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. Code using it can also skip `data-open`\n * and set `data-modal-state=\"open\"` directly. Later the modal manager sets it instead. When the\n * element closes itself, it removes both attributes, so it closes either way it was opened.\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 modal should show a \"Back\" button at the top left on a phone, below 768px,\n     * in place of the close button. From 768px up it keeps the close button. The value is ignored.\n     */\n    back: string;\n\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     * Present when the modal should fill the whole screen on a phone, below 768px. From 768px up it\n     * stays the usual centered box. The value is ignored. Only the stylesheet reads it.\n     */\n    fullScreen: 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 onBackButtonClickedBound = this.onBackButtonClicked.bind(this);\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    const backButton = this.shadowRoot.querySelector<HTMLButtonElement>('#back');\n    backButton.addEventListener('click', this.onBackButtonClickedBound);\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    const backButton = this.shadowRoot.querySelector<HTMLButtonElement>('#back');\n    backButton.removeEventListener('click', this.onBackButtonClickedBound);\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    this.focusCloseControl();\n  }\n\n  /**\n   * Focuses whichever of the back button and the close button is showing. The stylesheet picks\n   * which one shows, and focusing a hidden button does nothing, so we try the back button first and\n   * fall back to the close button when focus didn't land on it.\n   */\n  private focusCloseControl() {\n    if (this.dataset.back !== undefined) {\n      const backButton = this.shadowRoot.querySelector<HTMLButtonElement>('#back');\n      backButton.focus({ focusVisible: true });\n      if (this.shadowRoot.activeElement === backButton) {\n        return;\n      }\n    }\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  /**\n   * Closes the modal from inside, whichever attribute opened it. Removing `data-open` first lets a\n   * modal opened that way close through its own change, and removing `data-modal-state` after\n   * covers a modal opened by setting it directly.\n   */\n  private requestClose() {\n    delete this.dataset.open;\n    delete this.dataset.modalState;\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        this.requestClose();\n      }\n    }\n  }\n\n  private onBackButtonClicked(_event: MouseEvent) {\n    this.requestClose();\n  }\n\n  private onCloseButtonClicked(_event: MouseEvent) {\n    this.requestClose();\n  }\n\n  private onModalDismissRequested(_event: WindowEventMap['modal-dismiss-requested']) {\n    this.requestClose();\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","onBackButtonClickedBound","this","onBackButtonClicked","bind","onClickedBound","onClicked","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","focusCloseControl","undefined","back","backButton","focus","focusVisible","activeElement","closeEvent","requestClose","event","target","Element","closer","closest","contains","_event","customElements","get","define"],"sourceRoot":""}