@dialpad/dialtone
Version:
Dialpad's Dialtone design system monorepo
326 lines (325 loc) • 10.9 kB
JavaScript
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