From 7699d793fe0eca3834c3ece834ef508ecb631703 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Mon, 20 Jul 2020 11:03:38 -0400 Subject: [PATCH 1/2] Update the resolver element ref on scroll events if the position of the element has changed within the page --- .../public/resolver/view/use_camera.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.ts b/x-pack/plugins/security_solution/public/resolver/view/use_camera.ts index 6a1c35be57000b..fd64ecaa7bd3ba 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.ts @@ -284,7 +284,8 @@ export function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | n const [rect, setRect] = useState(null); // Using state as ref.current update does not trigger effect hook when reset const [currentNode, setCurrentNode] = useState(null); - + const [currentY, setCurrentY] = useState(0); + const [currentX, setCurrentX] = useState(0); const ref = useCallback((node: Element | null) => { setCurrentNode(node); if (node !== null) { @@ -292,6 +293,21 @@ export function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | n } }, []); const { ResizeObserver } = useContext(SideEffectContext); + const handleScroll = useCallback(() => { + if (currentX !== window.scrollX || currentY !== window.scrollY) { + if (currentNode !== null) { + setRect(currentNode.getBoundingClientRect()); + setCurrentY(window.scrollY); + setCurrentX(window.scrollX); + } + } + }, [currentX, currentY, setCurrentX, setCurrentY, currentNode]); + useEffect(() => { + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, [handleScroll]); useEffect(() => { if (currentNode !== null) { const resizeObserver = new ResizeObserver((entries) => { From 8838deaf624e9d9cd3f283b8d55a2918b9dc2026 Mon Sep 17 00:00:00 2001 From: Kevin Qualters Date: Mon, 20 Jul 2020 15:04:52 -0400 Subject: [PATCH 2/2] Refactor useEffect for handling scroll --- .../public/resolver/view/use_camera.ts | 60 ++++++++++++++----- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.ts b/x-pack/plugins/security_solution/public/resolver/view/use_camera.ts index fd64ecaa7bd3ba..661e038d04e32e 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.ts @@ -281,33 +281,61 @@ export function useCamera(): { * handle that. */ export function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | null) => void] { + // This hooks returns `rect`. const [rect, setRect] = useState(null); - // Using state as ref.current update does not trigger effect hook when reset + + const { ResizeObserver, requestAnimationFrame } = useContext(SideEffectContext); + + // Keep the current DOM node in state so that we can create a ResizeObserver for it via `useEffect`. const [currentNode, setCurrentNode] = useState(null); - const [currentY, setCurrentY] = useState(0); - const [currentX, setCurrentX] = useState(0); + + // `ref` will be used with a react element. When the element is available, this function will be called. const ref = useCallback((node: Element | null) => { + // track the node in state setCurrentNode(node); - if (node !== null) { - setRect(node.getBoundingClientRect()); - } }, []); - const { ResizeObserver } = useContext(SideEffectContext); - const handleScroll = useCallback(() => { - if (currentX !== window.scrollX || currentY !== window.scrollY) { - if (currentNode !== null) { - setRect(currentNode.getBoundingClientRect()); - setCurrentY(window.scrollY); - setCurrentX(window.scrollX); - } + + /** + * Any time the DOM node changes (to something other than `null`) recalculate the DOMRect and set it (which will cause it to be returned from the hook. + * This effect re-runs when the DOM node has changed. + */ + useEffect(() => { + if (currentNode !== null) { + // When the DOM node is received, immedaiately calculate its DOM Rect and return that + setRect(currentNode.getBoundingClientRect()); } - }, [currentX, currentY, setCurrentX, setCurrentY, currentNode]); + }, [currentNode]); + + /** + * When scroll events occur, recalculate the DOMRect. DOMRect represents the position of an element relative to the viewport, so that may change during scroll (depending on the layout.) + * This effect re-runs when the DOM node has changed. + */ useEffect(() => { + // the last scrollX and scrollY values that we handled + let previousX: number = window.scrollX; + let previousY: number = window.scrollY; + + const handleScroll = () => { + requestAnimationFrame(() => { + // synchronously read from the DOM + const currentX = window.scrollX; + const currentY = window.scrollY; + + if (currentNode !== null && (previousX !== currentX || previousY !== currentY)) { + setRect(currentNode.getBoundingClientRect()); + } + + previousX = currentX; + previousY = currentY; + }); + }; + window.addEventListener('scroll', handleScroll, { passive: true }); return () => { window.removeEventListener('scroll', handleScroll); }; - }, [handleScroll]); + }, [currentNode, requestAnimationFrame]); + useEffect(() => { if (currentNode !== null) { const resizeObserver = new ResizeObserver((entries) => {