UNPKG

reka-ui

Version:

Vue port for Radix UI Primitives.

122 lines (120 loc) 5.3 kB
import { handleAndDispatchCustomEvent } from "../shared/handleAndDispatchCustomEvent.js"; import { nextTick, ref, toValue, watchEffect } from "vue"; import { isClient } from "@vueuse/shared"; //#region src/DismissableLayer/utils.ts const POINTER_DOWN_OUTSIDE = "dismissableLayer.pointerDownOutside"; const FOCUS_OUTSIDE = "dismissableLayer.focusOutside"; function isLayerExist(layerElement, targetElement) { const targetLayer = targetElement.closest("[data-dismissable-layer]"); const mainLayer = layerElement.dataset.dismissableLayer === "" ? layerElement : layerElement.querySelector("[data-dismissable-layer]"); const nodeList = Array.from(layerElement.ownerDocument.querySelectorAll("[data-dismissable-layer]")); if (targetLayer && (mainLayer === targetLayer || nodeList.indexOf(mainLayer) < nodeList.indexOf(targetLayer))) return true; else return false; } /** * Listens for `pointerdown` outside a DOM subtree. We use `pointerdown` rather than `pointerup` * to mimic layer dismissing behaviour present in OS. * Returns props to pass to the node we want to check for outside events. */ function usePointerDownOutside(onPointerDownOutside, element, enabled = true) { const ownerDocument = element?.value?.ownerDocument ?? globalThis?.document; const isPointerInsideDOMTree = ref(false); const handleClickRef = ref(() => {}); watchEffect((cleanupFn) => { if (!isClient || !toValue(enabled)) return; const handlePointerDown = async (event) => { const target = event.target; if (!element?.value || !target) return; if (isLayerExist(element.value, target)) { isPointerInsideDOMTree.value = false; return; } if (event.target && !isPointerInsideDOMTree.value) { const eventDetail = { originalEvent: event }; function handleAndDispatchPointerDownOutsideEvent() { handleAndDispatchCustomEvent(POINTER_DOWN_OUTSIDE, onPointerDownOutside, eventDetail); } /** * On touch devices, we need to wait for a click event because browsers implement * a ~350ms delay between the time the user stops touching the display and when the * browser executes events. We need to ensure we don't reactivate pointer-events within * this timeframe otherwise the browser may execute events that should have been prevented. * * Additionally, this also lets us deal automatically with cancellations when a click event * isn't raised because the page was considered scrolled/drag-scrolled, long-pressed, etc. * * This is why we also continuously remove the previous listener, because we cannot be * certain that it was raised, and therefore cleaned-up. */ if (event.pointerType === "touch") { ownerDocument.removeEventListener("click", handleClickRef.value); handleClickRef.value = handleAndDispatchPointerDownOutsideEvent; ownerDocument.addEventListener("click", handleClickRef.value, { once: true }); } else handleAndDispatchPointerDownOutsideEvent(); } else ownerDocument.removeEventListener("click", handleClickRef.value); isPointerInsideDOMTree.value = false; }; /** * if this hook executes in a component that mounts via a `pointerdown` event, the event * would bubble up to the document and trigger a `pointerDownOutside` event. We avoid * this by delaying the event listener registration on the document. * This is how the DOM works, ie: * ``` * button.addEventListener('pointerdown', () => { * console.log('I will log'); * document.addEventListener('pointerdown', () => { * console.log('I will also log'); * }) * }); */ const timerId = window.setTimeout(() => { ownerDocument.addEventListener("pointerdown", handlePointerDown); }, 0); cleanupFn(() => { window.clearTimeout(timerId); ownerDocument.removeEventListener("pointerdown", handlePointerDown); ownerDocument.removeEventListener("click", handleClickRef.value); }); }); return { onPointerDownCapture: () => { if (!toValue(enabled)) return; isPointerInsideDOMTree.value = true; } }; } /** * Listens for when focus happens outside a DOM subtree. * Returns props to pass to the root (node) of the subtree we want to check. */ function useFocusOutside(onFocusOutside, element, enabled = true) { const ownerDocument = element?.value?.ownerDocument ?? globalThis?.document; const isFocusInsideDOMTree = ref(false); watchEffect((cleanupFn) => { if (!isClient || !toValue(enabled)) return; const handleFocus = async (event) => { if (!element?.value) return; await nextTick(); await nextTick(); const target = event.target; if (!element.value || !target || isLayerExist(element.value, target)) return; if (event.target && !isFocusInsideDOMTree.value) { const eventDetail = { originalEvent: event }; handleAndDispatchCustomEvent(FOCUS_OUTSIDE, onFocusOutside, eventDetail); } }; ownerDocument.addEventListener("focusin", handleFocus); cleanupFn(() => ownerDocument.removeEventListener("focusin", handleFocus)); }); return { onFocusCapture: () => { if (!toValue(enabled)) return; isFocusInsideDOMTree.value = true; }, onBlurCapture: () => { if (!toValue(enabled)) return; isFocusInsideDOMTree.value = false; } }; } //#endregion export { useFocusOutside, usePointerDownOutside }; //# sourceMappingURL=utils.js.map