Skip to content

The multiple-file upload lists every selected file twice, with two different remove buttons #338

Description

@phmatray

Problem / motivation

MudFileUpload renders its own file list in addition to the CustomContent drop zone FormCraft
supplies, so every selected file appears twice in the multiple-file upload — once as FormCraft's
chip and once as MudBlazor's, each with its own close button.

Measured with two files selected, rendering MudBlazorMultipleFileUploadComponent standalone:

.mud-chip count: 4                 ← for TWO files
.mud-chip-close-button count: 4
all buttons: 6                     ← the 4 close buttons, plus Browse and Clear All

Ancestry tells the two sets apart:

Chips Rendered under Text
FormCraft's div.mud-file-upload-custom-content a.png (1024 Bytes) — name and formatted size, from the MudChip loop at MudBlazorMultipleFileUploadComponent.razor:51
MudBlazor's span.mud-file-upload-filelist / div.mud-file-upload-files a.png — name only

Why it is worth fixing, beyond looking wrong

  1. There are two removal paths for the same file, and they are not equivalent. MudBlazor's close
    button runs MudFileUpload's internal removal, not FormCraft's RemoveFile — so it does not get
    Five more controls drop keyboard focus when they unmount or disable themselves #318's focus move, and it does not go through FormCraft's own value handling. Two buttons that look
    the same and behave differently is worse than either alone.
  2. It makes DOM assertions ambiguous, and has already cost time. fix(mudblazor): move focus deliberately after a self-unmounting control (#318) #324's chip tests had to scope to
    .mud-file-upload-custom-content .mud-chip-close-button precisely because a bare
    .mud-chip-close-button matches twice per file. Anyone writing upload tests later walks into the
    same trap — and a test that silently drives MudBlazor's copy asserts nothing about FormCraft.

This is pre-existing and the model stays correct either way, which is why it is low priority rather
than a defect in behaviour. But it is very unlikely to be intended: the component goes to real trouble
to render a custom drop zone with its own chips, and then MudBlazor lists the files again underneath.

Proposed solution

Render the file list once. Establish first — by measurement, not by reading the docs — whether
MudFileUpload can be told not to render its built-in list when CustomContent is supplied.

  • If it can be suppressed: suppress it. FormCraft's chips carry more information (formatted size)
    and, critically, the close button that routes through RemoveFile and therefore through Five more controls drop keyboard focus when they unmount or disable themselves #318's focus
    handling and FormCraft's value notification.
  • If it cannot: decide deliberately between dropping FormCraft's own chips in favour of MudBlazor's
    (losing the size text and the RemoveFile route, so probably not) and accepting the duplication with
    a documented reason. Do not leave it undecided — the point of this issue is to stop it being
    accidental.

Check the single-file component for the same thing rather than assuming: it binds Files one-way
with an explicit FilesChanged handler where the multiple-file component uses @bind-Files, so its
behaviour here may genuinely differ.

Alternatives considered

  • Leave it, and only document the selector trap. Cheapest, and it addresses the test-ambiguity half.
    Rejected as the whole answer: it leaves two visible remove buttons per file with different behaviour,
    which is a real if minor user-facing defect, not just a testing inconvenience.
  • Drop FormCraft's chips and keep MudBlazor's. Simplest markup. Rejected as the default: it loses the
    formatted size, and it routes removal away from RemoveFile — which means away from Five more controls drop keyboard focus when they unmount or disable themselves #318's focus move
    and FormCraft's own value handling. Only worth revisiting if MudBlazor's list proves unsuppressable.
  • Hide MudBlazor's list with CSS. Rejected outright: the buttons stay in the DOM and in the
    accessibility tree, so screen-reader and keyboard users still meet a duplicate remove control that
    bypasses FormCraft. It would hide the symptom from sighted users only.

Area

FormCraft.ForMudBlazor — field rendering (file upload)


Follow-up from #318 (landed as #324). Related: #319, #262

🧠 Brainstorm

Problem / context

FormCraft supplies MudFileUpload with <CustomContent> — a drop zone containing an icon, a count, and
a chip per file. What was not appreciated is that CustomContent replaces the drop target, not the
file list
: MudFileUpload still renders its own .mud-file-upload-filelist beneath. The duplication
has presumably been visible since the component was written; it surfaced now only because #324 needed a
selector that matched exactly one close button per file and found two.

The removal asymmetry is the part that makes this more than cosmetic. FormCraft's chip close calls
RemoveFile, which since #324 also moves focus when the last file goes. MudBlazor's calls its own
removal, which reaches FormCraft only through the @bind-Files writeback — the same writeback #319 is
about to change. Two paths into one value, one of which FormCraft does not control.

Approaches

A. Suppress MudBlazor's list, keep FormCraft's chips. Pros: one list, the richer one; removal
stays on RemoveFile so #318's focus handling and FormCraft's notification both apply; the test
selector becomes unambiguous. Cons: depends on MudFileUpload exposing a way to do it — unknown
until measured.

B. Drop FormCraft's chips, keep MudBlazor's. Pros: less markup to own; guaranteed available.
Cons: loses the formatted size; moves removal off RemoveFile, which would have to be re-plumbed
through FilesChanged to keep #318's behaviour — more work than it looks, and in the wrong direction.

C. Document and leave. Pros: free. Cons: leaves two differently-behaved remove buttons per file.

Recommendation

A, with B as the fallback if and only if measurement shows MudBlazor's list cannot be
suppressed. Step 1 of the plan is that measurement, deliberately before any decision — this issue exists
because something was assumed about CustomContent once already.

📋 Spec

Goal

Each selected file appears exactly once in the multiple-file upload, with exactly one remove control,
and that control routes through FormCraft's RemoveFile.

Scope

  • MudBlazorMultipleFileUploadComponent — the duplicate list.
  • The single-file component, checked for the same duplication and fixed if present.
  • A test pinning the count, so the duplication cannot return unnoticed.

Non-goals

Key files

  • FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorMultipleFileUploadComponent.razor (the chip loop at :51, the MudFileUpload binding at :17)
  • FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorFileUploadFieldComponent.razor (the single-file counterpart)
  • FormCraft.ForMudBlazor.UnitTests/Fields/FileUploadClearFocusTests.cs (whose FormCraftChipCloseSelector documents the current trap)

Validation rules

Edge cases

  • More than three files — FormCraft's loop renders CurrentValue.Take(3) plus a +N more chip; if
    MudBlazor's list is unsuppressable, the two lists disagree about how many files are shown, which is
    worth asserting either way.
  • Zero files — neither list should render a stray control.
  • The single-file component — different binding, so measure rather than infer.

Assumptions

  • MudFileUpload renders its file list independently of CustomContent — measured, and the thing Step
    1 re-confirms before acting.
  • Clearing a multiple-file upload stores null and notifies twice #319 changes how this component binds Files; whichever of the two lands second should re-measure
    the counts rather than trusting this issue's numbers.
  • Target is a patch. Base branch is dev.

🛠️ Implementation plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: one list, one remove control per file, routed through RemoveFile.

Architecture: all changes in FormCraft.ForMudBlazor; the UI-agnostic FormCraft core is untouched.

Tech stack: .NET 8 / 10 multi-target, Blazor, MudBlazor 9.8.0, xUnit + bUnit + Shouldly
(MudBlazorTestBase).

Global constraints:

Task 1: Pin the duplication, and establish whether it can be suppressed

Files: create FormCraft.ForMudBlazor.UnitTests/Fields/FileUploadFileListTests.cs.

Interfaces: none.

  • Step 1: Write a characterisation test asserting today's behaviour — two files render four .mud-chip-close-button elements — and label it clearly as pinning the defect, to be inverted in Task 2.
  • Step 2: Add the same characterisation for the single-file component, establishing on evidence whether it duplicates too rather than assuming from the multiple-file case.
  • Step 3: Determine whether MudFileUpload can be told not to render its built-in list — inspect its parameters, and prove the finding with a throwaway test rather than from documentation.
  • Step 4: Record the outcome in a comment on the characterisation test: suppressible (approach A) or not (approach B), and which parameter does it.
  • Step 5: Run the suite → PASS (these describe what happens today).
  • Step 6: Commit: test(mudblazor): pin the duplicated upload file list.

Task 2: Render each file once

Files: modify MudBlazorMultipleFileUploadComponent.razor (and the single-file component if Task 1 found it affected); invert the Task 1 characterisations.

Interfaces: none new.

Task 3: Simplify the now-unambiguous test selector, and document

Files: modify FormCraft.ForMudBlazor.UnitTests/Fields/FileUploadClearFocusTests.cs; modify README.md; modify CLAUDE.md.

Interfaces: none new.

  • Step 1: Simplify FormCraftChipCloseSelector now that .mud-chip-close-button is unambiguous, and rewrite its remarks to record what was true and what changed — do not silently delete the explanation.
  • Step 2: Add a CLAUDE.md note under the file-upload bullets: MudFileUpload renders its own file list independently of CustomContent, and what this issue did about it.
  • Step 3: Add a README ## 🎉 Unreleased entry, calling out that duplicate remove buttons are gone.
  • Step 4: Run dotnet build -c Release and dotnet test -c Release → both green.
  • Step 5: Commit: docs(mudblazor): record the upload file-list behaviour.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions