Fix issue chat composer dropzone scope

This commit is contained in:
Dotta committed 2026-04-24 13:31:02 -05:00
1 parent d845ff6d9b
commit 1feac07821
4 files changed
+91 -4

No files matched your search

@@ -69,12 +69,14 @@ vi.mock("./MarkdownEditor", () => ({
placeholder,
className,
contentClassName,
fileDropTarget,
}: {
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
className?: string;
contentClassName?: string;
fileDropTarget?: "editor" | "parent";
}, ref) => {
useImperativeHandle(ref, () => ({
focus: markdownEditorFocusMock,
@@ -85,6 +87,7 @@ vi.mock("./MarkdownEditor", () => ({
aria-label="Issue chat editor"
data-class-name={className}
data-content-class-name={contentClassName}
data-file-drop-target={fileDropTarget}
placeholder={placeholder}
value={value}
onChange={(event) => onChange?.(event.target.value)}
@@ -818,6 +821,7 @@ describe("IssueChatThread", () => {
expect(editor?.dataset.contentClassName).toContain("max-h-[28dvh]");
expect(editor?.dataset.contentClassName).toContain("overflow-y-auto");
expect(editor?.dataset.contentClassName).not.toContain("min-h-[72px]");
expect(editor?.dataset.fileDropTarget).toBe("parent");
act(() => {
root.unmount();
+2 -2
View File
@@ -2254,6 +2254,7 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
mentions={mentions}
onSubmit={handleSubmit}
imageUploadHandler={onImageUpload}
fileDropTarget="parent"
bordered={false}
contentClassName="max-h-[28dvh] overflow-y-auto pr-1 text-sm scrollbar-auto-hide"
/>
@@ -2270,7 +2271,6 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
<input
ref={attachInputRef}
type="file"
accept="image/png,image/jpeg,image/webp,image/gif"
className="hidden"
onChange={handleAttachFile}
/>
@@ -2279,7 +2279,7 @@ const IssueChatComposer = forwardRef<IssueChatComposerHandle, IssueChatComposerP
size="icon-sm"
onClick={() => attachInputRef.current?.click()}
disabled={attaching}
title="Attach image"
title="Attach file"
>
<Paperclip className="h-4 w-4" />
</Button>
+81
View File
@@ -148,6 +148,17 @@ async function flush() {
});
}
function createFileDragEvent(type: string) {
const event = new Event(type, { bubbles: true, cancelable: true }) as Event & {
dataTransfer: { types: string[]; files: File[]; dropEffect?: string };
};
event.dataTransfer = {
types: ["Files"],
files: [],
};
return event;
}
describe("MarkdownEditor", () => {
let container: HTMLDivElement;
let originalRangeRect: typeof Range.prototype.getBoundingClientRect;
@@ -319,6 +330,76 @@ describe("MarkdownEditor", () => {
root.unmount();
});
});
it("shows the editor-scoped dropzone by default when files are dragged over it", async () => {
const root = createRoot(container);
await act(async () => {
root.render(
<MarkdownEditor
value=""
onChange={() => {}}
placeholder="Markdown body"
imageUploadHandler={async () => "https://example.com/image.png"}
/>,
);
});
await flush();
const scope = container.querySelector('[data-testid="mdx-editor"]')?.parentElement as HTMLDivElement | null;
expect(scope).not.toBeNull();
act(() => {
scope?.dispatchEvent(createFileDragEvent("dragenter"));
});
expect(scope?.className).toContain("ring-1");
expect(container.textContent).toContain("Drop image to upload");
act(() => {
scope?.dispatchEvent(createFileDragEvent("dragleave"));
});
expect(scope?.className).not.toContain("ring-1");
await act(async () => {
root.unmount();
});
});
it("defers file-drop visuals to a parent container when requested", async () => {
const root = createRoot(container);
await act(async () => {
root.render(
<MarkdownEditor
value=""
onChange={() => {}}
placeholder="Markdown body"
imageUploadHandler={async () => "https://example.com/image.png"}
fileDropTarget="parent"
/>,
);
});
await flush();
const scope = container.querySelector('[data-testid="mdx-editor"]')?.parentElement as HTMLDivElement | null;
expect(scope).not.toBeNull();
act(() => {
scope?.dispatchEvent(createFileDragEvent("dragenter"));
});
expect(scope?.className).not.toContain("ring-1");
expect(container.textContent).not.toContain("Drop image to upload");
await act(async () => {
root.unmount();
});
});
it("anchors the mention menu inside the visual viewport when mobile offsets are present", () => {
expect(
computeMentionMenuPosition(
+4 -2
View File
@@ -68,6 +68,8 @@ interface MarkdownEditorProps {
imageUploadHandler?: (file: File) => Promise<string>;
/** Called when a non-image file is dropped onto the editor (e.g. .zip). */
onDropFile?: (file: File) => Promise<void>;
/** When set to `parent`, a wrapper owns drag/drop behavior and visuals. */
fileDropTarget?: "editor" | "parent";
bordered?: boolean;
/** List of mentionable entities. Enables @-mention autocomplete. */
mentions?: MentionOption[];
@@ -491,6 +493,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
onBlur,
imageUploadHandler,
onDropFile,
fileDropTarget = "editor",
bordered = true,
mentions,
onSubmit,
@@ -897,8 +900,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
return Array.from(evt.dataTransfer?.types ?? []).includes("Files");
}
const canDropImage = Boolean(imageUploadHandler);
const canDropFile = Boolean(imageUploadHandler || onDropFile);
const canDropFile = fileDropTarget === "editor" && Boolean(imageUploadHandler || onDropFile);
const handlePasteCapture = useCallback((event: ClipboardEvent<HTMLDivElement>) => {
const clipboard = event.clipboardData;
if (!clipboard || !ref.current) return;