Skip to content

File-upload component

components specs/components/file-upload.kmd

A drop-target region (dashed-outline zone with a hint + a browse pill) over a list of selected files — each row a name, a human-readable size, an optional per-file upload progress bar, and a remove-X. Files arrive by clicking the target (native dialog) or by OS drag-and-drop; the zone highlights while a drag hovers. gov.br ships a well-developed `upload`; KDS had none — this is it.

When this spec applies

Primary triggers

All triggers

Specification body

File upload

R1 — Anatomy

Two stacked regions in a column (default width 360px):

  • Drop target — a dashed-outline zone (128px tall; 1.5px stroke, 6/4 dash) carrying a hint and a browse pill (132×34px). The whole zone is one activation target (click / Enter / Space → the native file dialog).
  • File list (present only when files are selected) — one 44px row per file, --kds-space-*-spaced (12px list gap, 6px row gap, 12px pad), each row showing: the file name, its human size (R4), an optional progress bar (R3), and a remove-X (32px vector button).

R2 — Input paths

Files arrive by three paths, all converging on add_files:

  1. Browse — activating the target fires on_browse; the host opens a native file dialog and feeds the result back via add_files.
  2. OS drag-over — a file drag over the target lights the drag-over highlight (event-driven; also settable programmatically), exported as value = "drag-over" for the a11y / introspection channel, and clears when the drag leaves.
  3. OS drop — releasing files over the target ingests them (name = final path component, size = on-disk length) and fires on_add, identical to the dialog return.

Removing a file fires on_remove(index).

R3 — Transfer progress

Each file carries an optional upload progress in 0.0..=1.0, driven by the host via set_progress(index, progress) and rendered as a 4px bar on the row. A None progress renders no bar (not-yet-uploading or not-tracked). The value is clamped to 0.0..=1.0.

R4 — Size formatting

File sizes render human-readable, 1024-based, one decimal above bytes: < 1024"<n> B", else "<v.v> KB|MB|GB|TB" (human_size). This is a display concern; the stored size is the exact u64 byte count.

R5 — Accessibility

A SemRole::Group root holds:

  • the drop/browse target as a SemRole::Button (carrying value = "drag-over" while a drag hovers), and
  • when files are present, a SemRole::Group list — one entry per file: a Group labelled by the filename carrying its upload value, a size Label, and a SemRole::Button "Remove ".

Reuses Button + Group — no new role (the date-picker.kmd R6 screen-reader contract adapted to a dropzone).

R6 — Keyboard

Per date-picker.kmd R5, adapted: Tab visits the drop target, then each file's remove-X (via the focusables walk); Enter/Space activates the focused target (opens the dialog) or the focused remove-X. Single Tab-stop semantics are not imposed — each actionable element is reachable.

R7 — Accepted types & size limit

accept (MIME/extension globs) and max_size (bytes) are declarable on the component. Client-side enforcement is a follow-up (kroma#311): today they are carried as configuration and surfaced to the host, which may enforce them; the widget does not yet reject an over-limit or wrong-type file with an inline error.

R8 — gov.br parity

Satisfies the gov.br upload component (govbr-ds-parity, GVB-29): dropzone + browse, multi-file list with size and remove, drag-and-drop, per-file progress.

Não-escopo / deferred to impl follow-up (kroma#311)

  • Inline validation states (error/success per file) and the rejection of over-max_size / non-accept files with a user-facing message — R7 enforcement.
  • Long-name truncation with a tooltip — names render in full today; ellipsis + hover/focus tooltip is a follow-up.
  • Drag MOTION across sibling zones mid-hover — a windowed-session/runner concern (winit emits HoveredFile once on window-enter, not per move), not this widget.
  • Label/hint typography — from specs/fonts/typography.kmd; the widget is font-optional (renders text only when a font is set).

Requirements (testable)

Requirement: Anatomy and file-list rendering {#req-file-upload-anatomy}

The component SHALL render a dashed-outline drop target carrying a hint and a browse pill, and SHALL render the file list only when one or more files are selected, with one row per file exposing the file name, its human-readable size, and a remove-X control.

Scenario: Empty state shows only the drop target

  • GIVEN a file-upload component with no files selected
  • WHEN the component is rendered
  • THEN a dashed-outline drop target with a browse pill is present
  • AND no file-list rows are rendered

Scenario: Selecting files renders one row per file

  • GIVEN a file-upload component with no files selected
  • WHEN two files are added via add_files
  • THEN the file list is rendered
  • AND exactly two file rows are present, each showing a name, a human size, and a remove-X control

Requirement: Input paths converge on add_files {#req-file-upload-input-paths}

Files SHALL enter the component through browse activation, OS drag-over, and OS drop, all converging on add_files; activating the target SHALL fire on_browse, ingesting files SHALL fire on_add, and removing a file SHALL fire on_remove(index).

Scenario: Activating the target requests the native dialog

  • GIVEN a rendered file-upload component
  • WHEN the drop target is activated
  • THEN on_browse is fired
  • AND when the host feeds files back via add_files, on_add is fired

Scenario: OS drop ingests dropped files

  • GIVEN a rendered file-upload component
  • WHEN files are dropped over the target
  • THEN each dropped file is ingested with name = final path component and size = on-disk length
  • AND on_add is fired

Scenario: Removing a file emits its index

  • GIVEN a file-upload component with two selected files
  • WHEN the remove-X of the file at index 1 is activated
  • THEN on_remove(1) is fired

Requirement: Drag-over highlight {#req-file-upload-drag-over}

While a file drag hovers the target the component SHALL show the drag-over highlight and export value = "drag-over" on the target, and SHALL clear the highlight when the drag leaves.

Scenario: Highlight appears on hover and clears on leave

  • GIVEN a rendered file-upload component with no active drag
  • WHEN a file drag enters the target
  • THEN the drag-over highlight is shown
  • AND the target exports value = "drag-over"
  • WHEN the drag leaves the target
  • THEN the highlight is cleared

Requirement: Per-file transfer progress {#req-file-upload-progress}

Each file SHALL carry an optional progress in 0.0..=1.0 set via set_progress(index, progress), rendered as a 4px bar on the row; a None progress SHALL render no bar, and any supplied value SHALL be clamped to 0.0..=1.0.

Scenario: A tracked file renders a progress bar

  • GIVEN a file-upload component with one selected file whose progress is None
  • WHEN set_progress(0, 0.5) is called
  • THEN the file's row renders a 4px progress bar reflecting 0.5
  • AND a file whose progress remains None renders no bar

Scenario: Out-of-range progress is clamped

  • GIVEN a file-upload component with one selected file
  • WHEN set_progress(0, 1.7) is called
  • THEN the stored progress for that file is 1.0

Requirement: Human-readable size formatting {#req-file-upload-size}

File sizes SHALL render human-readable and 1024-based with one decimal above bytes — < 1024 bytes as "<n> B", otherwise "<v.v> KB|MB|GB|TB" — while the stored size SHALL remain the exact u64 byte count.

Scenario: Sub-kilobyte size renders as bytes

  • GIVEN a file whose on-disk size is 512 bytes
  • WHEN its size is formatted via human_size
  • THEN the rendered size is "512 B"
  • AND the stored size is the exact u64 value 512

Scenario: Kilobyte-scale size renders with one decimal

  • GIVEN a file whose on-disk size is 1536 bytes
  • WHEN its size is formatted via human_size
  • THEN the rendered size is "1.5 KB"

Requirement: Accessibility and keyboard reachability {#req-file-upload-a11y}

The component SHALL expose a SemRole::Group root containing the drop/browse target as a SemRole::Button (carrying value = "drag-over" while a drag hovers) and, when files are present, a Group list whose entries each carry a name-labelled group, a size Label, and a SemRole::Button labelled "Remove <name>"; Tab SHALL visit the drop target then each remove-X, and Enter/Space SHALL activate the focused target or remove-X.

Scenario: Semantic tree exposes target and per-file remove buttons

  • GIVEN a file-upload component with one selected file named report.pdf
  • WHEN the semantic tree is inspected
  • THEN the root is a SemRole::Group
  • AND the drop target is a SemRole::Button
  • AND the file entry carries a SemRole::Button labelled "Remove report.pdf"

Scenario: Keyboard reaches and activates the target and remove-X

  • GIVEN a file-upload component with one selected file
  • WHEN Tab is pressed from the start
  • THEN focus lands on the drop target, then on the file's remove-X on the next Tab
  • WHEN Enter or Space is pressed while the drop target is focused
  • THEN the target is activated and on_browse is fired

References