feat: added loading indicator logic in mentions

This commit is contained in:
Palanikannan1437 2024-03-26 16:32:07 +05:30
parent 2f4fc63858
commit 48f913fbe6
3 changed files with 72 additions and 38 deletions

View File

@ -23,25 +23,6 @@ export const Mentions = ({
readonly: readonly, readonly: readonly,
mentionHighlights: mentionHighlights, mentionHighlights: mentionHighlights,
suggestion: { suggestion: {
items: async ({ query }) => {
const suggestions = await mentionSuggestions?.();
if (!suggestions) {
return [];
}
const mappedSuggestions: IMentionSuggestion[] = suggestions.map((suggestion): IMentionSuggestion => {
const transactionId = uuidv4();
return {
...suggestion,
id: transactionId,
};
});
const filteredSuggestions = mappedSuggestions
.filter((suggestion) => suggestion.title.toLowerCase().startsWith(query.toLowerCase()))
.slice(0, 5);
return filteredSuggestions;
},
// @ts-ignore // @ts-ignore
render: () => { render: () => {
let component: ReactRenderer | null = null; let component: ReactRenderer | null = null;
@ -56,11 +37,11 @@ export const Mentions = ({
return; return;
} }
component = new ReactRenderer(MentionList, { component = new ReactRenderer(MentionList, {
props, props: { ...props, mentionSuggestions },
editor: props.editor, editor: props.editor,
}); });
props.editor.storage.mentionsOpen = true; props.editor.storage.mentionsOpen = true;
// @ts-ignore // @ts-expect-error - Tippy types are incorrect
popup = tippy("body", { popup = tippy("body", {
getReferenceClientRect: props.clientRect, getReferenceClientRect: props.clientRect,
appendTo: () => document.body, appendTo: () => document.body,
@ -70,7 +51,7 @@ export const Mentions = ({
trigger: "manual", trigger: "manual",
placement: "bottom-start", placement: "bottom-start",
}); });
document.addEventListener("scroll", hidePopup, true); // document.addEventListener("scroll", hidePopup, true);
}, },
onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => { onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
component?.updateProps(props); component?.updateProps(props);
@ -107,7 +88,7 @@ export const Mentions = ({
popup?.[0].destroy(); popup?.[0].destroy();
component?.destroy(); component?.destroy();
document.removeEventListener("scroll", hidePopup, true); // document.removeEventListener("scroll", hidePopup, true);
}, },
}; };
}, },

View File

@ -1,9 +1,9 @@
import { Editor } from "@tiptap/react"; import { Editor } from "@tiptap/react";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
import { IMentionSuggestion } from "src/types/mention-suggestion"; import { IMentionSuggestion } from "src/types/mention-suggestion";
import { v4 as uuidv4 } from "uuid";
interface MentionListProps { interface MentionListProps {
items: IMentionSuggestion[];
command: (item: { command: (item: {
id: string; id: string;
label: string; label: string;
@ -12,14 +12,42 @@ interface MentionListProps {
target: string; target: string;
redirect_uri: string; redirect_uri: string;
}) => void; }) => void;
query: string;
editor: Editor; editor: Editor;
mentionSuggestions: () => Promise<IMentionSuggestion[]>;
} }
export const MentionList = forwardRef((props: MentionListProps, ref) => { export const MentionList = forwardRef((props: MentionListProps, ref) => {
const { query, mentionSuggestions } = props;
const [items, setItems] = useState<IMentionSuggestion[]>([]);
const [selectedIndex, setSelectedIndex] = useState(0); const [selectedIndex, setSelectedIndex] = useState(0);
const [isLoading, setIsLoading] = useState(false); // New loading state
useEffect(() => {
const fetchSuggestions = async () => {
setIsLoading(true); // Start loading
const suggestions = await mentionSuggestions();
const mappedSuggestions: IMentionSuggestion[] = suggestions.map((suggestion): IMentionSuggestion => {
const transactionId = uuidv4();
return {
...suggestion,
id: transactionId,
};
});
const filteredSuggestions = mappedSuggestions
.filter((suggestion) => suggestion.title.toLowerCase().startsWith(query.toLowerCase()))
.slice(0, 5);
setItems(filteredSuggestions);
setIsLoading(false); // End loading
};
fetchSuggestions();
}, [query, mentionSuggestions]);
const selectItem = (index: number) => { const selectItem = (index: number) => {
const item = props.items[index]; const item = items[index];
if (item) { if (item) {
props.command({ props.command({
@ -33,12 +61,35 @@ export const MentionList = forwardRef((props: MentionListProps, ref) => {
} }
}; };
const commandListContainer = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const container = commandListContainer?.current;
const item = container?.children[selectedIndex] as HTMLElement;
if (item && container) updateScrollView(container, item);
}, [selectedIndex]);
const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
const containerHeight = container.offsetHeight;
const itemHeight = item ? item.offsetHeight : 0;
const top = item.offsetTop;
const bottom = top + itemHeight;
if (top < container.scrollTop) {
container.scrollTop -= container.scrollTop - top + 5;
} else if (bottom > containerHeight + container.scrollTop) {
container.scrollTop += bottom - containerHeight - container.scrollTop + 5;
}
};
const upHandler = () => { const upHandler = () => {
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length); setSelectedIndex((selectedIndex + items.length - 1) % items.length);
}; };
const downHandler = () => { const downHandler = () => {
setSelectedIndex((selectedIndex + 1) % props.items.length); setSelectedIndex((selectedIndex + 1) % items.length);
}; };
const enterHandler = () => { const enterHandler = () => {
@ -47,7 +98,7 @@ export const MentionList = forwardRef((props: MentionListProps, ref) => {
useEffect(() => { useEffect(() => {
setSelectedIndex(0); setSelectedIndex(0);
}, [props.items]); }, [items]);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
onKeyDown: ({ event }: { event: KeyboardEvent }) => { onKeyDown: ({ event }: { event: KeyboardEvent }) => {
@ -70,10 +121,15 @@ export const MentionList = forwardRef((props: MentionListProps, ref) => {
}, },
})); }));
return props.items && props.items.length !== 0 ? ( return (
<div className="mentions absolute max-h-40 w-48 space-y-0.5 overflow-y-auto rounded-md bg-custom-background-100 p-1 text-sm text-custom-text-300 shadow-custom-shadow-sm"> <div
{props.items.length ? ( ref={commandListContainer}
props.items.map((item, index) => ( className="mentions absolute max-h-40 w-48 space-y-0.5 overflow-y-auto rounded-md bg-custom-background-100 p-1 text-sm text-custom-text-300 shadow-custom-shadow-sm"
>
{isLoading ? (
<div className="flex justify-center items-center h-full text-gray-500">Loading...</div>
) : items.length ? (
items.map((item, index) => (
<div <div
key={item.id} key={item.id}
className={`flex cursor-pointer items-center gap-2 rounded p-1 hover:bg-custom-background-80 ${ className={`flex cursor-pointer items-center gap-2 rounded p-1 hover:bg-custom-background-80 ${
@ -92,16 +148,13 @@ export const MentionList = forwardRef((props: MentionListProps, ref) => {
</div> </div>
<div className="flex-grow space-y-1 truncate"> <div className="flex-grow space-y-1 truncate">
<p className="truncate text-sm font-medium">{item.title}</p> <p className="truncate text-sm font-medium">{item.title}</p>
{/* <p className="text-xs text-gray-400">{item.subtitle}</p> */}
</div> </div>
</div> </div>
)) ))
) : ( ) : (
<div className="item">No result</div> <div className="flex justify-center items-center h-full">No results</div>
)} )}
</div> </div>
) : (
<></>
); );
}); });

View File

@ -2722,7 +2722,7 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react@*", "@types/react@^18.2.42": "@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42":
version "18.2.42" version "18.2.42"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA== integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==