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.
Quando esta spec se aplica
Triggers primários
- Render a file-upload / dropzone
Todos os triggers
- Let a user attach/upload one or more files (browse or drag-and-drop)
- Show a dropzone with per-file progress + remove
- gov.br `upload` parity (GVB-29)
Corpo da especificação
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:
- Browse — activating the target fires
on_browse; the host opens a native file dialog and feeds the result back viaadd_files. - 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. - 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(carryingvalue = "drag-over"while a drag hovers), and - when files are present, a
SemRole::Grouplist — one entry per file: aGrouplabelled by the filename carrying its uploadvalue, a sizeLabel, and aSemRole::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-acceptfiles 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
HoveredFileonce 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_browseis fired - AND when the host feeds files back via
add_files,on_addis 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_addis 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
Nonerenders 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
u64value 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::Buttonlabelled"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_browseis fired
Referências
engines/sdk/kroma/src/widget/file_upload.rsmeta/docs/stack/specs/components/date-picker.kmdtools/design-gen/docs/govbr-ds-gap-analysis.md