Skip to content
Merged
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
18 changes: 6 additions & 12 deletions apps/web/src/features/shared/textarea-autocomplete/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import React, {
forwardRef,
MutableRefObject,
useEffect,
useState
} from "react";
import React, { forwardRef, MutableRefObject, useState } from "react";
import { useActiveAccount } from "@/core/hooks/use-active-account";
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
import { lookupAccountsQueryOptions, searchPath } from "@ecency/sdk";
Expand All @@ -22,7 +17,6 @@ export const TextareaAutocomplete = forwardRef<HTMLTextAreaElement, any>((props,
const isMobile = useIsMobile();
const queryClient = useQueryClient();

const [value, setValue] = useState(props.value);
const [rows, setRows] = useState(props.minrows || 10);
const [minrows, setMinrows] = useState(props.minrows || 10);
const [maxrows, setMaxrows] = useState(props.maxrows || 20);
Expand All @@ -35,9 +29,10 @@ export const TextareaAutocomplete = forwardRef<HTMLTextAreaElement, any>((props,
attrs.rows = isComment ? rows : rows;
}

useEffect(() => {
setValue(props.value);
}, [props.value, value]);
// The textarea is fully controlled by the parent's `value` prop. Mirroring it
// into local state (and re-syncing it in an effect keyed on that local state)
// made every change ping-pong between the two and tripped React's
// "Maximum update depth exceeded" guard.

const handleChange = (event: any) => {
const isMobile = typeof window !== "undefined" && window.innerWidth < 570;
Expand All @@ -60,7 +55,6 @@ export const TextareaAutocomplete = forwardRef<HTMLTextAreaElement, any>((props,
setRows(currentRows < maxrows ? currentRows : maxrows);
}

setValue(event.target.value);
props.onChange(event);
};

Expand All @@ -75,7 +69,7 @@ export const TextareaAutocomplete = forwardRef<HTMLTextAreaElement, any>((props,
}
}}
loadingComponent={Loading}
value={value}
value={props.value ?? ""}
placeholder={props.placeholder}
onChange={handleChange}
{...(isComment ? {} : { boundariesElement: ".body-input" })}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { useEffect, useRef, useState } from "react";
import { act, render } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { insertOrReplace } from "@/utils/input-util";

// The autocomplete library is not mocked, so the component runs as shipped.
vi.mock("@/core/hooks/use-active-account", () => ({
useActiveAccount: () => ({ activeUser: null })
}));
vi.mock("@ecency/sdk", () => ({ lookupAccountsQueryOptions: vi.fn(), searchPath: vi.fn() }));
vi.mock("@/features/shared", () => ({ UserAvatar: () => null }));
vi.mock("@/utils", () => ({ useIsMobile: () => false }));
vi.mock("@tanstack/react-query", () => ({ useQueryClient: () => ({}) }));

import { TextareaAutocomplete } from "@/features/shared/textarea-autocomplete";

const placeholder = (i: number) => `![Uploading image-${i}.png]()\n\n`;

// What the parent saw: every change it was told about and the text it holds.
const parent = { changes: 0, text: "" };

// A parent that owns the text, as the comment box and the post editor do, with
// a native paste listener that inserts one placeholder per pasted image the way
// the editor toolbar does.
function Editor({ images }: { images: number }) {
const [text, setText] = useState("");
const ref = useRef<HTMLTextAreaElement>(null);
parent.text = text;

useEffect(() => {
const el = ref.current!;
const onPaste = () => {
for (let i = 0; i < images; i++) {
insertOrReplace(el as unknown as HTMLInputElement, placeholder(i));
}
};
el.addEventListener("paste", onPaste);
return () => el.removeEventListener("paste", onPaste);
}, [images]);

return (
<TextareaAutocomplete
as="textarea"
value={text}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
parent.changes += 1;
setText(e.target.value);
}}
ref={ref}
isComment={true}
minrows={3}
maxrows={100}
/>
);
}

describe("TextareaAutocomplete bulk insert", () => {
it("keeps every placeholder when many images are pasted at once", () => {
// React gives up after 50 nested updates. Each insert used to add roughly one, so
// the count has to be comfortably above that for the test to mean anything.
const images = 80;
parent.changes = 0;
parent.text = "";
const { container } = render(<Editor images={images} />);
const textarea = container.querySelector("textarea")!;

act(() => {
textarea.dispatchEvent(new Event("paste", { bubbles: true }));
});

const expected = Array.from({ length: images }, (_, i) => placeholder(i)).join("");
expect(parent.changes).toBe(images);
expect(parent.text).toBe(expected);
expect(textarea.value).toBe(expected);
});
});
Loading