{"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. With `data-slide-from`, it's a drawer that slides in from an edge of the\n * screen instead of a centered box.\n *\n * The element shows and hides itself off `data-modal-state` rather than `data-open`. When\n * `data-open` changes, it asks the modal manager to open or close it, and the manager sets\n * `data-modal-state`.\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. The modal manager takes care of the backdrop, scroll\n * locking, making the rest of the page inert, and putting focus back on the opener. It closes\n * itself on the manager's `modal-dismiss-requested`, which covers a backdrop click and the escape\n * key.\n */\nclass ModalElement extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-close-label', 'data-modal-state', 'data-open', 'data-subtitle', '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     * Present when the modal should draw a line under its header. The value is ignored. Only the\n     * stylesheet reads it.\n     */\n    headerDivider: string;\n\n    /**\n     * Whether the modal is shown, as \"open\". Anything else, or no value at all, means closed. The\n     * modal manager sets this, so code using the element sets `data-open` instead.\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.\n     */\n    modalState: string;\n\n    /**\n     * Whether the modal should be 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.\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 edge of the screen the modal docks to as a drawer, as \"right\" or \"left\". It slides in\n     * from that edge when it opens and back out when it closes. With this left off, the modal is\n     * the usual centered box.\n     *\n     * A drawer stays on the page while closed, pushed off the edge, so the element makes it inert\n     * then to keep it out of reach of Tab and clicks.\n     */\n    slideFrom: string;\n\n    /**\n     * A line of text shown in the header under the title, which also becomes the modal's accessible\n     * description. A modal with no subtitle shows nothing there.\n     */\n    subtitle: 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.renderSubtitle();\n    this.renderCloseLabel();\n    this.renderInert();\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.sendOpenRequest();\n      } else if (oldValue === 'true') {\n        this.sendCloseRequest();\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-subtitle' && oldValue !== newValue) {\n      this.renderSubtitle();\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  /**\n   * Like the title, an aria-describedby on the host can't point at the subtitle inside our shadow\n   * root, so we describe the dialog with the same text directly.\n   */\n  private renderSubtitle() {\n    const subtitle = this.shadowRoot.querySelector<HTMLElement>('#subtitle');\n    subtitle.textContent = this.dataset.subtitle || '';\n    subtitle.hidden = !this.dataset.subtitle;\n\n    if (this.dataset.subtitle) {\n      this.setAttribute('aria-description', this.dataset.subtitle);\n    } else {\n      this.removeAttribute('aria-description');\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  /**\n   * Makes a closed drawer inert. A closed centered modal is hidden with display none, which already\n   * keeps it out of reach, so it's left alone.\n   */\n  private renderInert() {\n    if (this.dataset.slideFrom !== undefined) {\n      this.inert = this.dataset.modalState !== 'open';\n    }\n  }\n\n  private open() {\n    this.renderInert();\n    removeEventListener('modal-dismiss-requested', this.onModalDismissRequestedBound);\n    addEventListener('modal-dismiss-requested', this.onModalDismissRequestedBound);\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    this.renderInert();\n  }\n\n  /**\n   * Asks the modal manager to open the modal. The manager sets our data-modal-state, which shows\n   * it.\n   */\n  private sendOpenRequest() {\n    if (this.dataset.modalState === 'open') {\n      return;\n    }\n\n    const event = new Event('modal-open-requested', { bubbles: true, composed: true });\n    this.dispatchEvent(event);\n  }\n\n  private sendCloseRequest() {\n    if (this.dataset.modalState !== 'open') {\n      return;\n    }\n\n    const event = new Event('modal-close-requested', { bubbles: true, composed: true });\n    this.dispatchEvent(event);\n  }\n\n  /**\n   * Closes the modal from inside. Removing `data-open` closes it through its own change, the same\n   * way the code using it would.\n   */\n  private requestClose() {\n    delete this.dataset.open;\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","renderSubtitle","renderCloseLabel","renderInert","disconnectedCallback","removeEventListener","attributeChangedCallback","name","oldValue","newValue","isConnected","sendOpenRequest","sendCloseRequest","open","close","attachShadow","mode","innerHTML","title","textContent","dataset","hidden","subtitle","removeAttribute","closeButton","closeLabel","label","undefined","slideFrom","inert","modalState","focusCloseControl","back","backButton","focus","focusVisible","activeElement","event","Event","bubbles","composed","dispatchEvent","requestClose","target","Element","closer","closest","contains","_event","customElements","get","define"],"sourceRoot":""}