feat(FileUpload): improve document upload experience#3918
feat(FileUpload): improve document upload experience#3918desiree-np wants to merge 3 commits intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the F0Form file upload field UI to match the latest Figma spec by replacing the legacy FileUploadItem row with a new FileAttachment card component and revising the dropzone styles/behavior (including grouped border radii and maxFiles support).
Changes:
- Introduces
FileAttachment(replacingFileUploadItem) with grouped-list positioning and updated visuals. - Rewrites
FileFieldRendererdropzone and list rendering to match the new design and addsmaxFileshandling. - Updates i18n defaults and adjusts
F0AvatarIconmdsizing; refreshes Storybook story coverage.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Dependency lockfile update (includes Storybook patch bump). |
| packages/react/src/patterns/F0FormField/stories/F0FormField.stories.tsx | Adds a multiple-file story and reformats file. |
| packages/react/src/patterns/F0Form/fields/file/types.ts | Adds maxFiles + updates types; renames upload item props to attachment props. |
| packages/react/src/patterns/F0Form/fields/file/FileUploadItem.tsx | Removes old upload row component. |
| packages/react/src/patterns/F0Form/fields/file/FileFieldRenderer.tsx | Updates dropzone UI/states; renders FileAttachment; adds maxFiles limit logic. |
| packages/react/src/patterns/F0Form/fields/file/FileAttachment.tsx | New attachment card component with grouped border radius support. |
| packages/react/src/lib/providers/i18n/i18n-provider-defaults.ts | Adds file upload strings (fileWeight, maxFilesReached). |
| packages/react/src/components/avatars/F0AvatarIcon/F0AvatarIcon.tsx | Adjusts md avatar icon size to match Figma. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| const addFiles = useCallback( | ||
| (files: File[]) => { | ||
| setValidationError(null) | ||
| setValidationError(null); | ||
|
|
||
| const filesToProcess = isMultiple ? files : [files[0]] | ||
| const filesToProcess = isMultiple ? files : [files[0]]; | ||
|
|
||
| for (const file of filesToProcess) { | ||
| const validationMsg = validateFile(file) | ||
| const validationMsg = validateFile(file); | ||
| if (validationMsg) { | ||
| setValidationError(validationMsg) | ||
| continue | ||
| setValidationError(validationMsg); | ||
| continue; | ||
| } | ||
|
|
||
| if (!resolvedUseUpload) { | ||
| console.warn( | ||
| "[F0Form] No useUpload hook provided. Pass useUpload to <F0Form> or to the file field config." | ||
| ) | ||
| "[F0Form] No useUpload hook provided. Pass useUpload to <F0Form> or to the file field config.", | ||
| ); | ||
| } | ||
|
|
||
| const key = `${file.name}-${file.size}-${Date.now()}-${Math.random()}` | ||
| const key = `${file.name}-${file.size}-${Date.now()}-${Math.random()}`; | ||
| setEntries((prev) => { | ||
| if (!isMultiple) return [{ key, file }] | ||
| return [...prev, { key, file }] | ||
| }) | ||
| if (!isMultiple) return [{ key, file }]; | ||
| return [...prev, { key, file }]; | ||
| }); | ||
| } |
There was a problem hiding this comment.
maxFiles is only used to hide the dropzone once the limit is reached, but addFiles() can still append more than maxFiles if the user selects/drops multiple files at once (e.g. 2 existing + select 5 with maxFiles=3). This makes maxFiles unenforced and can exceed the UI/UX contract.
Consider capping filesToProcess to the remaining slots when field.maxFiles is set, and surface an i18n error (e.g. translations.maxFilesReached with {{maxFiles}}) when the selection exceeds the limit.
| // In single mode, hide dropzone once a file entry exists. | ||
| // In multiple mode, hide dropzone if maxFiles limit is reached. | ||
| const isAtLimit = | ||
| isMultiple && field.maxFiles != null && entries.length >= field.maxFiles; | ||
| const showDropzone = !isAtLimit && (isMultiple || !hasFiles); |
There was a problem hiding this comment.
maxFiles introduces new behavior (hiding the dropzone at the limit and limiting additions), but the existing test suite for FileFieldRenderer doesn’t cover it (no maxFiles assertions). Adding tests for: (1) dropzone hidden when entries reach the limit, (2) selecting/dropping more than the remaining slots is prevented and shows maxFilesReached would help prevent regressions.
| import { useCallback, useId, useMemo, useRef, useState } from "react"; | ||
| import { ControllerRenderProps, FieldValues } from "react-hook-form"; | ||
|
|
There was a problem hiding this comment.
packages/react is formatted with oxfmt configured with semi:false (see packages/react/.oxfmtrc.jsonc:4), but this file now includes semicolons throughout. This will likely be rewritten by the formatter and may cause pnpm -C packages/react format:check to fail.
Please re-run the package formatter (e.g. pnpm -C packages/react format) so the diff matches repo formatting conventions.
| import { useCallback, useEffect, useRef, useState } from "react"; | ||
|
|
||
| import { F0AvatarFile } from "@/components/avatars/F0AvatarFile"; |
There was a problem hiding this comment.
This file uses semicolons, but packages/react formatting is handled by oxfmt with semi:false (packages/react/.oxfmtrc.jsonc:4). Please re-run pnpm -C packages/react format so the diff matches the repo’s formatting conventions (no semicolons).
| import type { Meta, StoryObj } from "@storybook/react-vite"; | ||
|
|
||
| import { useCallback, useRef, useState } from "react" | ||
| import { useCallback, useRef, useState } from "react"; | ||
|
|
There was a problem hiding this comment.
This story file has been reformatted with semicolons, but packages/react uses oxfmt with semi:false (packages/react/.oxfmtrc.jsonc:4). Please re-run pnpm -C packages/react format so formatting stays consistent and format:check doesn’t fail.
| import type { | ||
| F0BaseField, | ||
| F0BaseFieldRenderIfFunction, | ||
| CommonRenderIfCondition, | ||
| } from "../types" | ||
| } from "../types"; | ||
|
|
There was a problem hiding this comment.
This file now includes semicolons, but packages/react is formatted with oxfmt configured with semi:false (packages/react/.oxfmtrc.jsonc:4). Re-run pnpm -C packages/react format to align with repo formatting and avoid format:check failures.
| import { i18nCountries } from "./partials/countries"; | ||
|
|
||
| export const defaultTranslations = { | ||
| countries: i18nCountries, |
There was a problem hiding this comment.
This file was reformatted with semicolons, but packages/react uses oxfmt with semi:false (packages/react/.oxfmtrc.jsonc:4). Please re-run pnpm -C packages/react format so the diff matches project formatting conventions.
| import { F0Icon, F0IconProps, IconType } from "@/components/F0Icon"; | ||
| import { cn } from "@/lib/utils"; | ||
|
|
||
| import { BaseAvatarProps } from "../internal/BaseAvatar" | ||
| import { BaseAvatarProps } from "../internal/BaseAvatar"; | ||
|
|
There was a problem hiding this comment.
This file now uses semicolons, but packages/react is formatted with oxfmt configured with semi:false (packages/react/.oxfmtrc.jsonc:4). Re-run pnpm -C packages/react format to keep formatting consistent.
- Add FileAttachment component with position prop (single/top/middle/bottom) for grouped card lists with overlapping borders via -mt-px - Rewrite FileFieldRenderer: rounded-xl dropzone, drag/error border states, AlertCircle error hint, maxFiles support, position-aware card rendering - Remove FileUploadItem (replaced by FileAttachment) - Update F0AvatarIcon md size from size-9 to size-8 (36px → 32px) per Figma - Add fileWeight and maxFilesReached i18n keys - Add FileMultiple story to F0FormField.stories
03061f0 to
9b68e5e
Compare
✅ No New Circular DependenciesNo new circular dependencies detected. Current count: 0 |
📦 Alpha Package Version PublishedUse Use |
🔍 Visual review for your branch is published 🔍Here are the links to: |
Coverage Report for packages/react
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // In single mode, hide dropzone once a file entry exists. | ||
| // In multiple mode, hide dropzone if maxFiles limit is reached. | ||
| const isAtLimit = | ||
| isMultiple && field.maxFiles != null && entries.length >= field.maxFiles | ||
| const showDropzone = !isAtLimit && (isMultiple || !hasFiles) |
There was a problem hiding this comment.
New maxFiles behavior (hiding the dropzone / preventing extra uploads) isn’t covered by tests. Add a unit test that sets multiple: true + maxFiles, uploads/drops more than the limit, and asserts the limit is enforced (and that the dropzone/message state is correct).
| import userEvent from "@testing-library/user-event"; | ||
| import React from "react"; | ||
| import { describe, expect, it, vi } from "vitest"; | ||
| import { z } from "zod"; |
There was a problem hiding this comment.
This test file has been reformatted with semicolons, but packages/react/.oxfmtrc.jsonc sets semi: false. Please run the package formatter (or revert to the no-semicolon style) to keep formatting consistent and avoid format-check CI failures.
| fileTooLarge: "File exceeds {{maxSize}} MB limit", | ||
| invalidFileType: "File type not accepted. Accepted formats: {{types}}", | ||
| fileWeight: "File weight: {{size}}", | ||
| maxFilesReached: "Maximum {{maxFiles}} files", |
There was a problem hiding this comment.
maxFilesReached was added to the default i18n translations but is not referenced anywhere in packages/react/src (dead key). Either wire it into the file field UX (e.g., when reaching/exceeding maxFiles) or remove it to avoid unused translation drift.
| maxFilesReached: "Maximum {{maxFiles}} files", |
Summary
Rework of the file upload UI (
FileFieldRenderer+FileAttachment) to be pixel-accurate to the Figma spec.FileUploadItemis removed and replaced by the newFileAttachmentcomponent.Changes
FileAttachment.tsx(new)Replaces
FileUploadItem. Key additions:positionprop (single | top | middle | bottom) drives per-card border-radius so cards in a list share a rounded group-mt-pxon non-first cards to collapse double borders into a single 1px lineborder-f1-border-secondary(normal) /border-f1-border-critical(error)F0AvatarFile size="lg"(40px), filenametext-sm font-medium, subtitletext-sm text-f1-foreground-secondaryF0Button variant="outline" size="sm" hideLabel icon={Cross}FileFieldRenderer.tsx(rewritten)rounded-xl(16px) dropzoneborder-f1-border-selected-bold bg-f1-background-selectedborder-f1-border-critical-bold bg-f1-background-critical/10border-[1px](notborder-2)py-10(40px) vertical padding,gap-3between icon and text,gap-4between dropzone and card listAlertCircleicon +text-sm font-medium text-f1-foreground-critical(matchesInputMessagespattern)maxFilessupport — hides dropzone when limit is reachedtext-base(14px)FileUploadItem.tsx(deleted)Superseded by
FileAttachment.F0AvatarIcon.tsxmdsize corrected:size-9(36px) →size-8(32px) per Figmai18n-provider-defaults.tsfileWeight: "File weight: {{size}}"maxFilesReached: "Maximum {{maxFiles}} files"F0FormField.stories.tsxFileMultiplestoryDesign reference
Figma:
https://www.figma.com/design/pZzg1KTe9lpKTSGPUZa8OJ/Components30003:118744/30003:11886929978:1363130003:119126Token decisions
border-f1-border-secondary(6% opacity)border-f1-border(20% opacity)rounded-xl(16px)Notes
F0AvatarIcon mdsize change (32px) also affectsCommunityPost,ActivityItem,CardSelectable— all consumemdand will render correctly at the intended Figma sizepnpm tsc --noEmitpasses with zero errors