[Feature] Add Spinner component - #521
Open
tvq wants to merge 2 commits into
Open
Conversation
The registry builder only read heredocs inside VisualCodeExample, so standalone Codeblock(<<~RUBY, ...) samples in a component's docs (Form, InputOTP) were dropped and left empty headings behind. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port of shadcn/ui Spinner: an inline SVG loading indicator with
role="status" and animate-spin, sized from the outside with size-*.
shadcn has no `loading` prop; it composes a standalone Spinner into
buttons, badges and empty states. RubyUI had no equivalent.
Departures from upstream, each forced by Phlex or by our Button:
- `label:` kwarg instead of overriding aria-label through attrs.
Phlex `mix` concatenates attribute values, so passing
aria: {label: "Saving"} rendered aria-label="Loading Saving".
- `label: nil` renders aria-hidden="true" with no role, for a spinner
inside a control that announces aria-busy itself. Upstream nests a
role="status" spinner inside the button, which reads the label twice.
- `shrink-0` on the icon: our Button has no [&_svg]:shrink-0 yet.
The SVG is inlined because the gem ships no icon dependency. Docs show
how to swap view_template for phlex-icons once the file is generated
into the app, which is the shadcn customization model.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 13 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="gem/lib/ruby_ui/spinner/spinner.rb">
<violation number="1" location="gem/lib/ruby_ui/spinner/spinner.rb:40">
P3: When `label: ""` (an empty string) is passed, `@label.nil?` is false, so the spinner renders `role="status" aria-label=""` — an empty live region that announces nothing. `label` only falls back to decorative mode on `nil`, not on blank values. Treat blank strings as decorative too (or validate them) so an accidentally empty label can't produce a silent `role="status"`.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Comment on lines
+40
to
+43
| def announcement_attrs | ||
| return {aria: {hidden: "true"}} if @label.nil? | ||
|
|
||
| {role: "status", aria: {label: @label}} |
There was a problem hiding this comment.
P3: When label: "" (an empty string) is passed, @label.nil? is false, so the spinner renders role="status" aria-label="" — an empty live region that announces nothing. label only falls back to decorative mode on nil, not on blank values. Treat blank strings as decorative too (or validate them) so an accidentally empty label can't produce a silent role="status".
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At gem/lib/ruby_ui/spinner/spinner.rb, line 40:
<comment>When `label: ""` (an empty string) is passed, `@label.nil?` is false, so the spinner renders `role="status" aria-label=""` — an empty live region that announces nothing. `label` only falls back to decorative mode on `nil`, not on blank values. Treat blank strings as decorative too (or validate them) so an accidentally empty label can't produce a silent `role="status"`.</comment>
<file context>
@@ -0,0 +1,46 @@
+ end
+
+ # A spinner inside a labelled control is decorative — the control announces busy.
+ def announcement_attrs
+ return {aria: {hidden: "true"}} if @label.nil?
+
</file context>
Suggested change
| def announcement_attrs | |
| return {aria: {hidden: "true"}} if @label.nil? | |
| {role: "status", aria: {label: @label}} | |
| def announcement_attrs | |
| return {aria: {hidden: "true"}} if @label.nil? || @label.empty? | |
| {role: "status", aria: {label: @label}} | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
RubyUI::Spinner— a port of shadcn/ui Spinner. Inline SVG (lucideloader-circle),role="status",aria-label,data-slot="spinner",size-4 animate-spin. No JS, no Stimulus, no dependencies. Size and colour come from the caller viaclass:.Docs page at
/docs/spinnerwith the upstream examples that fit our component set: standalone, sizes, colours, insideButton(disabled +aria-busy), insideBadge, insideEmptyas a loading screen, plus Customization and Accessibility sections.Why
shadcn has no
loadingprop on Button. It ships a standalone Spinner that gets composed into buttons, badges, input groups and empty states. RubyUI had Skeleton, Progress and Empty but nothing to put inside them — this closes that gap without touching Button, so it can land in any order relative to the Button work.Departures from upstream (deliberate, each forced by Phlex or by our Button)
label:kwarg instead of overridingaria-labelthrough attrs. Phlexmixconcatenates attribute values, soaria: {label: "Saving"}renderedaria-label="Loading Saving". Same trade-off every other RubyUI component makes (variant:,size:,pressed:).label: nil→ decorative (aria-hidden="true", no role). For a spinner inside a control that announcesaria-busyitself. Upstream nests arole="status"spinner inside the button, which makes the button's accessible name read the label twice. Default stays"Loading"for parity on a standalone spinner.shrink-0on the icon. Our Button lacks shadcn's[&_svg]:shrink-0; this can go once Button gets it.Everything else passes through
**attrs, sodata: {icon: "inline-start"}works unchanged when Button learns the vega padding convention.Also in this PR
Separate first commit: the MCP registry builder ignored standalone
Codeblock(<<~RUBY, ...)samples in docs views, leaving empty headings indocs_markdown. Fixed so the Spinner docs (and Form / InputOTP, which had the same gap) render fully for MCP consumers.registry.json,llms.txt,llms-full.txtandsitemap.xmlare regenerated.Test steps
Then
cd docs && bin/dev, open/docs/spinner, toggle dark mode. Inspect a Button example: the<button>carriesaria-busy="true", the<svg>inside carriesaria-hidden="true".Screenshots (light / dark) to follow.
Summary by cubic
Adds
RubyUI::Spinner, an inline SVG loading indicator ported from shadcn/ui, with no JS, Stimulus, or icon dependencies. Size and color come from the caller viaclass:, so it composes into Button, Badge, and Empty without changes to those components.New Features
role="status"and anaria-labeldefaulting to "Loading"; passlabel:to describe the operation.label: nilto render the spinner decorative (aria-hidden="true") inside a control that announcesaria-busyitself, so the label is not read twice./docs/spinnercovering sizes, colors, composition, customization, and accessibility.Bug Fixes
Codeblock(<<~RUBY, ...)samples in docs views are included indocs_markdown; Form and InputOTP previously rendered with empty headings.registry.json,llms.txt,llms-full.txt, andsitemap.xml.Written for commit e040d23. Summary will update on new commits.