UNPKG

@navikt/ds-react

Version:

React components from the Norwegian Labour and Welfare Administration.

93 lines (85 loc) 3.42 kB
import { useEffect, useRef } from "react"; import { useEventCallback } from "../../../utils/hooks"; import { CUSTOM_EVENTS, CustomPointerEvent, dispatchCustomEvent, } from "./dispatchCustomEvent"; /** * Listens for `pointerup` outside a react subtree. * Returns props to pass to the node we want to check for outside events. * By checking `isPointerInsideReactTreeRef` we can determine if the event happened outside the subtree of the node, saving some element-comparisons. */ export function usePointerUpOutside( callback?: (event: CustomPointerEvent) => void, ownerDocument: Document = globalThis?.document, enabled: boolean = true, ) { // Keep callback ref stable const handlePointerUpOutside = useEventCallback(callback) as EventListener; // Tracks if the pointer interaction started inside the React subtree. const isPointerInsideReactTreeRef = useRef(false); useEffect(() => { if (!enabled) { return; } const handlePointerUp = (event: PointerEvent) => { /** * The `DismisableLayer`-API is based on the ability to stop events from propagating and in the end calling `onDismiss` * if `usePointerUpOutside`-callback does not run `event.preventDefault()`. * * Although `pointerup` is already a cancelable event, we still dispatch a custom event * to keep parity with focus outside handling and ensure ordering. */ if (event.target && !isPointerInsideReactTreeRef.current) { dispatchCustomEvent( CUSTOM_EVENTS.POINTER_UP_OUTSIDE, handlePointerUpOutside, { originalEvent: event }, ); } /* Reset for next interaction. */ isPointerInsideReactTreeRef.current = false; }; /* Mostly relevant if user moved touch after touch-start */ const handlePointerCancel = () => { /* Reset state if interaction is cancelled */ isPointerInsideReactTreeRef.current = false; }; /** * If this hook executes in a component that mounts via a `pointerup` event, the event * would bubble up to the document and trigger a `pointerUpOutside` event. We avoid * this by delaying the event listener registration on the document. * This is not React specific, but rather how the DOM works, ie: * ``` * button.addEventListener('pointerup', () => { * console.log('I will log'); * document.addEventListener('pointerup', () => { * console.log('I will also log'); * }) * }); */ const timerId = window.setTimeout(() => { ownerDocument.addEventListener("pointerup", handlePointerUp); ownerDocument.addEventListener("pointercancel", handlePointerCancel); }, 0); return () => { window.clearTimeout(timerId); ownerDocument.removeEventListener("pointerup", handlePointerUp); ownerDocument.removeEventListener("pointercancel", handlePointerCancel); }; }, [ownerDocument, handlePointerUpOutside, enabled]); /** * Ensures we check React component tree (not just DOM tree). * This makes sure that if you start or end a pointer interaction inside the * React tree (e.g. Modal), we don't trigger the outside event on pointer up. */ return { onPointerDownCapture: () => { isPointerInsideReactTreeRef.current = true; }, onPointerUpCapture: () => { isPointerInsideReactTreeRef.current = true; }, }; }