UNPKG

@dialpad/dialtone

Version:

Dialpad's Dialtone design system monorepo

424 lines (423 loc) 13.6 kB
import { DtIconClose } from "@dialpad/dialtone-icons/vue3"; import Modal from "../../common/mixins/modal.js"; import { MODAL_KIND_MODIFIERS, MODAL_SIZE_MODIFIERS, MODAL_BANNER_KINDS } from "./modal_constants.js"; import { getUniqueString, hasSlotContent, disableRootScrolling, returnFirstEl, 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 { resolveComponent, openBlock, createBlock, mergeProps, toHandlers, withCtx, createElementBlock, normalizeClass, renderSlot, createTextVNode, toDisplayString, createCommentVNode, createVNode, Transition, withDirectives, createElementVNode, vShow } from "vue"; import _export_sfc from "../../_virtual/_plugin-vue_export-helper.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 = { compatConfig: { MODE: 3 }, 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: [ /** * Native button click event * * @event click * @type {PointerEvent | KeyboardEvent} */ "click", /** * Native keydown event * * @event keydown * @type {KeyboardEvent} */ "keydown", /** * 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, hasSlotContent }; }, computed: { modalListeners() { return { 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(returnFirstEl(this.$el).getRootNode().host); } else { enableRootScrolling(returnFirstEl(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`); } } } }; const _hoisted_1 = ["aria-describedby", "aria-labelledby"]; const _hoisted_2 = ["id"]; const _hoisted_3 = ["id"]; const _hoisted_4 = { key: 4, class: "d-modal__footer" }; function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { const _component_dt_icon_close = resolveComponent("dt-icon-close"); const _component_dt_button = resolveComponent("dt-button"); const _component_sr_only_close_button = resolveComponent("sr-only-close-button"); const _component_dt_lazy_show = resolveComponent("dt-lazy-show"); return openBlock(), createBlock(_component_dt_lazy_show, mergeProps({ transition: "d-zoom", show: $props.show, class: [ "d-modal", $data.MODAL_KIND_MODIFIERS[$props.kind], $data.MODAL_SIZE_MODIFIERS[$props.size], $props.modalClass ], "data-qa": "dt-modal", "aria-hidden": $options.open }, toHandlers($options.modalListeners)), { default: withCtx(() => [ $props.show && ($data.hasSlotContent(_ctx.$slots.banner) || $props.bannerTitle) ? (openBlock(), createElementBlock("div", { key: 0, "data-qa": "dt-modal-banner", class: normalizeClass([ "d-modal__banner", $props.bannerClass, $options.bannerKindClass ]) }, [ renderSlot(_ctx.$slots, "banner", {}, () => [ createTextVNode(toDisplayString($props.bannerTitle), 1) ]) ], 2)) : createCommentVNode("", true), createVNode(Transition, { appear: "", name: "d-modal__dialog" }, { default: withCtx(() => [ withDirectives(createElementVNode("div", { class: normalizeClass([ "d-modal__dialog", { "d-modal__dialog--scrollable": $props.fixedHeaderFooter }, $props.dialogClass ]), role: "dialog", "aria-modal": "true", "aria-describedby": $props.describedById, "aria-labelledby": $props.labelledById }, [ $data.hasSlotContent(_ctx.$slots.header) ? (openBlock(), createElementBlock("div", { key: 0, id: $props.labelledById, class: "d-modal__header", "data-qa": "dt-modal-title" }, [ renderSlot(_ctx.$slots, "header") ], 8, _hoisted_2)) : (openBlock(), createElementBlock("h2", { key: 1, id: $props.labelledById, class: "d-modal__header", "data-qa": "dt-modal-title" }, toDisplayString($props.title), 9, _hoisted_3)), $data.hasSlotContent(_ctx.$slots.default) ? (openBlock(), createElementBlock("div", { key: 2, class: normalizeClass([ "d-modal__content", $props.contentClass ]), "data-qa": "dt-modal-copy" }, [ renderSlot(_ctx.$slots, "default") ], 2)) : (openBlock(), createElementBlock("p", { key: 3, class: normalizeClass([ "d-modal__content", $props.contentClass ]), "data-qa": "dt-modal-copy" }, toDisplayString($props.copy), 3)), $options.hasFooterSlot ? (openBlock(), createElementBlock("footer", _hoisted_4, [ renderSlot(_ctx.$slots, "footer") ])) : createCommentVNode("", true), !$props.hideClose ? (openBlock(), createBlock(_component_dt_button, mergeProps({ key: 5, class: "d-modal__close", circle: "", size: "lg", importance: "clear", "aria-label": $props.closeButtonProps.ariaLabel }, $props.closeButtonProps, { onClick: $options.close }), { icon: withCtx(() => [ createVNode(_component_dt_icon_close, { size: "400" }) ]), _: 1 }, 16, ["aria-label", "onClick"])) : createCommentVNode("", true), _ctx.showVisuallyHiddenClose ? (openBlock(), createBlock(_component_sr_only_close_button, { key: 6, "visually-hidden-close-label": _ctx.visuallyHiddenCloseLabel, onClose: $options.close }, null, 8, ["visually-hidden-close-label", "onClose"])) : createCommentVNode("", true) ], 10, _hoisted_1), [ [vShow, $props.show] ]) ]), _: 3 }) ]), _: 3 }, 16, ["show", "class", "aria-hidden"]); } const modal = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]); export { modal as default }; //# sourceMappingURL=modal.vue.js.map