@inkline/inkline
Version:
Inkline is the intuitive UI Components library that gives you a developer-friendly foundation for building high-quality, accessible, and customizable Vue.js 3 Design Systems.
226 lines (225 loc) • 6.34 kB
JavaScript
import { onMounted, onUnmounted, ref, watch } from "vue";
import { focusFirstDescendant, off, on } from "@grozav/utils";
import { arrow, autoUpdate, computePosition, flip, offset, shift } from "@floating-ui/dom";
import { extractRefHTMLElement } from "@inkline/inkline/utils";
export function usePopupControl(props) {
const visible = ref(props.componentProps.value.visible);
const instance = ref();
const animating = ref(false);
const triggerStack = ref(0);
onMounted(() => {
addEventListeners();
});
onUnmounted(() => {
removeEventListeners();
});
watch(
() => props.componentProps.value.visible,
(value) => {
if (value) {
show();
} else {
hide();
}
}
);
function addEventListeners() {
const triggerRef = extractRefHTMLElement(props.triggerRef);
const popupRef = extractRefHTMLElement(props.popupRef);
if (!triggerRef || !popupRef) {
return;
}
[].concat(props.componentProps.value.events).forEach((trigger) => {
switch (trigger) {
case "hover":
on(
triggerRef,
"mouseenter",
props.componentProps.value.interactable ? hoverShow : show
);
on(
triggerRef,
"mouseleave",
props.componentProps.value.interactable ? hoverHide : hide
);
if (props.componentProps.value.interactable) {
on(popupRef, "mouseenter", hoverShow);
on(popupRef, "mouseleave", hoverHide);
}
break;
case "click":
on(triggerRef, "click", onClick);
break;
case "focus":
for (const child of triggerRef.children) {
on(child, "focus", show);
on(child, "blur", hide);
}
break;
default:
break;
}
});
}
function removeEventListeners() {
const triggerRef = extractRefHTMLElement(props.triggerRef);
const popupRef = extractRefHTMLElement(props.popupRef);
if (!triggerRef || !popupRef) {
return;
}
[].concat(props.componentProps.value.events).forEach((trigger) => {
switch (trigger) {
case "hover":
off(
triggerRef,
"mouseenter",
props.componentProps.value.interactable ? hoverShow : show
);
off(
triggerRef,
"mouseleave",
props.componentProps.value.interactable ? hoverHide : hide
);
if (props.componentProps.value.interactable) {
off(popupRef, "mouseenter", hoverShow);
off(popupRef, "mouseleave", hoverHide);
}
break;
case "click":
off(triggerRef, "click", onClick);
break;
case "focus":
for (const child of triggerRef.children) {
off(child, "focus", show);
off(child, "blur", hide);
}
break;
default:
break;
}
});
}
function show() {
if (props.componentProps.value.disabled || props.componentProps.value.readonly || visible.value) {
return;
}
triggerStack.value += 1;
visible.value = true;
createPopup();
props.emit("update:visible", true);
}
function hide() {
if (props.componentProps.value.disabled || props.componentProps.value.readonly || !visible.value) {
return;
}
triggerStack.value -= 1;
if (triggerStack.value <= 0) {
triggerStack.value = 0;
visible.value = false;
props.emit("update:visible", false);
setTimeout(() => destroyPopup(), props.componentProps.value.animationDuration);
}
}
function onClick() {
if (visible.value) {
hide();
} else {
show();
}
}
function onClickOutside() {
props.emit("click:outside");
if (!props.componentProps.value.visible) {
hide();
}
}
function onKeyEscape() {
hide();
}
function hoverShow() {
animating.value = false;
show();
}
function hoverHide() {
animating.value = true;
setTimeout(() => {
if (animating.value) {
hide();
}
}, props.componentProps.value.hoverHideDelay);
}
function createPopup() {
if (typeof window === "undefined") {
return;
}
const triggerRef = extractRefHTMLElement(props.triggerRef);
const popupRef = extractRefHTMLElement(props.popupRef);
const arrowRef = extractRefHTMLElement(props.arrowRef);
if (!triggerRef || !popupRef) {
throw new Error("Trigger and popup elements are required.");
}
instance.value = autoUpdate(triggerRef, popupRef, () => {
computePosition(triggerRef, popupRef, {
strategy: "absolute",
placement: props.componentProps.value.placement,
middleware: [
offset(props.componentProps.value.offset),
flip(),
shift({ padding: 6 })
].concat(arrowRef ? [arrow({ element: arrowRef })] : []),
...props.componentProps.value.popupOptions
}).then(({ x, y, placement, middlewareData }) => {
Object.assign(popupRef.style, {
left: `${x}px`,
top: `${y}px`
});
popupRef?.setAttribute("data-popup-placement", placement);
if (arrowRef) {
const { x: arrowX, y: arrowY } = middlewareData.arrow;
const staticSide = {
top: "bottom",
right: "left",
bottom: "top",
left: "right"
}[placement.split("-")[0]];
Object.assign(arrowRef.style, {
left: arrowX !== null ? `${arrowX}px` : "",
top: arrowY !== null ? `${arrowY}px` : "",
right: "",
bottom: "",
[staticSide]: "-6px"
});
}
});
});
}
function destroyPopup() {
if (instance.value) {
instance.value();
instance.value = void 0;
}
}
function focusTrigger() {
const triggerRef = extractRefHTMLElement(props.triggerRef);
if (!triggerRef) {
return;
}
for (const child of triggerRef.children) {
if (focusFirstDescendant(child)) {
child.focus();
break;
}
}
}
return {
visible,
show,
hide,
onClick,
onClickOutside,
onKeyEscape,
focusTrigger,
createPopup,
destroyPopup
};
}