Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 23 additions & 14 deletions apps/web/core/components/core/render-if-visible-HOC.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import type { ReactNode, MutableRefObject } from "react";
import React, { useState, useRef, useEffect } from "react";
import { cn } from "@plane/utils";
import { runIdleTask } from "@/lib/idle-task";

type Props = {
defaultHeight?: string;
Expand All @@ -19,7 +20,7 @@ type Props = {
placeholderChildren?: ReactNode;
defaultValue?: boolean;
shouldRecordHeights?: boolean;
useIdletime?: boolean;
useIdleTime?: boolean;
forceRender?: boolean;
};

Expand All @@ -36,25 +37,29 @@ function RenderIfVisible(props: Props) {
//placeholder children
placeholderChildren = null, //placeholder children
defaultValue = false,
useIdletime = false,
useIdleTime = false,
forceRender = false,
} = props;
const [shouldVisible, setShouldVisible] = useState<boolean>(defaultValue);
const placeholderHeight = useRef<string>(defaultHeight);
const intersectionRef = useRef<HTMLElement | null>(null);
const visibilityIdleTaskRef = useRef<ReturnType<typeof runIdleTask> | null>(null);
const heightIdleTaskRef = useRef<ReturnType<typeof runIdleTask> | null>(null);

const isVisible = shouldVisible || forceRender;

// Set visibility with intersection observer
useEffect(() => {
if (intersectionRef.current) {
const target = intersectionRef.current;
if (target) {
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), {
timeout: 300,
});
if (typeof window !== "undefined" && useIdleTime) {
visibilityIdleTaskRef.current?.cancel();
visibilityIdleTaskRef.current = runIdleTask(() =>
setShouldVisible(entries[entries.length - 1].isIntersecting)
);
} else {
setShouldVisible(entries[entries.length - 1].isIntersecting);
}
Expand All @@ -64,23 +69,27 @@ function RenderIfVisible(props: Props) {
rootMargin: `${verticalOffset}% ${horizontalOffset}% ${verticalOffset}% ${horizontalOffset}%`,
}
);
observer.observe(intersectionRef.current);
observer.observe(target);
return () => {
if (intersectionRef.current) {
// eslint-disable-next-line react-hooks/exhaustive-deps
observer.unobserve(intersectionRef.current);
}
visibilityIdleTaskRef.current?.cancel();
visibilityIdleTaskRef.current = null;
observer.unobserve(target);
};
}
}, [intersectionRef, children, root, verticalOffset, horizontalOffset]);
}, [intersectionRef, root, verticalOffset, horizontalOffset, useIdleTime]);

//Set height after render
useEffect(() => {
if (intersectionRef.current && isVisible && shouldRecordHeights) {
window.requestIdleCallback(() => {
heightIdleTaskRef.current?.cancel();
heightIdleTaskRef.current = runIdleTask(() => {
if (intersectionRef.current) placeholderHeight.current = `${intersectionRef.current.offsetHeight}px`;
});
}
return () => {
heightIdleTaskRef.current?.cancel();
heightIdleTaskRef.current = null;
};
}, [isVisible, intersectionRef, shouldRecordHeights]);

const child = isVisible ? <>{children}</> : placeholderChildren;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export const KanBan = observer(function KanBan(props: IKanBan) {
/>
}
defaultValue={groupIndex < 5 && subGroupIndex < 2}
useIdletime
useIdleTime
>
<KanbanGroup
groupId={subList.id}
Expand Down
27 changes: 27 additions & 0 deletions apps/web/core/lib/idle-task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/

export type IdleTaskHandle = {
cancel: () => void;
};

/**
* Schedule lightweight work for idle time and return a cancel handle.
* Falls back to setTimeout when requestIdleCallback is unavailable.
*/
export const runIdleTask = (callback: () => void): IdleTaskHandle => {
if (typeof window !== "undefined" && typeof window.requestIdleCallback === "function") {
const idleId = window.requestIdleCallback(callback, { timeout: 300 });
return {
cancel: () => window.cancelIdleCallback(idleId),
};
}

const timeoutId = window.setTimeout(callback, 0);
return {
cancel: () => window.clearTimeout(timeoutId),
};
};