UNPKG

@dialpad/dialtone

Version:

Dialpad's Dialtone design system monorepo

1 lines 16.2 kB
{"version":3,"file":"modal.vue.cjs","sources":["../../../components/modal/modal.vue"],"sourcesContent":["<template>\n <dt-lazy-show\n transition=\"d-zoom\"\n :show=\"show\"\n :class=\"[\n 'd-modal',\n MODAL_KIND_MODIFIERS[kind],\n MODAL_SIZE_MODIFIERS[size],\n modalClass,\n ]\"\n data-qa=\"dt-modal\"\n :aria-hidden=\"open\"\n v-on=\"modalListeners\"\n >\n <div\n v-if=\"show && ($slots.banner || bannerTitle)\"\n data-qa=\"dt-modal-banner\"\n :class=\"[\n 'd-modal__banner',\n bannerClass,\n bannerKindClass,\n ]\"\n >\n <!-- @slot Slot for the banner, defaults to bannerTitle prop -->\n <slot name=\"banner\">\n {{ bannerTitle }}\n </slot>\n </div>\n <transition\n appear\n name=\"d-modal__dialog\"\n >\n <div\n v-show=\"show\"\n :class=\"[\n 'd-modal__dialog',\n { 'd-modal__dialog--scrollable': fixedHeaderFooter },\n dialogClass,\n ]\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-describedby=\"describedById\"\n :aria-labelledby=\"labelledById\"\n >\n <div\n v-if=\"$slots.header\"\n :id=\"labelledById\"\n class=\"d-modal__header\"\n data-qa=\"dt-modal-title\"\n >\n <!-- @slot Slot for dialog header section, taking the place of any \"title\" text prop -->\n <slot name=\"header\" />\n </div>\n <h2\n v-else\n :id=\"labelledById\"\n class=\"d-modal__header\"\n data-qa=\"dt-modal-title\"\n >\n {{ title }}\n </h2>\n <div\n v-if=\"$slots.default\"\n :class=\"[\n 'd-modal__content',\n contentClass,\n ]\"\n data-qa=\"dt-modal-copy\"\n >\n <!-- @slot Default slot for dialog body section, taking the place of any \"copy\" text prop -->\n <slot />\n </div>\n <p\n v-else\n :class=\"[\n 'd-modal__content',\n contentClass,\n ]\"\n data-qa=\"dt-modal-copy\"\n >\n {{ copy }}\n </p>\n <footer\n v-if=\"hasFooterSlot\"\n class=\"d-modal__footer\"\n >\n <!-- @slot Slot for dialog footer content, often containing cancel and confirm buttons. -->\n <slot name=\"footer\" />\n </footer>\n <dt-button\n v-if=\"!hideClose\"\n class=\"d-modal__close\"\n circle\n size=\"lg\"\n importance=\"clear\"\n :aria-label=\"closeButtonProps.ariaLabel\"\n v-bind=\"closeButtonProps\"\n @click=\"close\"\n >\n <template #icon>\n <dt-icon-close\n size=\"400\"\n />\n </template>\n </dt-button>\n <sr-only-close-button\n v-if=\"showVisuallyHiddenClose\"\n :visually-hidden-close-label=\"visuallyHiddenCloseLabel\"\n @close=\"close\"\n />\n </div>\n </transition>\n </dt-lazy-show>\n</template>\n\n<script>\n/* eslint-disable max-lines */\nimport { DtButton } from '@/components/button';\nimport { DtIconClose } from '@dialpad/dialtone-icons/vue2';\nimport Modal from '@/common/mixins/modal';\nimport {\n MODAL_BANNER_KINDS,\n MODAL_KIND_MODIFIERS,\n MODAL_SIZE_MODIFIERS,\n} from './modal_constants';\nimport { getUniqueString, disableRootScrolling, enableRootScrolling } from '@/common/utils';\nimport { DtLazyShow } from '@/components/lazy_show';\nimport { EVENT_KEYNAMES } from '@/common/constants';\nimport SrOnlyCloseButtonMixin from '@/common/mixins/sr_only_close_button';\nimport SrOnlyCloseButton from '@/common/sr_only_close_button.vue';\nimport { NOTICE_KINDS } from '@/components/notice';\n\n/**\n * Modals focus the user’s attention exclusively on one task or piece of information\n * via a window that sits on top of the page content.\n * @see https://dialtone.dialpad.com/components/modal.html\n */\nexport default {\n name: 'DtModal',\n\n components: {\n DtLazyShow,\n DtButton,\n DtIconClose,\n SrOnlyCloseButton,\n },\n\n mixins: [Modal, SrOnlyCloseButtonMixin],\n\n props: {\n /**\n * A set of props to be passed into the modal's close button.\n * Requires an 'ariaLabel' property.\n */\n closeButtonProps: {\n type: Object,\n required: true,\n validator: (props) => {\n return !!props.ariaLabel;\n },\n },\n\n /**\n * Body text to display as the modal's main content.\n */\n copy: {\n type: String,\n default: '',\n },\n\n /**\n * Id to use for the dialog's aria-describedby.\n * Recommended only if the dialog content itself isn't enough to give full context,\n * as screen readers should recite the dialog contents by default before any aria-description.\n */\n describedById: {\n type: String,\n default: '',\n },\n\n /**\n * Id to use for the dialog's aria-labelledby.\n */\n labelledById: {\n type: String,\n default: function () { return getUniqueString(); },\n },\n\n /**\n * Whether the modal should be shown.\n * Parent component can sync on this value to control the modal's visibility.\n * @values true, false\n */\n show: {\n type: Boolean,\n default: false,\n },\n\n /**\n * Title text to display in the modal header.\n */\n title: {\n type: String,\n default: '',\n },\n\n /**\n * Title text to display in the modal banner.\n */\n bannerTitle: {\n type: String,\n default: '',\n },\n\n /**\n * The theme of the modal. kind - default or danger,\n * @values default, danger\n */\n kind: {\n type: String,\n default: 'default',\n validator: (k) => Object.keys(MODAL_KIND_MODIFIERS).includes(k),\n },\n\n /**\n * The size of the modal. size - default or full,\n * @values default, full\n */\n size: {\n type: String,\n default: 'default',\n validator: (s) => Object.keys(MODAL_SIZE_MODIFIERS).includes(s),\n },\n\n /**\n * Additional class name for the root modal element.\n * Can accept String, Object, and Array, i.e. has the\n * same API as Vue's built-in handling of the class attribute.\n */\n modalClass: {\n type: [String, Object, Array],\n default: '',\n },\n\n /**\n * Additional class name for the dialog element within the modal.\n * Can accept String, Object, and Array, i.e. has the\n * same API as Vue's built-in handling of the class attribute.\n */\n dialogClass: {\n type: [String, Object, Array],\n default: '',\n },\n\n /**\n * Additional class name for the content element within the modal.\n * Can accept String, Object, and Array, i.e. has the\n * same API as Vue's built-in handling of the class attribute.\n */\n contentClass: {\n type: [String, Object, Array],\n default: '',\n },\n\n /**\n * Sets the color of the banner.\n * @values base, error, info, success, warning\n */\n bannerKind: {\n type: String,\n default: 'warning',\n validate (kind) {\n return NOTICE_KINDS.includes(kind);\n },\n },\n\n /**\n * Additional class name for the banner element within the modal.\n * Can accept String, Object, and Array, i.e. has the\n * same API as Vue's built-in handling of the class attribute.\n */\n bannerClass: {\n type: [String, Object, Array],\n default: '',\n },\n\n /**\n * Hides the close button on the modal\n * @values true, false\n */\n hideClose: {\n type: Boolean,\n default: false,\n },\n\n /**\n * Whether the modal will close when you click outside of the dialog on the overlay.\n * @values true, false\n */\n closeOnClick: {\n type: Boolean,\n default: true,\n },\n\n /**\n * Scrollable modal that allows scroll the modal content keeping the header and footer fixed\n * @values true, false\n */\n fixedHeaderFooter: {\n type: Boolean,\n default: true,\n },\n\n /**\n * The element that is focused when the modal is opened. This can be an\n * HTMLElement within the modal, a string starting with '#' which will\n * find the element by ID. 'first' which will automatically focus\n * the first element, or 'dialog' which will focus the dialog window itself.\n * If the dialog is modal this prop cannot be 'none'.\n */\n initialFocusElement: {\n type: [String, HTMLElement],\n default: 'first',\n validator: initialFocusElement => {\n return initialFocusElement === 'first' ||\n (initialFocusElement instanceof HTMLElement) ||\n initialFocusElement.startsWith('#');\n },\n },\n },\n\n emits: [\n /**\n * The modal will emit a \"false\" boolean value for this event when the user performs a modal-closing action.\n * Parent components can sync on this value to create a 2-way binding to control modal visibility.\n *\n * @event update:show\n * @type {Boolean}\n */\n 'update:show',\n ],\n\n data () {\n return {\n MODAL_KIND_MODIFIERS,\n MODAL_SIZE_MODIFIERS,\n MODAL_BANNER_KINDS,\n EVENT_KEYNAMES,\n };\n },\n\n computed: {\n modalListeners () {\n return {\n ...this.$listeners,\n\n click: event => {\n if (!this.closeOnClick) return;\n (event.target === event.currentTarget) && this.close();\n this.$emit('click', event);\n },\n\n keydown: event => {\n switch (event.code) {\n case EVENT_KEYNAMES.esc:\n case EVENT_KEYNAMES.escape:\n this.close();\n break;\n case EVENT_KEYNAMES.tab:\n this.trapFocus(event);\n break;\n }\n this.$emit('keydown', event);\n },\n\n 'after-enter': event => {\n this.$emit('update:show', true);\n (event.target === event.currentTarget) && this.setFocusAfterTransition();\n },\n };\n },\n\n open () {\n return `${!this.show}`;\n },\n\n hasFooterSlot () {\n return !!this.$slots.footer;\n },\n\n bannerKindClass () {\n return MODAL_BANNER_KINDS[this.bannerKind];\n },\n },\n\n watch: {\n show: {\n handler (isShowing) {\n if (isShowing) {\n // Set a reference to the previously-active element, to which we'll return focus on modal close.\n this.previousActiveElement = document.activeElement;\n disableRootScrolling(this.$el.getRootNode().host);\n } else {\n enableRootScrolling(this.$el.getRootNode().host);\n // Modal is being hidden, so return focus to the previously active element before clearing the reference.\n this.previousActiveElement?.focus();\n this.previousActiveElement = null;\n }\n },\n },\n\n $props: {\n immediate: true,\n deep: true,\n handler () {\n this.validateProps();\n },\n },\n },\n\n methods: {\n close () {\n this.$emit('update:show', false);\n },\n\n setFocusAfterTransition () {\n if (this.initialFocusElement === 'first') {\n this.focusFirstElement();\n } else if (this.initialFocusElement.startsWith('#')) {\n this.focusElementById(this.initialFocusElement);\n } else if (this.initialFocusElement instanceof HTMLElement) {\n this.initialFocusElement.focus();\n }\n },\n\n trapFocus (e) {\n if (this.show) {\n this.focusTrappedTabPress(e);\n }\n },\n\n validateProps () {\n if (this.hideClose && !this.visuallyHiddenClose) {\n console.error(`If hideClose prop is true, visuallyHiddenClose and visuallyHiddenCloseLabel props\n need to be set so the component always includes a close button`);\n }\n },\n },\n};\n</script>\n"],"names":["DtLazyShow","DtButton","DtIconClose","SrOnlyCloseButton","Modal","SrOnlyCloseButtonMixin","getUniqueString","MODAL_KIND_MODIFIERS","MODAL_SIZE_MODIFIERS","NOTICE_KINDS","MODAL_BANNER_KINDS","EVENT_KEYNAMES","disableRootScrolling","enableRootScrolling"],"mappings":"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"}