Skip to content

[FileUpload, FileDropzone]: add validator, hint, error and success props and report every rejection reason #999

Description

@ly-tempel-bitweb

Problem

A form field can show at most one hint and one validation message at the same time. The validation message is either an error or a success, and it comes first, then the hint. The two must not cancel each other out.

FileUpload and FileDropzone don't follow this:

  • FileUpload:
    • When the component rejects a file, the restrictions hint disappears and only the error remains.
    • When a hint is passed through helper, it renders before the component's error.
    • helper holds a single feedback text, so a hint and an error can't be passed together.
    • An error passed through helper turns the border red but doesn't set aria-invalid.
  • FileDropzone:
    • When the component rejects a file, an error passed through helper is hidden until the rejection clears.
    • helper holds a single feedback text, as in FileUpload.

They also only enforce two restrictions: accept and maxSize. Consumers can't add a rule of their own, such as a minimum size, rejecting empty files, or a file naming convention. The only workaround is controlled mode, where the consumer sets isValid: false on files in onChange. Two things are still missing that way:

  • The rejection reason doesn't reach the error under the field or the screen-reader announcement. The user sees a danger tag but not why.
  • The consumer has to repeat the duplicate and single-file handling that useFileUpload already does.

Their messages also lag behind Angular FileUpload:

  • Only the first reason is reported. A file that fails both the type and the size check is reported for one of them.
  • The texts use "Fail(id)" / "File(s)" for every case, so a single file reads ungrammatically ("Fail(id) 'a.pdf' on liiga suured").
  • The screen-reader text for an invalid file is "Faili üleslaadimine ebaõnnestus". That's misleading for a file that was rejected rather than failed to upload.

FileUpload's layout and stories also differ from Figma and Angular FileUpload:

  • In the default size, the add button's "Lisa manus" text is 16px. It should be 14px, as in the small size.
  • The gap between the clear button and the separator is too large. It should be 8px.
  • The stories don't match Angular's. The Disabled, ValidationFailed and ValidationSuccess states are separate stories, and the list differs.

Solution

  1. Take the hint, error and success as separate props:

    • hint: guidance shown after the automatic allowed-types and size hint, in one hint.
    • error: an error of your own, such as a server error, combined with the component's own rejection message into one error.
    • success: shown in the error's place while there is no error.

    Keep the restrictions hint visible when there's an error. Render the validation message before the hint. Set aria-invalid whenever an error is shown, whatever its source. Decide whether helper stays as a deprecated alias for hint.

  2. Add an optional validator prop to UseFileUploadProps, so both components get it:

    export type FileUploadValidator = (file: File) => string | null | undefined;

    It returns the reason a file is rejected, or nothing to accept it.

    • It runs inside useFileUpload on every selected file, including ones accept or maxSize rejected, so it may receive a file of the wrong type or size.
    • A file it rejects is handled like a built-in rejection. It gets isValid: false and is kept or discarded according to keepRejectedFiles.
    • The restrictions hint can't describe the rule, so consumers describe it in hint.
    • It only checks files picked after it is set, like accept.
    • It is synchronous. Server-side checks stay with the consumer, who sets isLoading and isValid on the file.
    • FileDropzone should not pass it to react-dropzone's own validator option. Running it in the hook sends picked and dropped files through one path.
  3. Report every reason per file in one error:

    • Built-in reasons come first, grouped by reason.
    • Then each validator reason, followed by the names of the files it rejected. Files failing the same rule are grouped.
    • A trailing full stop on a validator reason is trimmed, because the messages are joined with . .
    • With keepRejectedFiles, build the error from the rejected files still listed, so earlier rejections survive later selections.
  4. Give the file messages singular and plural forms. Pass the file count to file-upload.size-rejected, file-upload.extension-rejected, file-upload.duplicates-skipped and file-upload.added, and pick the form from it.

  5. Change the file-upload.failed texts to "Sobimatu fail" / "Invalid file" / "Недопустимый файл". Keep the key name.

  6. Fix the FileUpload layout:

    • Use the small add button whenever the buttons sit beside the files, whatever size is, so its text is 14px.
    • Set an 8px gap between the clear button and the separator, with the separator's own spacing removed.
  7. Align the FileUpload stories with Angular:

    • Combine Disabled, ValidationFailed and ValidationSuccess into one States story.
    • Match the rest of the list and the wording to Angular FileUpload's stories.

Tests should cover:

  • hint, error and success render in the right order, and success is hidden while there's an error.
  • A file failing several rules gets every reason in the one error.
  • Files rejected by the validator are kept, or discarded when keepRejectedFiles is off.
  • Files failing the same validator rule are grouped in the error.
  • A validator returning null, undefined or '' accepts the file.

Also add stories for each component and document the new props.

DoD

  • The component is developed using best practices, conventions, and modern frontend trends
  • The component follows BEM naming conventions (if applicable)
  • The component implementation matches the Figma design, including all defined states and interactions
  • The component supports responsiveness and scales across breakpoints (if applicable)
  • The component does not introduce runtime errors or warnings
  • Linter passes
  • The component is exported from its tier barrel
  • The component has unit tests with at least 80% code coverage (enforced by the global Jest coverageThreshold over src/tedi/components, not per component)
  • CodeCov passes its threshold (80% on the changed code, with 10% tolerance — a separate gate from the Jest one above)
  • The component has passed code review
  • The component has passed Chromatic visual review
  • Storybook stories are created:
    • Cover all Figma variations
    • Include edge cases, empty/error states
  • Component’s API (props, slots, events) is documented
  • ZeroHeight is updated with design/development guidelines
  • External dependencies used in the component are documented in Storybook and Zeroheight, with rationale
  • Semantic commit format type(scope): Subject text #issue-ref is used (for semantic-release), with the issue reference required - see Commit convention
  • External libraries comply with validated open source licenses
  • Component is versioned correctly and ready for release if required
  • Update the React development status on the statuses page in Zeroheight
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

tedi-readyTEDI-Ready component issue

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions