feat: eye dropper (#6615)

This commit is contained in:
David Luzar 2023-06-02 17:06:11 +02:00 committed by GitHub
parent 644685a5a8
commit 079aa72475
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 803 additions and 250 deletions

View file

@ -1,42 +1,86 @@
import { useEffect, useRef } from "react";
import { useEffect } from "react";
import { EVENT } from "../constants";
export const useOutsideClick = (handler: (event: Event) => void) => {
const ref = useRef(null);
export function useOutsideClick<T extends HTMLElement>(
ref: React.RefObject<T>,
/** if performance is of concern, memoize the callback */
callback: (event: Event) => void,
/**
* Optional callback which is called on every click.
*
* Should return `true` if click should be considered as inside the container,
* and `false` if it falls outside and should call the `callback`.
*
* Returning `true` overrides the default behavior and `callback` won't be
* called.
*
* Returning `undefined` will fallback to the default behavior.
*/
isInside?: (
event: Event & { target: HTMLElement },
/** the element of the passed ref */
container: T,
) => boolean | undefined,
) {
useEffect(() => {
function onOutsideClick(event: Event) {
const _event = event as Event & { target: T };
useEffect(
() => {
const listener = (event: Event) => {
const current = ref.current as HTMLElement | null;
if (!ref.current) {
return;
}
// Do nothing if clicking ref's element or descendent elements
if (
!current ||
current.contains(event.target as Node) ||
[...document.querySelectorAll("[data-prevent-outside-click]")].some(
(el) => el.contains(event.target as Node),
)
) {
return;
}
const isInsideOverride = isInside?.(_event, ref.current);
handler(event);
};
if (isInsideOverride === true) {
return;
} else if (isInsideOverride === false) {
return callback(_event);
}
document.addEventListener("pointerdown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("pointerdown", listener);
document.removeEventListener("touchstart", listener);
};
},
// Add ref and handler to effect dependencies
// It's worth noting that because passed in handler is a new ...
// ... function on every render that will cause this effect ...
// ... callback/cleanup to run every render. It's not a big deal ...
// ... but to optimize you can wrap handler in useCallback before ...
// ... passing it into this hook.
[ref, handler],
);
// clicked element is in the descenendant of the target container
if (
ref.current.contains(_event.target) ||
// target is detached from DOM (happens when the element is removed
// on a pointerup event fired *before* this handler's pointerup is
// dispatched)
!document.documentElement.contains(_event.target)
) {
return;
}
return ref;
};
const isClickOnRadixPortal =
_event.target.closest("[data-radix-portal]") ||
// when radix popup is in "modal" mode, it disables pointer events on
// the `body` element, so the target element is going to be the `html`
// (note: this won't work if we selectively re-enable pointer events on
// specific elements as we do with navbar or excalidraw UI elements)
(_event.target === document.documentElement &&
document.body.style.pointerEvents === "none");
// if clicking on radix portal, assume it's a popup that
// should be considered as part of the UI. Obviously this is a terrible
// hack you can end up click on radix popups that outside the tree,
// but it works for most cases and the downside is minimal for now
if (isClickOnRadixPortal) {
return;
}
// clicking on a container that ignores outside clicks
if (_event.target.closest("[data-prevent-outside-click]")) {
return;
}
callback(_event);
}
// note: don't use `click` because it often reports incorrect `event.target`
document.addEventListener(EVENT.POINTER_DOWN, onOutsideClick);
document.addEventListener(EVENT.TOUCH_START, onOutsideClick);
return () => {
document.removeEventListener(EVENT.POINTER_DOWN, onOutsideClick);
document.removeEventListener(EVENT.TOUCH_START, onOutsideClick);
};
}, [ref, callback, isInside]);
}