diff --git a/apps/web/core/components/core/render-if-visible-HOC.tsx b/apps/web/core/components/core/render-if-visible-HOC.tsx index bd8a77a04a2..4e40869c65c 100644 --- a/apps/web/core/components/core/render-if-visible-HOC.tsx +++ b/apps/web/core/components/core/render-if-visible-HOC.tsx @@ -7,6 +7,7 @@ import type { ReactNode, MutableRefObject } from "react"; import React, { useState, useRef, useEffect } from "react"; import { cn } from "@plane/utils"; +import { safeCancelIdleCallback, safeRequestIdleCallback } from "@/lib/polyfills"; type Props = { defaultHeight?: string; @@ -47,12 +48,15 @@ function RenderIfVisible(props: Props) { // Set visibility with intersection observer useEffect(() => { - if (intersectionRef.current) { + const node = intersectionRef.current; + if (node) { + let idleHandle: number | undefined; const observer = new IntersectionObserver( (entries) => { //DO no remove comments for future - if (typeof window !== undefined && window.requestIdleCallback && useIdletime) { - window.requestIdleCallback(() => setShouldVisible(entries[entries.length - 1].isIntersecting), { + if (useIdletime) { + if (idleHandle !== undefined) safeCancelIdleCallback(idleHandle); + idleHandle = safeRequestIdleCallback(() => setShouldVisible(entries[entries.length - 1].isIntersecting), { timeout: 300, }); } else { @@ -64,20 +68,18 @@ function RenderIfVisible(props: Props) { rootMargin: `${verticalOffset}% ${horizontalOffset}% ${verticalOffset}% ${horizontalOffset}%`, } ); - observer.observe(intersectionRef.current); + observer.observe(node); return () => { - if (intersectionRef.current) { - // eslint-disable-next-line react-hooks/exhaustive-deps - observer.unobserve(intersectionRef.current); - } + observer.unobserve(node); + if (idleHandle !== undefined) safeCancelIdleCallback(idleHandle); }; } - }, [intersectionRef, children, root, verticalOffset, horizontalOffset]); + }, [intersectionRef, children, root, verticalOffset, horizontalOffset, useIdletime]); //Set height after render useEffect(() => { if (intersectionRef.current && isVisible && shouldRecordHeights) { - window.requestIdleCallback(() => { + safeRequestIdleCallback(() => { if (intersectionRef.current) placeholderHeight.current = `${intersectionRef.current.offsetHeight}px`; }); } diff --git a/apps/web/core/lib/polyfills/index.ts b/apps/web/core/lib/polyfills/index.ts index 2243dc620c2..1d7e735f420 100644 --- a/apps/web/core/lib/polyfills/index.ts +++ b/apps/web/core/lib/polyfills/index.ts @@ -27,4 +27,31 @@ if (typeof window !== "undefined" && window) { }; } -export {}; +// Defensive wrappers for use at call sites that may run before the side-effect +// above is applied (e.g., lazy-loaded chunks like gantt-layout-loader that can +// execute before app/provider.tsx finishes evaluating). +export const safeRequestIdleCallback: typeof window.requestIdleCallback = (cb, options) => { + if (typeof window !== "undefined" && window.requestIdleCallback) { + return window.requestIdleCallback(cb, options); + } + const start = Date.now(); + // setTimeout's return type is `number | NodeJS.Timeout` depending on the resolved + // typings; in a browser context (the only path that reaches this fallback) it is + // always `number`. Cast to satisfy the DOM-shaped IdleCallbackHandle return. + return setTimeout( + () => + cb({ + didTimeout: false, + timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), + }), + 1 + ) as unknown as number; +}; + +export const safeCancelIdleCallback: typeof window.cancelIdleCallback = (id) => { + if (typeof window !== "undefined" && window.cancelIdleCallback) { + window.cancelIdleCallback(id); + return; + } + clearTimeout(id); +};