UNPKG

@dialpad/dialtone

Version:

Dialpad's Dialtone design system monorepo

326 lines (325 loc) 10.9 kB
import { DtIconClose } from "@dialpad/dialtone-icons/vue2"; import Modal from "../../common/mixins/modal.js"; import { MODAL_KIND_MODIFIERS, MODAL_SIZE_MODIFIERS, MODAL_BANNER_KINDS } from "./modal_constants.js"; import { getUniqueString, disableRootScrolling, enableRootScrolling } from "../../common/utils.js"; import { EVENT_KEYNAMES } from "../../common/constants.js"; import SrOnlyCloseButtonMixin from "../../common/mixins/sr_only_close_button.js"; import SrOnlyCloseButton from "../../common/sr_only_close_button.vue.js"; import normalizeComponent from "../../_virtual/_plugin-vue2_normalizer.js"; import DtLazyShow from "../lazy_show/lazy_show.vue.js"; import DtButton from "../button/button.vue.js"; import { NOTICE_KINDS } from "../notice/notice_constants.js"; const _sfc_main = { name: "DtModal", components: { DtLazyShow, DtButton, DtIconClose, SrOnlyCloseButton }, mixins: [Modal, SrOnlyCloseButtonMixin], props: { /** * A set of props to be passed into the modal's close button. * Requires an 'ariaLabel' property. */ closeButtonProps: { type: Object, required: true, validator: (props) => { return !!props.ariaLabel; } }, /** * Body text to display as the modal's main content. */ copy: { type: String, default: "" }, /** * Id to use for the dialog's aria-describedby. * Recommended only if the dialog content itself isn't enough to give full context, * as screen readers should recite the dialog contents by default before any aria-description. */ describedById: { type: String, default: "" }, /** * Id to use for the dialog's aria-labelledby. */ labelledById: { type: String, default: function() { return getUniqueString(); } }, /** * Whether the modal should be shown. * Parent component can sync on this value to control the modal's visibility. * @values true, false */ show: { type: Boolean, default: false }, /** * Title text to display in the modal header. */ title: { type: String, default: "" }, /** * Title text to display in the modal banner. */ bannerTitle: { type: String, default: "" }, /** * The theme of the modal. kind - default or danger, * @values default, danger */ kind: { type: String, default: "default", validator: (k) => Object.keys(MODAL_KIND_MODIFIERS).includes(k) }, /** * The size of the modal. size - default or full, * @values default, full */ size: { type: String, default: "default", validator: (s) => Object.keys(MODAL_SIZE_MODIFIERS).includes(s) }, /** * Additional class name for the root modal element. * Can accept String, Object, and Array, i.e. has the * same API as Vue's built-in handling of the class attribute. */ modalClass: { type: [String, Object, Array], default: "" }, /** * Additional class name for the dialog element within the modal. * Can accept String, Object, and Array, i.e. has the * same API as Vue's built-in handling of the class attribute. */ dialogClass: { type: [String, Object, Array], default: "" }, /** * Additional class name for the content element within the modal. * Can accept String, Object, and Array, i.e. has the * same API as Vue's built-in handling of the class attribute. */ contentClass: { type: [String, Object, Array], default: "" }, /** * Sets the color of the banner. * @values base, error, info, success, warning */ bannerKind: { type: String, default: "warning", validate(kind) { return NOTICE_KINDS.includes(kind); } }, /** * Additional class name for the banner element within the modal. * Can accept String, Object, and Array, i.e. has the * same API as Vue's built-in handling of the class attribute. */ bannerClass: { type: [String, Object, Array], default: "" }, /** * Hides the close button on the modal * @values true, false */ hideClose: { type: Boolean, default: false }, /** * Whether the modal will close when you click outside of the dialog on the overlay. * @values true, false */ closeOnClick: { type: Boolean, default: true }, /** * Scrollable modal that allows scroll the modal content keeping the header and footer fixed * @values true, false */ fixedHeaderFooter: { type: Boolean, default: true }, /** * The element that is focused when the modal is opened. This can be an * HTMLElement within the modal, a string starting with '#' which will * find the element by ID. 'first' which will automatically focus * the first element, or 'dialog' which will focus the dialog window itself. * If the dialog is modal this prop cannot be 'none'. */ initialFocusElement: { type: [String, HTMLElement], default: "first", validator: (initialFocusElement) => { return initialFocusElement === "first" || initialFocusElement instanceof HTMLElement || initialFocusElement.startsWith("#"); } } }, emits: [ /** * The modal will emit a "false" boolean value for this event when the user performs a modal-closing action. * Parent components can sync on this value to create a 2-way binding to control modal visibility. * * @event update:show * @type {Boolean} */ "update:show" ], data() { return { MODAL_KIND_MODIFIERS, MODAL_SIZE_MODIFIERS, MODAL_BANNER_KINDS, EVENT_KEYNAMES }; }, computed: { modalListeners() { return { ...this.$listeners, click: (event) => { if (!this.closeOnClick) return; event.target === event.currentTarget && this.close(); this.$emit("click", event); }, keydown: (event) => { switch (event.code) { case EVENT_KEYNAMES.esc: case EVENT_KEYNAMES.escape: this.close(); break; case EVENT_KEYNAMES.tab: this.trapFocus(event); break; } this.$emit("keydown", event); }, "after-enter": (event) => { this.$emit("update:show", true); event.target === event.currentTarget && this.setFocusAfterTransition(); } }; }, open() { return `${!this.show}`; }, hasFooterSlot() { return !!this.$slots.footer; }, bannerKindClass() { return MODAL_BANNER_KINDS[this.bannerKind]; } }, watch: { show: { handler(isShowing) { var _a; if (isShowing) { this.previousActiveElement = document.activeElement; disableRootScrolling(this.$el.getRootNode().host); } else { enableRootScrolling(this.$el.getRootNode().host); (_a = this.previousActiveElement) == null ? void 0 : _a.focus(); this.previousActiveElement = null; } } }, $props: { immediate: true, deep: true, handler() { this.validateProps(); } } }, methods: { close() { this.$emit("update:show", false); }, setFocusAfterTransition() { if (this.initialFocusElement === "first") { this.focusFirstElement(); } else if (this.initialFocusElement.startsWith("#")) { this.focusElementById(this.initialFocusElement); } else if (this.initialFocusElement instanceof HTMLElement) { this.initialFocusElement.focus(); } }, trapFocus(e) { if (this.show) { this.focusTrappedTabPress(e); } }, validateProps() { if (this.hideClose && !this.visuallyHiddenClose) { console.error(`If hideClose prop is true, visuallyHiddenClose and visuallyHiddenCloseLabel props need to be set so the component always includes a close button`); } } } }; var _sfc_render = function render() { var _vm = this, _c = _vm._self._c; return _c("dt-lazy-show", _vm._g({ class: [ "d-modal", _vm.MODAL_KIND_MODIFIERS[_vm.kind], _vm.MODAL_SIZE_MODIFIERS[_vm.size], _vm.modalClass ], attrs: { "transition": "d-zoom", "show": _vm.show, "data-qa": "dt-modal", "aria-hidden": _vm.open } }, _vm.modalListeners), [_vm.show && (_vm.$slots.banner || _vm.bannerTitle) ? _c("div", { class: [ "d-modal__banner", _vm.bannerClass, _vm.bannerKindClass ], attrs: { "data-qa": "dt-modal-banner" } }, [_vm._t("banner", function() { return [_vm._v(" " + _vm._s(_vm.bannerTitle) + " ")]; })], 2) : _vm._e(), _c("transition", { attrs: { "appear": "", "name": "d-modal__dialog" } }, [_c("div", { directives: [{ name: "show", rawName: "v-show", value: _vm.show, expression: "show" }], class: [ "d-modal__dialog", { "d-modal__dialog--scrollable": _vm.fixedHeaderFooter }, _vm.dialogClass ], attrs: { "role": "dialog", "aria-modal": "true", "aria-describedby": _vm.describedById, "aria-labelledby": _vm.labelledById } }, [_vm.$slots.header ? _c("div", { staticClass: "d-modal__header", attrs: { "id": _vm.labelledById, "data-qa": "dt-modal-title" } }, [_vm._t("header")], 2) : _c("h2", { staticClass: "d-modal__header", attrs: { "id": _vm.labelledById, "data-qa": "dt-modal-title" } }, [_vm._v(" " + _vm._s(_vm.title) + " ")]), _vm.$slots.default ? _c("div", { class: [ "d-modal__content", _vm.contentClass ], attrs: { "data-qa": "dt-modal-copy" } }, [_vm._t("default")], 2) : _c("p", { class: [ "d-modal__content", _vm.contentClass ], attrs: { "data-qa": "dt-modal-copy" } }, [_vm._v(" " + _vm._s(_vm.copy) + " ")]), _vm.hasFooterSlot ? _c("footer", { staticClass: "d-modal__footer" }, [_vm._t("footer")], 2) : _vm._e(), !_vm.hideClose ? _c("dt-button", _vm._b({ staticClass: "d-modal__close", attrs: { "circle": "", "size": "lg", "importance": "clear", "aria-label": _vm.closeButtonProps.ariaLabel }, on: { "click": _vm.close }, scopedSlots: _vm._u([{ key: "icon", fn: function() { return [_c("dt-icon-close", { attrs: { "size": "400" } })]; }, proxy: true }], null, false, 4008927507) }, "dt-button", _vm.closeButtonProps, false)) : _vm._e(), _vm.showVisuallyHiddenClose ? _c("sr-only-close-button", { attrs: { "visually-hidden-close-label": _vm.visuallyHiddenCloseLabel }, on: { "close": _vm.close } }) : _vm._e()], 1)])], 1); }; var _sfc_staticRenderFns = []; var __component__ = /* @__PURE__ */ normalizeComponent( _sfc_main, _sfc_render, _sfc_staticRenderFns ); const modal = __component__.exports; export { modal as default }; //# sourceMappingURL=modal.vue.js.map