UNPKG

@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
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 }; }