Skip to content

Add loading, progress, and invalid states to limel-file (component and per-file)#4154

Merged
Kiarokh merged 6 commits into
mainfrom
file-loading-progress
Jul 2, 2026
Merged

Add loading, progress, and invalid states to limel-file (component and per-file)#4154
Kiarokh merged 6 commits into
mainfrom
file-loading-progress

Conversation

@Kiarokh

@Kiarokh Kiarokh commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Why

limel-file had no way to reflect that something is happening to the selected file — a known gap. Its consumer limec-file-picker can today only grey the field out via disabled during work, and there is no way to surface upload progress at all.

Important

Before starting the review, please note that some of the decisions in this PR may feel initially strange. This is due to two reasons:

  1. limel-file allows only selection of one single file. We know consumers have requested multiple file selection possibility. We want to enable this feature in future. To keep this component future-proof this means, there shouldn't be a one-on-one relationship between the file(chip) and the file picker. Each file can individually become invalid or be on loading state.
  2. An optimal FileInfo interface, should have a status prop or similar (name not decided yet). That would allow each file to reflect its own status. For example: "Uploading…", "Failed!", "43%" (progressed, loaded, or uploaded), etc… Right now in this PR, we have some accessibility issues with aria-live, and we can't properly explain to the assistive tech what is going on. But a future per-file status in the follow up PR will help solve this issue. Such statuses will also be reflected on the chip's badge itself, helping sighted users to understand the status of what is happening better

Two tiers of "busy" state, built up in atomic commits:

  • feat(file): add a loading prop — a component-level loading prop: the component's own indeterminate busy state (spinner + aria-busy), controlled entirely by the consumer, shown whether or not a file is selected. Does not disable interactivity.
  • feat(file): support a per-file loading stateFileInfo.loading, rendered as an indeterminate indicator on that file's chip. A loading file also puts the parent into its loading state. Independent of the component prop; the multi-file-ready per-file model.
  • feat(chip-set): forward the progress property to the chiplimel-chip already had a determinate progress bar, but the Chip model consumed by limel-chip-set didn't expose it. Adds progress to Chip + forwards it (with a chip-set example).
  • feat(file): support a per-file progress stateFileInfo.progress (0–100), a determinate bar on the file's chip, for measurable work like uploads or client-side resizing.
  • feat(file): support a per-file invalid stateFileInfo.invalid, an error state on the file's chip, independent of the component-level invalid prop.

Per-file status lives on FileInfo, so it maps 1:1 onto the chip and is ready for a future multi-file limel-file — each file owns its own state, the pattern already used by limel-chip-set's Chip[] and limebb-document-chips. Every field is optional and defaults to off, so this is fully backward compatible. Each feat carries its own documentation example.

Design notes

  • Component vs. per-file. loading exists both at the component level (the whole picker is busy — a spinner) and per file (FileInfo.loading — the chip's own indicator). A loading file bubbles up to the component's busy state (aria-busy + spinner); the two are otherwise independent. progress and per-file invalid are per-file only. There is intentionally no component-level progress — a cumulative/overall bar can be added later if/when the component supports multiple files.
  • Per-file status only renders while a file is selected (there must be a chip to render it on).
  • loading does not disable interactivity, matching other components' loading states.
  • ARIA conveys that the component is busy (aria-busy), not what — announcing the specific activity ("Uploading…") is a natural fit for a future per-file status label.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added loading support to the file component, including busy behavior with a spinner.
    • Added progress support (0–100) to file/chip rendering, enabling determinate progress display.
    • Added per-file invalid and helperText handling to improve messaging in chip UI.
  • Style & Accessibility
    • Updated busy state semantics (e.g., aria-busy) and adjusted UI while busy.
  • Examples & Documentation
    • Added new examples covering file loading, per-file loading, per-file progress, per-file invalidation, and chip-set progress.
    • Extended documentation example tags for the new loading/progress scenarios.

@Kiarokh Kiarokh requested a review from a team as a code owner July 1, 2026 13:19
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Kiarokh, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 777c3140-9c2b-4812-b8b0-ac7440210125

📥 Commits

Reviewing files that changed from the base of the PR and between 78fc20a and f45838f.

⛔ Files ignored due to path filters (1)
  • etc/lime-elements.api.md is excluded by !etc/lime-elements.api.md
📒 Files selected for processing (4)
  • src/components/file/examples/file-per-file-invalid.tsx
  • src/components/file/examples/file-per-file-progress.tsx
  • src/components/file/file.tsx
  • src/global/shared-types/file.types.ts
📝 Walkthrough

Walkthrough

Adds progress and loading state support to chip and file rendering, expands shared file metadata, and introduces updated file examples plus a chip progress example.

Changes

File loading and progress feature

Layer / File(s) Summary
Chip progress contract
src/components/chip-set/chip.types.ts, src/components/chip-set/chip-set.tsx, src/components/chip-set/examples/chip-set-progress.tsx
Chip<T> gains optional progress, ChipSet forwards it to limel-chip, and a new progress example is added.
File loading and progress rendering
src/global/shared-types/file.types.ts, src/components/file/file.tsx, src/components/file/file.scss
FileInfo gains loading, progress, and invalid; limel-file uses loading-aware busy state, spinner rendering, helper text, and chip forwarding.
File examples and docs
src/components/file/examples/*
File examples are added or updated for loading, per-file loading/progress/invalid states, accepted types text, and example layout styling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Example
  participant LimelFile
  participant LimelChipSet
  participant LimelChip
  participant Spinner

  Example->>LimelFile: value with loading/progress
  LimelFile->>Spinner: render while busy
  LimelFile->>LimelChipSet: render chip with helperText
  LimelChipSet->>LimelChip: forward chip progress
Loading

Possibly related PRs

Suggested labels: released

Suggested reviewers: adrianschmidt, Befkadu1

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding loading, progress, and invalid states to limel-file at both component and per-file levels.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch file-loading-progress

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

Documentation has been published to https://lundalogik.github.io/lime-elements/versions/PR-4154/

@Kiarokh Kiarokh force-pushed the file-loading-progress branch from 81c1a7f to 5b73004 Compare July 1, 2026 13:26
@Kiarokh Kiarokh marked this pull request as draft July 1, 2026 13:48
@Kiarokh Kiarokh force-pushed the file-loading-progress branch from 5b73004 to a1965ff Compare July 1, 2026 14:04
@Kiarokh Kiarokh changed the title Add loading and progress indicators to limel-file Add per-file loading, progress, and invalid indicators to limel-file Jul 1, 2026
@Kiarokh Kiarokh force-pushed the file-loading-progress branch 3 times, most recently from 809257b to 3b78e08 Compare July 1, 2026 15:07
@Kiarokh Kiarokh changed the title Add per-file loading, progress, and invalid indicators to limel-file Add loading, progress, and invalid states to limel-file (component and per-file) Jul 1, 2026
import { Component, h, Host, State } from '@stencil/core';

/**
* Per-file loading

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this independent of the component-level loading prop you wonder? (see the loading example)

Because this is what a future multi-file limel-file would use to reflect the status of each individual file. Today, limel-file only allows consumers to select one single file. But we know that many have requested a file-picker component that allows selection of multiple files. In such a file picker, each file can have its own various states (e.g. loading, progress, invalid, etc…)

@Kiarokh Kiarokh force-pushed the file-loading-progress branch 5 times, most recently from 3c7d9ec to 8746a14 Compare July 2, 2026 09:05
Comment thread src/components/file/file.tsx Outdated
Comment on lines +79 to +80
* Optional helper text to display below the component. When the component
* is `invalid`, it is rendered as an error message.

@Befkadu1 Befkadu1 Jul 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This description with the invalid behavior makes a general-purpose prop sound error-specific.

Suggested change
* Optional helper text to display below the component. When the component
* is `invalid`, it is rendered as an error message.
* Optional helper text to display below the component.

}

private handleChange = (event: CustomEvent<FileInfo>) => {
this.value = event.detail;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe is it good to set this.loading = false if the attached file is removed? It doesn't make sense that it shows loading icons while the file is already removed. I know that this example is only to show the loading example but might be good to have a realistic example

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the component might be loading due to some other reason. Maybe it doesn't have the file, but some background process is making it ready for the upload, or whatever... It's basically trying to get ready to go to the file upload party

value={value}
/>
<limel-example-controls>
<limel-example-controls

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change should not supposed to be in this commit

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know 😄 Just ignore it please

@Kiarokh Kiarokh force-pushed the file-loading-progress branch from 8746a14 to edd41bd Compare July 2, 2026 11:02
@Kiarokh Kiarokh marked this pull request as ready for review July 2, 2026 11:09
@Kiarokh Kiarokh removed their assignment Jul 2, 2026
Kiarokh and others added 4 commits July 2, 2026 13:25
Add a `helperText` prop, forwarded to the underlying chip-set, so a
consumer can display guidance below the component — and, when the component
is `invalid`, an error message — like the other input-type components.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `loading` prop that puts the whole component in an indeterminate busy
state — rendering a spinner and setting `aria-busy` — without disabling its
interactivity. It is the component's own loading state, controlled entirely
by the consumer: the spinner shows whenever `loading` is `true`, whether or
not a file is selected, and stays until the consumer sets it back to `false`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `loading` to `FileInfo`, rendered as an indeterminate indicator on that
file's chip. Because a busy file means the component has work in progress,
a loading file also puts the parent component into its loading state
(`aria-busy` and the component spinner).

This is independent of the component-level `loading` prop, and is what a
future multi-file `limel-file` will use to reflect the status of each file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`limel-chip` already supports a determinate `progress` bar, but the `Chip`
model consumed by `limel-chip-set` did not expose it. Add `progress` to the
`Chip` type and forward it to the underlying chip, so consumers can render
determinate progress on a chip in the set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Kiarokh Kiarokh force-pushed the file-loading-progress branch from 8953176 to 034257d Compare July 2, 2026 11:26
@Kiarokh Kiarokh enabled auto-merge (rebase) July 2, 2026 11:26
Befkadu1
Befkadu1 previously approved these changes Jul 2, 2026
@Kiarokh

Kiarokh commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI-generated review from 6 parallel agents, updated for the current revision. Treat as input, not a verdict — agents can be wrong or miss context.

Consolidated PR Review — updated

PR Summary

Adds loading / progress / invalid states to limel-file at two levels: a component-level loading prop (spinner + aria-busy) and per-file status on the shared FileInfo (loading / progress / invalid) rendered on the file's chip. A file that is loading or has progress also puts the whole component into its busy state; per-file invalid deliberately does not (validity stays the consumer's call). Also forwards Chip.progress through limel-chip-set, adds a helperText prop, and ships a documentation example per feature. 6 atomic commits; every new field/prop is optional and defaults off.

Merge Readiness — READY TO MERGE ✅

Strictly additive and better than main on every dimension — no existing API or behavior changes, and defaults-off means the rendered output is unchanged when the new props are unset. The design choices that might look surprising at first (per-file status on FileInfo; the aria-busy-without-aria-live gap) are explained in the PR description and tied to the planned multi-file + per-file status follow-up.

  • Blockers: None.
  • Non-blocking but worth knowing: one accessibility item, already acknowledged in the description and deferred to the follow-up — see Top Recommendations.

Dimensions

1. Backward Compatibility — SAFE ✅

Purely additive; nothing existing changes.

What works well: every new surface (FileInfo.loading/progress/invalid, Chip.progress, limel-file loading/helperText) is optional and defaulted; the API report changes are all additions, none removed or re-typed; new render paths are gated on the new state, so unset props reproduce main's output exactly. Other FileInfo consumers (file-dropzone, file-input, profile-picture) only read known fields, so extra optional fields can't break them.

Issues: None.

2. Code Quality — GOOD ✅

Consistent with siblings; docs match behavior.

What works well: prop declarations, the aria-busy ternary, and helperText forwarding all match existing components; the "helperText renders as an error when invalid" JSDoc is accurate through the chip-set → helper-line chain; each feature ships a focused example, and the examples clearly document the two-tier busy model and that a per-file invalid does not auto-invalidate the field.

Issues: None.

Minor nits (1)
  • 🔵 No spec/e2e coverage for the new states — but limel-file has no spec at all today, so this is a pre-existing gap, not a regression.

3. Architecture — GOOD ✅

Coherent two-tier model, now explicitly motivated.

What works well: the data flow limel-file → FileInfo → getChipArray() → chip-set → chip forwards 1:1 onto the existing Chip model; putting per-file status on FileInfo is a deliberate, documented choice for the coming multi-file component (avoiding a one-to-one file↔picker coupling); and the bubbling rule is principled — the "busy" states (loading, progress) lift to the component, while validity stays a form-level decision the consumer owns, matching limel-chip-set.

Issues: None.

4. Security — SAFE ✅

No new injection surface.

What works well: helperText renders as escaped JSX text, never innerHTML; aria-busy is derived from booleans; the progress bar's CSS-custom-property sink is pre-existing on limel-chip, and this PR only forwards the already-typed number.

Issues: None.

Minor nits (1)
  • 🔵 progress isn't clamped to 0–100 (chip.tsx:441-460, forwarded via file.tsx getChipArray); pre-existing and not exploitable (a CSS custom-property value can't escape into other rules — worst case is a self-inflicted width glitch), just a robustness nice-to-have.

5. Observability — GOOD ✅

Real busy signal, now accurate for uploads too.

What works well: aria-busy on the host is a genuine signal that didn't exist on main, and it's now driven by loading and progress, so it's correct during determinate uploads as well; determinate per-file progress rides on the chip's already-accessible role="progressbar" + aria-valuenow.

Issues:

Minor nits (2)
  • 🔵 Redundant (not conflicting) nested aria-busy when a per-file loading is set: both the host (file.tsx:144) and the chip's inner button (chip.tsx:246) get aria-busy="true". Harmless.
  • 🔵 The chip progress bar's accessible name is aria-label="%" (chip.tsx:451), which reads poorly ("percent, 40"); pre-existing chip behavior, now given a bit more exposure.

6. Performance — SAFE ✅

No new hot paths or leaks.

What works well: the spinner renders only while busy and unmounts declaratively (no timers/listeners); its animation is pure CSS on the compositor; progress updates change a single CSS custom property, not DOM structure; the new chip fields are trivial pass-throughs reusing the chip's existing progress rendering.

Issues: None.

Minor nits (2)
  • 🔵 The isLoading getter is recomputed on each of its ~3 calls per render (file.tsx, isLoading getter) — a couple of property reads, immeasurable.
  • 🔵 @Prop({ reflect: true }) loading writes a loading attribute and the render sets aria-busy — two host attribute writes per toggle (file.tsx:116,144); low-frequency and inconsequential, matching the existing invalid/disabled pattern.

Top Recommendations

  1. 🟡 [Medium from Observability] — No spoken feedback when the busy state changes (already planned as a follow-up)
    • Where: src/components/file/file.tsx:144 (host aria-busy) and renderSpinner; src/components/spinner/spinner.tsx (the spinner has no role/label).
    • What: aria-busy is a passive state — screen readers don't announce load start/finish when it toggles, and the spinner carries no role or label of its own. A role="status" / aria-live region, or an accessible label, would give spoken feedback.
    • Why: assistive-tech users get no cue that work began or completed. This is explicitly acknowledged in the PR description and slated for the follow-up per-file status PR (which will also surface on the chip badge for sighted users), so it is non-blocking — captured here only so reviewers know it was checked.

Kiarokh and others added 2 commits July 2, 2026 16:13
Add `progress` to `FileInfo`, rendered as a determinate progress bar on that
file's chip, reflecting the percentage of an ongoing process such as an
upload or a client-side resize.

Set per file so each file owns its own progress, ready for future multi-file
support. While per-file `loading` covers indeterminate work, `progress`
covers measurable work; the two are independent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `invalid` to `FileInfo`, forwarded to the file's chip, so an individual
file can be marked as invalid — for example one that failed to upload or is
of a disallowed type. This is independent of the component-level `invalid`
prop (which marks the whole field), matching how per-chip `invalid` works in
`limel-chip-set`, and readies the component for per-file validation once it
supports multiple files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* by the consumer clearing these — not by `progress` reaching `100`, since
* a file at `100%` may still be finalizing (e.g. awaiting the server).
*/
private get isBusy(): boolean {

@Kiarokh Kiarokh Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this is called isBusy and not isLoading?

Because I got a feedback,

See the comment src/components/file/file.tsx:602-608 — isLoading's "busy" semantics disagree with the chip's own progress rendering at the value boundaries. isLoading treats any defined progress (including 0) as busy: this.value?.progress !== undefined. But renderProgressBar() in src/components/chip/chip.tsx:442 uses a falsy check, if (!this.progress) return;, which was previously dead code for this component since chip-set.tsx never forwarded progress — this PR activates it. Result: - progress: 0 (the exact value the shipped example file-per-file-progress.tsx starts at): the file shows a spinner + aria-busy="true", but the chip's own progress bar doesn't render at all. - progress: 1–99: both the file-level indeterminate spinner and the chip's determinate bar render simultaneously — two contradictory busy indicators for the same operation. - progress: 100: the spinner keeps spinning even though the value implies completion. This was independently flagged by all four review angles — high confidence.

and the gist of it was:

Don't show the indeterminate spinner while the chip is already showing a determinate bar.

I disagreed. Because if one (or several files in future version of this component) are in progress, for the parent component it means something is happening inside me. I'm busy for whatever reason. So I'll remain busy until the internal processes of my children are done. Then I'll show that I'm done.

Sanity-checking the matter with Claude, and this is the final comment form that discussion. Posting it here, in case anyone else wonders about these decissions:

You're right, and it's a better argument than the one I was making. I'll drop the recommendation.

The flaw in "don't show the spinner while a bar is showing" is that it treats the spinner and the bar as two answers to the same question. They're not — they're at different scopes:

  • Component spinner answers "does this component have unsettled work?" — a boolean about the container.
  • Chip bar answers "how far along is this file?" — a detail about one child.

The spinner isn't claiming the file's duration is unknown; it's claiming the component isn't settled. Both can be true at once without contradiction. So your model — "I'm busy for whatever reason, and I stay busy until my children's work is done, then I show done" — is coherent, and the three "boundaries" the reviewer flagged all resolve cleanly under it:

  • 0 → component busy (spinner), child has nothing measurable to draw yet. Fine.
  • 1–99 → component busy (spinner) + child detail (bar). Complementary, not contradictory.
  • 100 → 100% is bytes transferred, not operation complete. With the upload flow you're still waiting on the server's 201. The component is legitimately still finalizing, so it stays busy until the consumer clears the state. The spinner only "spins forever" if a consumer forgets to clear it — which is a consumer bug, not ours.
    The clincher is exactly the multi-file future you raised: with N files at 10/40/90/100%, the parent can't show a determinate bar — no single percentage is correct. An indeterminate "I'm busy" is the only right parent-level visual, with each chip carrying its own bar. So keeping the spinner during progress isn't just acceptable, it's the design that scales. My suggestion would've left multi-file with no parent busy affordance at all — a step backward.

This also lines up with what I already posted: the review's Observability section calls the "busy signal driven by loading and progress" a positive. I stand by that over this new finding. The one real kernel — isLoading uses progress !== undefined (so 0 counts) while the chip uses !progress (so 0 doesn't draw) — isn't an inconsistency to fix; those are two correct answers to two different questions ("is work attached?" vs. "is there a percentage to paint?").

So: no code change. The finding is a mismatch between the reviewer's mental model and your (deliberate, forward-compatible) one.

If you want to preempt the next person raising it, the only cheap things worth doing — all optional:

A one-line comment on the getter documenting the aggregate-busy decision ("stays busy while a file is loading or has progress, incl. 0; completion is consumer-signalled, not progress === 100").
Optionally rename isLoading → isBusy to match "busy for whatever reason" and stop the name implying "indeterminate only."

@Kiarokh Kiarokh force-pushed the file-loading-progress branch from 78fc20a to f45838f Compare July 2, 2026 14:13
@Kiarokh Kiarokh merged commit 83812c2 into main Jul 2, 2026
18 checks passed
@Kiarokh Kiarokh deleted the file-loading-progress branch July 2, 2026 14:18
@lime-opensource

Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 39.39.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@coderabbitai coderabbitai Bot mentioned this pull request Jul 7, 2026
13 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants