From 74f3503983516cb6b1242bedf68c23561c8c2578 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Fri, 2 Oct 2026 16:31:32 +0200 Subject: [PATCH] fix(prompt-input): match file extension patterns in accept An accept entry like .xlsx never matched in matchesAccept, which only compares MIME types, so files allowed by the file dialog were dropped when added, pasted or dropped. Extension patterns are now matched against the file name, case-insensitive, as the HTML accept attribute does. --- .changeset/prompt-input-accept-extensions.md | 5 ++ .../components/(chatbot)/prompt-input.mdx | 2 +- .../elements/__tests__/prompt-input.test.tsx | 67 +++++++++++++++++++ packages/elements/src/prompt-input.tsx | 6 +- 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 .changeset/prompt-input-accept-extensions.md diff --git a/.changeset/prompt-input-accept-extensions.md b/.changeset/prompt-input-accept-extensions.md new file mode 100644 index 00000000..76ac5b6b --- /dev/null +++ b/.changeset/prompt-input-accept-extensions.md @@ -0,0 +1,5 @@ +--- +"ai-elements": patch +--- + +Match file extension patterns such as `.pdf` in the PromptInput `accept` prop diff --git a/apps/docs/content/components/(chatbot)/prompt-input.mdx b/apps/docs/content/components/(chatbot)/prompt-input.mdx index ad2c9f5b..c8487f80 100644 --- a/apps/docs/content/components/(chatbot)/prompt-input.mdx +++ b/apps/docs/content/components/(chatbot)/prompt-input.mdx @@ -283,7 +283,7 @@ Buttons can display tooltips with optional keyboard shortcut hints. Hover over t }, accept: { description: - 'File types to accept (e.g., "image/*"). Leave undefined for any.', + 'File types to accept, as MIME types or extensions (e.g., "image/*" or ".pdf"). Leave undefined for any.', type: "string", }, multiple: { diff --git a/packages/elements/__tests__/prompt-input.test.tsx b/packages/elements/__tests__/prompt-input.test.tsx index d961a3ba..6af997a4 100644 --- a/packages/elements/__tests__/prompt-input.test.tsx +++ b/packages/elements/__tests__/prompt-input.test.tsx @@ -1685,6 +1685,73 @@ describe("file validation", () => { }); }); + it("matches file extension patterns against the file name", async () => { + setupPromptInputTests(); + const onSubmit = vi.fn(); + const onError = vi.fn(); + const user = userEvent.setup(); + + // Browsers often report an empty or generic type for these files + const sheetFile = new File(["sheet"], "report.xlsx", { type: "" }); + const csvFile = new File(["a,b"], "DATA.CSV", { + type: "application/octet-stream", + }); + const textFile = new File(["text"], "notes.txt", { type: "text/plain" }); + + const AttachmentConsumer = () => { + const attachments = usePromptInputAttachments(); + return ( + <> + + + +
{attachments.files.length}
+ + ); + }; + + render( + + + + + + + ); + + await user.click(screen.getByTestId("add-sheet")); + expect(screen.getByTestId("count")).toHaveTextContent("1"); + + await user.click(screen.getByTestId("add-csv")); + expect(screen.getByTestId("count")).toHaveTextContent("2"); + expect(onError).not.toHaveBeenCalled(); + + await user.click(screen.getByTestId("add-text")); + expect(screen.getByTestId("count")).toHaveTextContent("2"); + expect(onError).toHaveBeenCalledWith({ + code: "accept", + message: expect.any(String), + }); + }); + it("handles accept with extra whitespace in patterns", async () => { setupPromptInputTests(); const onSubmit = vi.fn(); diff --git a/packages/elements/src/prompt-input.tsx b/packages/elements/src/prompt-input.tsx index 412c846d..8bdf9101 100644 --- a/packages/elements/src/prompt-input.tsx +++ b/packages/elements/src/prompt-input.tsx @@ -490,7 +490,7 @@ export type PromptInputProps = Omit< HTMLAttributes, "onSubmit" | "onError" > & { - // e.g., "image/*" or leave undefined for any + // e.g., "image/*", ".pdf" or leave undefined for any accept?: string; multiple?: boolean; // When true, accepts drops anywhere on document. Default false (opt-in). @@ -564,6 +564,10 @@ export const PromptInput = ({ .filter(Boolean); return patterns.some((pattern) => { + if (pattern.startsWith(".")) { + // e.g: .xlsx -> matched against the file name, case-insensitive + return f.name.toLowerCase().endsWith(pattern.toLowerCase()); + } if (pattern.endsWith("/*")) { // e.g: image/* -> image/ const prefix = pattern.slice(0, -1);