UNPKG

@dialpad/dialtone-vue

Version:

Vue component library for Dialpad's design system Dialtone

703 lines (702 loc) 26.4 kB
import { POPOVER_CONTENT_WIDTHS as h, POPOVER_HEADER_FOOTER_PADDING_CLASSES as d, POPOVER_PADDING_CLASSES as l, POPOVER_APPEND_TO_VALUES as c, POPOVER_INITIAL_FOCUS_STRINGS as u, POPOVER_STICKY_VALUES as f, POPOVER_ROLES as m } from "./popover-constants.js"; import { enableRootScrolling as y, disableRootScrolling as v, isOutOfViewPort as E, warnIfUnmounted as g, getUniqueString as p } from "../../common/utils/index.js"; import { Portal as O } from "../../node_modules/@linusborg/vue-simple-portal.js"; import _ from "../../common/mixins/modal.js"; import { createTippyPopover as b, getPopperOptions as C } from "./tippy-utils.js"; import w from "./popover-header-footer.js"; import P from "../../shared/sr_only_close_button.js"; import { n as S } from "../../_plugin-vue2_normalizer-DSLOjnn3.js"; import A from "../lazy-show/lazy-show.js"; const x = { name: "DtPopover", /******************** * CHILD COMPONENTS * ********************/ components: { SrOnlyCloseButton: P, DtLazyShow: A, PopoverHeaderFooter: w, Portal: O }, mixins: [_], props: { /** * Controls whether the popover is shown. Leaving this null will have the popover trigger on click by default. * If you set this value, the default trigger behavior will be disabled, and you can control it as you need. * Supports .sync modifier * @values null, true, false */ open: { type: Boolean, default: null }, /** * Opens the popover on right click (context menu). If you set this value to `true`, * the default trigger behavior will be disabled. * @values true, false */ openOnContext: { type: Boolean, default: !1 }, /** * Element type (tag name) of the root element of the component. */ elementType: { type: String, default: "div" }, /** * Named transition when the content display is toggled. * @see DtLazyShow */ transition: { type: String, default: "fade" }, /** * ARIA role for the content of the popover. Defaults to "dialog". * <a class="d-link" href="https://www.w3.org/TR/wai-aria/#aria-haspopup" target="_blank">aria-haspopup</a> */ role: { type: String, default: "dialog", validator: (e) => m.includes(e) }, /** * ID of the element that serves as the label for the popover content. * Defaults to the "anchor" element; this exists to provide a different * ID of the label element if, for example, the anchor slot contains * other items that do not serve as a label. You should provide this * or ariaLabel, but not both. */ ariaLabelledby: { type: String, default: null }, /** * Descriptive label for the popover content. You should provide this * or ariaLabelledby, but not both. */ ariaLabel: { type: String, default: null }, /** * Padding size class for the popover content. * @values none, small, medium, large */ padding: { type: String, default: "large", validator: (e) => Object.keys(l).some((t) => t === e) }, /** * Additional class name for the content wrapper element. */ contentClass: { type: [String, Array, Object], default: "" }, /** * Width configuration for the popover content. When its value is 'anchor', * the popover content will have the same width as the anchor. * @values null, anchor */ contentWidth: { type: String, default: "", validator: (e) => h.includes(e) }, /** * Tabindex value for the content. Passing null, no tabindex attribute will be set. */ contentTabindex: { type: Number || null, default: -1 }, /** * External anchor id to use in those cases the anchor can't be provided via the slot. * For instance, using the combobox's input as the anchor for the popover. */ externalAnchor: { type: String, default: "" }, /** * The id of the tooltip */ id: { type: String, default() { return p(); } }, /** * Displaces the content box from its anchor element * by the specified number of pixels. * <a * class="d-link" * href="https://atomiks.github.io/tippyjs/v6/all-props/#offset" * target="_blank" * > * Tippy.js docs * </a> */ offset: { type: Array, default: () => [0, 4] }, /** * Determines if the popover hides upon clicking the * anchor or outside the content box. * @values true, false */ hideOnClick: { type: Boolean, default: !0 }, /** * Determines modal state. If enabled popover has a modal overlay * preventing interaction with elements below it, but it is invisible. * @values true, false */ modal: { type: Boolean, default: !0 }, /** * If the popover does not fit in the direction described by "placement", * it will attempt to change its direction to the "fallbackPlacements". * <a * class="d-link" * href="https://popper.js.org/docs/v2/modifiers/flip/#fallbackplacements" * target="_blank" * > * Popper.js docs * </a> * */ fallbackPlacements: { type: Array, default: () => ["auto"] }, /** * The direction the popover displays relative to the anchor. * <a * class="d-link" * href="https://atomiks.github.io/tippyjs/v6/all-props/#placement" * target="_blank" * > * Tippy.js docs * </a> * @values top, top-start, top-end, * right, right-start, right-end, * left, left-start, left-end, * bottom, bottom-start, bottom-end, * auto, auto-start, auto-end */ placement: { type: String, default: "bottom-end" }, /** * If set to false the dialog will display over top of the anchor when there is insufficient space. * If set to true it will never move from its position relative to the anchor and will clip instead. * <a * class="d-link" * href="https://popper.js.org/docs/v2/modifiers/prevent-overflow/#tether" * target="_blank" * > * Popper.js docs * </a> * @values true, false */ tether: { type: Boolean, default: !0 }, /** * If the popover sticks to the anchor. This is usually not needed, but can be needed * if the reference element's position is animating, or to automatically update the popover * position in those cases the DOM layout changes the reference element's position. * `true` enables it, `reference` only checks the "reference" rect for changes and `popper` only * checks the "popper" rect for changes. * <a * class="d-link" * href="https://atomiks.github.io/tippyjs/v6/all-props/#sticky" * target="_blank" * > * Tippy.js docs * </a> * @values true, false, reference, popper */ sticky: { type: [Boolean, String], default: !1, validator: (e) => f.includes(e) }, /** * Determines maximum height for the popover before overflow. * Possible units rem|px|em */ maxHeight: { type: String, default: "" }, /** * Determines maximum width for the popover before overflow. * Possible units rem|px|%|em */ maxWidth: { type: String, default: "" }, /** * Determines visibility for close button * @values true, false */ showCloseButton: { type: Boolean, default: !1 }, /** * Additional class name for the header content wrapper element. */ headerClass: { type: [String, Array, Object], default: "" }, /** * Additional class name for the footer content wrapper element. */ footerClass: { type: [String, Array, Object], default: "" }, /** * Additional class name for the dialog element. */ dialogClass: { type: [String, Array, Object], default: "" }, /** * The element that is focused when the popover is opened. This can be an * HTMLElement within the popover, 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'. * @values none, dialog, first */ initialFocusElement: { type: [String, HTMLElement], default: "first", validator: (e) => u.includes(e) || e instanceof HTMLElement || e.startsWith("#") }, /** * If the popover should open pressing up or down arrow key on the anchor element. * This can be set when not passing open prop. * @values true, false */ openWithArrowKeys: { type: Boolean, default: !1 }, /** * Sets the element to which the popover is going to append to. * 'body' will append to the nearest body (supports shadow DOM). * 'root' will try append to the iFrame's parent body if it is contained in an iFrame * and has permissions to access it, else, it'd default to 'parent'. * @values 'body', 'parent', 'root', HTMLElement */ appendTo: { type: [HTMLElement, String], default: "body", validator: (e) => c.includes(e) || e instanceof HTMLElement } }, emits: [ /** * Emitted when popover is shown or hidden * * @event opened * @type {Boolean | Array} */ "opened", /** * Emitted to sync value with parent * * @event update:opened * @type {Boolean | Array} */ "update:open", /** * Emitted when the mouse enters the popover * * @event mouseenter-popover */ "mouseenter-popover", /** * Emitted when the mouse leaves the popover * * @event mouseleave-popover */ "mouseleave-popover", /** * Emitted when the mouse enters the popover anchor * * @event mouseenter-popover-anchor */ "mouseenter-popover-anchor", /** * Emitted when the mouse leaves the popover anchor * * @event mouseleave-popover-anchor */ "mouseleave-popover-anchor" ], data() { return { POPOVER_PADDING_CLASSES: l, POPOVER_HEADER_FOOTER_PADDING_CLASSES: d, intersectionObserver: null, mutationObserver: null, isOutsideViewport: !1, isOpen: !1, anchorEl: null, popoverContentEl: null }; }, computed: { popoverListeners() { return { ...this.$listeners, keydown: (e) => { this.onKeydown(e), this.$emit("keydown", e); }, "after-leave": () => { this.onLeaveTransitionComplete(); }, "after-enter": () => { this.onEnterTransitionComplete(); } }; }, calculatedMaxHeight() { return this.isOutsideViewport && this.modal ? "calc(100vh - var(--dt-space-300))" : this.maxHeight; }, labelledBy() { return this.ariaLabelledby || !this.ariaLabel && p("DtPopover__anchor"); } }, watch: { $props: { immediate: !0, deep: !0, handler() { this.validateProps(); } }, modal(e) { var t; (t = this.tip) == null || t.setProps({ zIndex: e ? 650 : this.calculateAnchorZindex() }); }, offset(e) { var t; (t = this.tip) == null || t.setProps({ offset: e }); }, sticky(e) { var t; (t = this.tip) == null || t.setProps({ sticky: e }); }, fallbackPlacements() { var e; (e = this.tip) == null || e.setProps({ popperOptions: this.popperOptions() }); }, tether() { var e; (e = this.tip) == null || e.setProps({ popperOptions: this.popperOptions() }); }, placement(e) { var t; (t = this.tip) == null || t.setProps({ placement: e }); }, open: { handler: function(e) { e !== null && (this.isOpen = e); }, immediate: !0 }, isOpen(e, t) { var o, n; e ? (this.initTippyInstance(), (o = this.tip) == null || o.show()) : !e && t !== e && (this.removeEventListeners(), (n = this.tip) == null || n.hide()); } }, mounted() { var e; g(this.$el, this.$options.name), this.popoverContentEl = (e = this.$refs.content) == null ? void 0 : e.$el, this.updateAnchorEl(), this.mutationObserver = new MutationObserver(this.updateAnchorEl), this.mutationObserver.observe(this.$refs.anchor, { childList: !0 }), this.intersectionObserver = new IntersectionObserver(this.hasIntersectedViewport), this.intersectionObserver.observe(this.popoverContentEl); }, beforeDestroy() { var e, t, o; (e = this.tip) == null || e.destroy(), (t = this.intersectionObserver) == null || t.disconnect(), (o = this.mutationObserver) == null || o.disconnect(), this.removeReferences(), this.removeEventListeners(); }, /****************** * METHODS * ******************/ methods: { hasIntersectedViewport(e) { var n; const t = (n = e == null ? void 0 : e[0]) == null ? void 0 : n.target; if (!t) return; const o = E(t); this.isOutsideViewport = o.bottom || o.top; }, updateAnchorEl() { var o, n; const t = (this.externalAnchor ? this.$refs.anchor.getRootNode().querySelector(`#${this.externalAnchor}`) : null) ?? this.$refs.anchor.children[0]; if (t !== this.anchorEl) { if (this.anchorEl = t, (o = this.tip) == null || o.destroy(), delete this.tip, !this.anchorEl) { console.warn("No anchor found for popover"); return; } this.isOpen && (this.initTippyInstance(), (n = this.tip) == null || n.show()); } }, popperOptions() { return C({ fallbackPlacements: this.fallbackPlacements, tether: this.tether, hasHideModifierEnabled: !0 }); }, validateProps() { this.modal && this.initialFocusElement === "none" && console.error('If the popover is modal you must set the initialFocusElement prop. Possible values: "dialog", "first", HTMLElement'); }, calculateAnchorZindex() { var e; return this.$el.getRootNode().querySelector('.d-modal[aria-hidden="false"], .d-modal--transparent[aria-hidden="false"]') || // Special case because we don't have any dialtone drawer component yet. Render at 650 when // anchor of popover is within a drawer. (e = this.anchorEl) != null && e.closest(".d-zi-drawer") ? 650 : 300; }, defaultToggleOpen(e) { var t, o; this.openOnContext || (this.open ?? ((t = this.anchorEl) != null && t.contains(e.target) && !((o = this.anchorEl) != null && o.disabled) && this.toggleOpen())); }, async onContext(e) { var t; this.openOnContext && (e.preventDefault(), this.isOpen = !0, await this.$nextTick(), (t = this.tip) == null || t.setProps({ placement: "right-start", getReferenceClientRect: () => ({ width: 0, height: 0, top: e.clientY, bottom: e.clientY, left: e.clientX, right: e.clientX }) })); }, toggleOpen() { this.isOpen = !this.isOpen; }, onArrowKeyPress(e) { var t; this.open === null && this.openWithArrowKeys && (t = this.anchorEl) != null && t.contains(e.target) && (this.isOpen || (this.isOpen = !0)); }, addEventListeners() { window.addEventListener("dt-popover-close", this.closePopover), this.contentWidth === "anchor" && window.addEventListener("resize", this.onResize); }, removeEventListeners() { window.removeEventListener("dt-popover-close", this.closePopover), this.contentWidth === "anchor" && window.removeEventListener("resize", this.onResize); }, closePopover() { this.isOpen = !1; }, /** * Prevents scrolling outside of the currently opened modal popover by: * - when anchor is not within another popover: setting the body to overflow: hidden * - when anchor is within another popover: set the popover dialog container to it's non-modal z-index * since it is no longer the active modal. This puts it underneath the overlay and prevents scrolling. */ preventScrolling() { var e, t, o; if (this.modal) { const n = (e = this.anchorEl) == null ? void 0 : e.closest("body, .tippy-box"); if (!n) return; ((t = n.tagName) == null ? void 0 : t.toLowerCase()) === "body" ? (v(this.anchorEl.getRootNode().host), (o = this.tip) == null || o.setProps({ offset: this.offset })) : n.classList.add("d-zi-popover"); } }, /** * Resets the prevent scrolling properties set in preventScrolling() back to normal. */ enableScrolling() { var t, o, n; const e = (t = this.anchorEl) == null ? void 0 : t.closest("body, .tippy-box"); e && (((o = e.tagName) == null ? void 0 : o.toLowerCase()) === "body" ? (y(this.anchorEl.getRootNode().host), (n = this.tip) == null || n.setProps({ offset: this.offset })) : e.classList.remove("d-zi-popover")); }, removeReferences() { this.anchorEl = null, this.popoverContentEl = null, this.tip = null; }, async onShow() { this.contentWidth === "anchor" && await this.setPopoverContentAnchorWidth(), this.contentWidth === null && (this.popoverContentEl.style.width = "auto"), this.addEventListeners(); }, async onLeaveTransitionComplete() { var e; this.modal && (await this.focusFirstElement(this.$refs.anchor), await this.$nextTick(), this.enableScrolling()), (e = this.tip) == null || e.unmount(), this.$emit("opened", !1), this.open !== null && this.$emit("update:open", !1); }, async onEnterTransitionComplete() { this.focusInitialElement(), await this.$nextTick(), this.preventScrolling(), this.$emit("opened", !0, this.$refs.popover__content), this.open !== null && this.$emit("update:open", !0); }, focusInitialElement() { var e, t; this.initialFocusElement === "dialog" && ((t = (e = this.$refs.content) == null ? void 0 : e.$el) == null || t.focus()), this.initialFocusElement.startsWith("#") && this.focusInitialElementById(), this.initialFocusElement === "first" && this.focusFirstElementIfNeeded(this.$refs.popover__content), this.initialFocusElement instanceof HTMLElement && this.initialFocusElement.focus(); }, focusInitialElementById() { var t, o, n; const e = (o = (t = this.$refs.content) == null ? void 0 : t.$el) == null ? void 0 : o.querySelector(this.initialFocusElement); e ? e.focus() : console.warn('Could not find the element specified in dt-popover prop "initialFocusElement". Defaulting to focusing the dialog.'), e ? e.focus() : (n = this.$refs.content) == null || n.$el.focus(); }, onResize() { this.closePopover(); }, onClickOutside() { var t; if (!this.hideOnClick) return; ((t = this.popoverContentEl) == null ? void 0 : t.querySelector(".d-popover__anchor--opened")) || this.closePopover(); }, onKeydown(e) { e.key === "Tab" && this.modal && this.focusTrappedTabPress(e, this.popoverContentEl), e.key === "Escape" && this.closePopover(); }, async setPopoverContentAnchorWidth() { var e; await this.$nextTick(), this.popoverContentEl.style.width = `${(e = this.anchorEl) == null ? void 0 : e.clientWidth}px`; }, focusFirstElementIfNeeded(e) { var o, n; this._getFocusableElements(e, !0).length !== 0 ? this.focusFirstElement(e) : this.showCloseButton ? (o = this.$refs.popover__header) == null || o.focusCloseButton() : (n = this.$refs.content) == null || n.$el.focus(); }, /** * Return's the anchor ClientRect object relative to the window. * Refer to: https://atomiks.github.io/tippyjs/v6/all-props/#getreferenceclientrect for more information * @param error */ getReferenceClientRect(e) { var r, a; const t = (r = this.anchorEl) == null ? void 0 : r.getBoundingClientRect(); if (this.appendTo !== "root" || e) return t; const o = (a = this.anchorEl) == null ? void 0 : a.ownerDocument, n = (o == null ? void 0 : o.defaultView) || (o == null ? void 0 : o.parentWindow), s = n == null ? void 0 : n.frameElement; if (!s) return t; const i = s.getBoundingClientRect(); return { width: t == null ? void 0 : t.width, height: t == null ? void 0 : t.height, top: (i == null ? void 0 : i.top) + (t == null ? void 0 : t.top), left: (i == null ? void 0 : i.left) + (t == null ? void 0 : t.left), right: (i == null ? void 0 : i.right) + (t == null ? void 0 : t.right), bottom: (i == null ? void 0 : i.bottom) + (t == null ? void 0 : t.bottom) }; }, initTippyInstance() { var o, n, s; let e = null, t = !1; switch (this.appendTo) { case "body": e = (n = (o = this.anchorEl) == null ? void 0 : o.getRootNode()) == null ? void 0 : n.querySelector("body"); break; case "root": try { e = window.parent.document.body; } catch (i) { console.error("Could not attach the popover to iframe parent window: ", i), e = "parent", t = !0; } break; default: e = this.appendTo; break; } (s = this.tip) == null || s.destroy(), this.tip = b(this.anchorEl, { popperOptions: this.popperOptions(), contentElement: this.popoverContentEl, placement: this.placement, offset: this.offset, sticky: this.sticky, appendTo: e, interactive: !0, trigger: "manual", getReferenceClientRect: () => this.getReferenceClientRect(t), // We have to manage hideOnClick functionality manually to handle // popover within popover situations. hideOnClick: !1, zIndex: this.modal ? 650 : this.calculateAnchorZindex(), onClickOutside: this.onClickOutside, onShow: this.onShow }); }, onMouseEnter() { this.$emit("mouseenter-popover"); }, onMouseLeave() { this.$emit("mouseleave-popover"); }, onMouseEnterAnchor() { this.$emit("mouseenter-popover-anchor"); }, onMouseLeaveAnchor() { this.$emit("mouseleave-popover-anchor"); }, hasFooter() { var e, t; return this.$slots.footerContent || ((t = (e = this.$scopedSlots).footerContent) == null ? void 0 : t.call(e)); } } }; var k = function() { var t = this, o = t._self._c; return o("div", [t.modal && t.isOpen ? o("portal", [o("div", { staticClass: "d-modal--transparent", attrs: { "aria-hidden": "false" }, on: { click: function(n) { n.preventDefault(), n.stopPropagation(); } } })]) : t._e(), o(t.elementType, t._g({ ref: "popover", tag: "component", class: ["d-popover", { "d-popover__anchor--opened": t.isOpen }], attrs: { "data-qa": "dt-popover-container" } }, t.$listeners), [o("div", { ref: "anchor", attrs: { id: !t.ariaLabelledby && t.labelledBy, "data-qa": t.$attrs["data-qa"] ? `${t.$attrs["data-qa"]}-anchor` : "dt-popover-anchor", tabindex: t.openOnContext ? 0 : void 0 }, on: { "!click": function(n) { return t.defaultToggleOpen.apply(null, arguments); }, contextmenu: t.onContext, keydown: [function(n) { return !n.type.indexOf("key") && t._k(n.keyCode, "up", 38, n.key, ["Up", "ArrowUp"]) ? null : (n.preventDefault(), t.onArrowKeyPress.apply(null, arguments)); }, function(n) { return !n.type.indexOf("key") && t._k(n.keyCode, "down", 40, n.key, ["Down", "ArrowDown"]) ? null : (n.preventDefault(), t.onArrowKeyPress.apply(null, arguments)); }], "!keydown": function(n) { return !n.type.indexOf("key") && t._k(n.keyCode, "escape", void 0, n.key, void 0) ? null : t.closePopover.apply(null, arguments); }, mouseenter: t.onMouseEnter, mouseleave: t.onMouseLeave } }, [t._t("anchor", null, { attrs: { "aria-expanded": t.isOpen.toString(), "aria-controls": t.id, "aria-haspopup": t.role } })], 2), o("dt-lazy-show", t._g({ ref: "content", class: ["d-popover__dialog", { "d-popover__dialog--modal": t.modal }, t.dialogClass], style: { "max-height": t.calculatedMaxHeight, "max-width": t.maxWidth }, attrs: { id: t.id, role: t.role, "data-qa": t.$attrs["data-qa"] ? `${t.$attrs["data-qa"]}__dialog` : "dt-popover", "aria-hidden": `${!t.isOpen}`, "aria-labelledby": t.labelledBy, "aria-label": t.ariaLabel, "aria-modal": `${!t.modal}`, transition: t.transition, show: t.isOpen, tabindex: t.contentTabindex, appear: "" }, on: { mouseenter: t.onMouseEnterAnchor, mouseleave: t.onMouseLeaveAnchor } }, t.popoverListeners), [t.$slots.headerContent || t.showCloseButton ? o("popover-header-footer", { ref: "popover__header", class: t.POPOVER_HEADER_FOOTER_PADDING_CLASSES[t.padding], attrs: { "content-class": t.headerClass, type: "header", "show-close-button": t.showCloseButton }, on: { close: t.closePopover }, scopedSlots: t._u([{ key: "content", fn: function() { return [t._t("headerContent", null, { close: t.closePopover })]; }, proxy: !0 }], null, !0) }) : t._e(), o("div", { ref: "popover__content", class: [ "d-popover__content", t.POPOVER_PADDING_CLASSES[t.padding], t.contentClass ], attrs: { "data-qa": t.$attrs["data-qa"] ? `${t.$attrs["data-qa"]}-content` : "dt-popover-content" } }, [t._t("content", null, { close: t.closePopover })], 2), t.hasFooter() ? o("popover-header-footer", { ref: "popover__footer", class: t.POPOVER_HEADER_FOOTER_PADDING_CLASSES[t.padding], attrs: { type: "footer", "content-class": t.footerClass }, scopedSlots: t._u([{ key: "content", fn: function() { return [t._t("footerContent", null, { close: t.closePopover })]; }, proxy: !0 }], null, !0) }) : t._e(), t.showCloseButton ? t._e() : o("sr-only-close-button", { on: { close: t.closePopover } })], 1)], 1)], 1); }, L = [], $ = /* @__PURE__ */ S( x, k, L ); const H = $.exports; export { H as default }; //# sourceMappingURL=popover.js.map