maz-ui
Version:
A standalone components library for Vue.Js 3 & Nuxt.Js 3
326 lines (325 loc) • 14.8 kB
JavaScript
import { defineComponent, mergeModels, useAttrs, useTemplateRef, computed, useModel, nextTick, watch, onMounted, onBeforeUnmount, onUnmounted, createElementBlock, openBlock, Fragment, createCommentVNode, createBlock, normalizeStyle, normalizeClass, unref, createElementVNode, mergeProps, renderSlot, Teleport, createVNode, Transition, withCtx, withDirectives } from "vue";
import { offset, hide, shift, autoPlacement, flip, useFloating, autoUpdate } from "@floating-ui/vue";
import { i as isClient } from "../chunks/isClient.WI4oSt66.js";
import { useInstanceUniqId } from "../composables/useInstanceUniqId.js";
import { d as directive } from "../chunks/vClickOutside.DIOiluy0.js";
import { g as getColor } from "../chunks/types.D0Bp_UhS.js";
import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper.B--vMWp3.js";
import '../assets/MazPopover.DU4IHV-5.css';const _hoisted_1 = ["id", "aria-expanded", "aria-haspopup", "aria-describedby", "aria-labelledby"], _hoisted_2 = ["id", "role", "aria-label", "aria-labelledby", "aria-describedby", "aria-modal", "tabindex", "aria-live"], _sfc_main = /* @__PURE__ */ defineComponent({
name: "MazPopover",
inheritAttrs: !1,
__name: "MazPopover",
props: /* @__PURE__ */ mergeModels({
modelValue: { type: Boolean },
position: { default: "auto" },
preferPosition: {},
fallbackPosition: {},
trigger: { default: "click" },
role: { default: "dialog" },
ariaLabel: {},
announceChanges: { type: Boolean, default: !1 },
disabled: { type: Boolean, default: !1 },
offset: { default: 8 },
delay: { default: 0 },
hoverDelay: { default: 150 },
transition: { default: "scale-pop" },
teleportTo: { default: "body" },
overlayClass: {},
panelClass: {},
panelStyle: {},
closeOnClickOutside: { type: Boolean, default: !0 },
closeOnEscape: { type: Boolean, default: !0 },
persistent: { type: Boolean, default: !1 },
id: {},
ariaLabelledby: {},
ariaDescribedby: {},
color: { default: "background" },
trapFocus: { type: Boolean, default: !0 },
keepOpenOnHover: { type: Boolean, default: !1 },
block: { type: Boolean, default: !1 },
positionReference: {}
}, {
modelValue: { default: !1 },
modelModifiers: {}
}),
emits: /* @__PURE__ */ mergeModels(["update:model-value", "open", "close", "after-close-animation", "toggle"], ["update:modelValue"]),
setup(__props, { expose: __expose, emit: __emit }) {
const emits = __emit, triggerId = useInstanceUniqId({
componentName: "MazPopover",
providedId: __props.id
}), attrs = useAttrs(), triggerRef = useTemplateRef("trigger"), panelRef = useTemplateRef("panel"), middleware = computed(() => {
const middleware2 = [
offset(__props.offset),
hide(),
shift({ padding: 5 })
];
return __props.position === "auto" && !__props.preferPosition && !__props.fallbackPosition ? middleware2.push(autoPlacement({
allowedPlacements: ["top", "bottom", "left", "right"]
})) : middleware2.push(flip({
fallbackPlacements: __props.fallbackPosition ? [__props.fallbackPosition] : void 0
})), middleware2;
}), floatingPosition = computed(() => __props.position === "auto" ? __props.preferPosition : __props.position), transitionName = computed(() => ["scale-pop", "scale-fade"].includes(__props.transition) ? `maz-${__props.transition}` : __props.transition), positionRef = computed(() => {
if (!__props.positionReference)
return triggerRef.value;
if (typeof __props.positionReference == "string") {
const withinTrigger = triggerRef.value?.querySelector(__props.positionReference);
return withinTrigger || (isClient() ? document.querySelector(__props.positionReference) : null);
}
return __props.positionReference;
}), reference = computed(() => positionRef.value || triggerRef.value), { floatingStyles, placement, update, middlewareData } = useFloating(
reference,
panelRef,
{
placement: floatingPosition,
middleware,
transform: !1,
whileElementsMounted: autoUpdate
}
), computedPosition = computed(() => placement.value ?? floatingPosition.value), isOpen = useModel(__props, "modelValue");
let openTimeout = null, closeTimeout = null, initialFocusElement = null, ignoreNextClickOutside = !1;
const panelId = computed(() => `${triggerId.value}-panel`), rootStyles = computed(() => attrs.style), panelStyles = computed(() => ({
...floatingStyles.value,
pointerEvents: isOpen.value ? "auto" : "none"
})), isTouchDevice = computed(() => isClient() ? "ontouchstart" in globalThis || navigator.maxTouchPoints > 0 : !1), effectiveTrigger = computed(() => __props.trigger === "adaptive" ? isTouchDevice.value ? "click" : "hover" : __props.trigger), triggerEvents = computed(() => {
if (__props.disabled || effectiveTrigger.value === "manual")
return {};
const events = {};
return effectiveTrigger.value === "hover" && (events.onMouseenter = () => {
clearCloseTimeout(), open();
}, events.onMouseleave = close), effectiveTrigger.value === "click" && (events.onClick = toggle), events;
}), panelEvents = computed(() => effectiveTrigger.value !== "hover" ? {} : {
onMouseenter: () => {
__props.keepOpenOnHover && clearCloseTimeout();
},
onMouseleave: () => {
__props.keepOpenOnHover && close();
}
}), panelClasses = computed(() => [
__props.overlayClass,
__props.panelClass,
`--position-${computedPosition.value}`,
`--${getColor(__props.color)}`
]);
function cleanup() {
clearOpenTimeout(), clearCloseTimeout(), isClient() && document.removeEventListener("keydown", onKeydown);
}
function open() {
__props.disabled || (clearCloseTimeout(), effectiveTrigger.value === "click" && (ignoreNextClickOutside = !0), __props.delay > 0 ? openTimeout = setTimeout(() => {
setOpen(!0);
}, __props.delay) : setOpen(!0));
}
function close() {
clearOpenTimeout(), __props.delay > 0 && effectiveTrigger.value === "hover" ? closeTimeout = setTimeout(() => {
setOpen(!1);
}, __props.delay) : effectiveTrigger.value === "hover" && __props.keepOpenOnHover ? closeTimeout = setTimeout(() => {
setOpen(!1);
}, __props.hoverDelay) : setOpen(!1);
}
function toggle() {
isOpen.value ? close() : open();
}
function setOpen(value) {
isOpen.value = value, ignoreNextClickOutside = !1, value ? (emits("open"), emits("toggle", value), nextTick(() => {
update(), setupFocusTrap();
})) : (emits("toggle", value), emits("close"), __props.trapFocus && restoreFocus());
}
function clearOpenTimeout() {
openTimeout && (clearTimeout(openTimeout), openTimeout = null);
}
function clearCloseTimeout() {
closeTimeout && (clearTimeout(closeTimeout), closeTimeout = null);
}
function getTransformOrigin(position) {
switch (position) {
case "top":
return "center bottom";
case "top-start":
return "left bottom";
case "top-end":
return "right bottom";
case "bottom":
return "center top";
case "bottom-start":
return "left top";
case "bottom-end":
return "right top";
case "left":
return "right center";
case "left-start":
return "right top";
case "left-end":
return "right bottom";
case "right":
return "left center";
case "right-start":
return "left top";
case "right-end":
return "left bottom";
default:
return "center";
}
}
function setupFocusTrap() {
__props.role === "tooltip" || effectiveTrigger.value === "hover" || !__props.trapFocus || !isClient() || (initialFocusElement = document.activeElement, nextTick(() => {
const focusableElements = panelRef.value?.querySelectorAll(
'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select, [tabindex]:not([tabindex="-1"])'
);
focusableElements && focusableElements.length > 0 ? focusableElements[0].focus({ preventScroll: !0 }) : panelRef.value?.focus({ preventScroll: !0 });
}));
}
function restoreFocus() {
__props.role === "tooltip" || effectiveTrigger.value === "hover" || !__props.trapFocus || !isClient() || nextTick(() => {
initialFocusElement?.focus({ preventScroll: !0 });
});
}
function onKeydown(event) {
isOpen.value && (event.key === "Escape" && __props.closeOnEscape && !__props.persistent && (event.preventDefault(), close()), event.key === "Tab" && __props.trapFocus && handleTrapFocus(event));
}
function handleTrapFocus(event) {
if (!panelRef.value || !isClient())
return;
const focusableElements = panelRef.value.querySelectorAll(
'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length === 0)
return;
const firstElement = focusableElements[0], lastElement = focusableElements[focusableElements.length - 1];
event.shiftKey ? document.activeElement === firstElement && (event.preventDefault(), lastElement.focus()) : document.activeElement === lastElement && (event.preventDefault(), firstElement.focus());
}
function onClickOutside(event) {
if (effectiveTrigger.value !== "manual") {
if (ignoreNextClickOutside) {
ignoreNextClickOutside = !1;
return;
}
if (__props.closeOnClickOutside && !__props.persistent) {
if (triggerRef.value && triggerRef.value.contains(event.target))
return;
close();
}
}
}
return watch(isOpen, (value, oldValue) => {
!isClient() || value === oldValue || (value ? open() : oldValue && !value && close());
}, { immediate: !0 }), watch(() => __props.position, () => {
isOpen.value && nextTick(() => update());
}), onMounted(() => {
document.addEventListener("keydown", onKeydown);
}), onBeforeUnmount(cleanup), onUnmounted(cleanup), __expose({
/**
* Open the popover
* @description Programmatically open the popover
* @usage `mazPopoverInstance.value?.open()`
*/
open,
/**
* Close the popover
* @description Programmatically close the popover
* @usage `mazPopoverInstance.value?.close()`
*/
close,
/**
* Toggle the popover
* @description Programmatically toggle the popover open/close state
* @usage `mazPopoverInstance.value?.toggle()`
*/
toggle,
/**
* Check if the popover is open
* @type {ModelRef<boolean>}
* @description Reactive reference to the popover open state
* @usage `const isPopoverOpen = mazPopoverInstance.value?.isOpen`
*/
isOpen,
/**
* Update the popover position
* @description Manually recalculate and update the popover position
* @usage `mazPopoverInstance.value?.updatePosition()`
*/
updatePosition: update,
/**
* Panel reference
* @type {Ref<HTMLElement>}
* @description Reference to the popover panel element
* @usage `mazPopoverInstance.value?.panelRef`
*/
panelRef
}), (_ctx, _cache) => (openBlock(), createElementBlock(Fragment, null, [
_ctx.$slots.trigger ? (openBlock(), createElementBlock("div", {
key: 0,
class: normalizeClass(["m-popover m-reset-css", [
unref(attrs).class,
{
"--open": isOpen.value,
"--disabled": __props.disabled,
"--block": __props.block
}
]]),
style: normalizeStyle(rootStyles.value)
}, [
createElementVNode("div", mergeProps({
id: unref(triggerId),
ref: "trigger",
role: "button",
class: "m-popover-trigger",
"aria-expanded": __props.role === "dialog" || __props.role === "menu" ? isOpen.value : void 0,
"aria-haspopup": __props.role === "dialog" ? "dialog" : void 0,
"aria-describedby": __props.role === "tooltip" && isOpen.value ? panelId.value : __props.ariaDescribedby,
"aria-labelledby": __props.ariaLabelledby
}, triggerEvents.value), [
renderSlot(_ctx.$slots, "trigger", {
open,
close,
toggle,
isOpen: isOpen.value,
trigger: effectiveTrigger.value
}, void 0, !0)
], 16, _hoisted_1)
], 6)) : createCommentVNode("", !0),
(openBlock(), createBlock(Teleport, { to: __props.teleportTo }, [
createVNode(Transition, {
name: transitionName.value,
appear: "",
onAfterLeave: _cache[0] || (_cache[0] = ($event) => emits("after-close-animation"))
}, {
default: withCtx(() => [
isOpen.value ? withDirectives((openBlock(), createElementBlock("div", mergeProps({
key: 0,
id: panelId.value,
ref: "panel",
role: __props.role,
"aria-label": __props.ariaLabel,
"aria-labelledby": __props.role === "dialog" ? __props.ariaLabelledby || unref(triggerId) : void 0,
"aria-describedby": __props.role === "dialog" ? __props.ariaDescribedby : void 0,
"aria-modal": __props.role === "dialog" ? "true" : void 0,
tabindex: __props.role === "dialog" ? "-1" : void 0,
class: ["m-popover-panel", panelClasses.value],
"aria-live": __props.announceChanges ? "polite" : void 0,
style: [
__props.panelStyle,
panelStyles.value,
{
transformOrigin: getTransformOrigin(computedPosition.value),
visibility: unref(middlewareData).hide?.referenceHidden ? "hidden" : "visible"
}
]
}, panelEvents.value), [
renderSlot(_ctx.$slots, "default", {
open,
close,
toggle,
isOpen: isOpen.value
}, void 0, !0)
], 16, _hoisted_2)), [
[unref(directive), onClickOutside]
]) : createCommentVNode("", !0)
]),
_: 3
}, 8, ["name"])
], 8, ["to"]))
], 64));
}
}), MazPopover = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-641d39cb"]]);
export {
MazPopover as default
};