Skip to content

Add reusable clipboard utility and copy button#458

Open
barry01-hash wants to merge 3 commits intotheblockcade:mainfrom
barry01-hash:stellarcade
Open

Add reusable clipboard utility and copy button#458
barry01-hash wants to merge 3 commits intotheblockcade:mainfrom
barry01-hash:stellarcade

Conversation

@barry01-hash
Copy link
Copy Markdown
Contributor

@barry01-hash barry01-hash commented Mar 29, 2026

Closes #426

Summary by CodeRabbit

  • New Features
    • Added a reusable CopyButton with explicit feedback states (idle → copying → copied), customizable labels, accessible status announcements, and auto-reset behavior.
    • Added clipboard utilities with Clipboard API + fallback support for older browsers.
  • Bug Fixes / Reliability
    • Improved error handling and recovery for unsupported or failing clipboard actions.
  • Tests
    • Added unit tests covering success, fallback, and failure scenarios.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 29, 2026

Warning

Rate limit exceeded

@barry01-hash has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 34 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 9 minutes and 34 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3328e281-cb79-4ef7-b0ce-626ae471081d

📥 Commits

Reviewing files that changed from the base of the PR and between 5deb303 and 6c5ee0d.

📒 Files selected for processing (7)
  • frontend/src/components/v1/CopyButton.tsx
  • frontend/src/components/v1/index.ts
  • frontend/src/hooks/v1/validation.ts
  • frontend/src/i18n/messages/en.json
  • frontend/src/utils/v1/clipboard.ts
  • frontend/tests/components/v1/CopyButton.test.tsx
  • frontend/tests/utils/v1/clipboard.test.ts
📝 Walkthrough

Walkthrough

Adds a new CopyButton component and a clipboard utility with Clipboard API + execCommand fallback, plus tests and index re-exports; CopyButton manages explicit copy states, error propagation, callbacks, and timed UI feedback.

Changes

Cohort / File(s) Summary
CopyButton component
frontend/src/components/v1/CopyButton.tsx
New function component with explicit status states (idlecopyingcopied), customizable labels, onCopy/onCopyError callbacks, aria-live status, local error rendering, timeout reset with cleanup, and test IDs.
v1 components index
frontend/src/components/v1/index.ts
Re-exports CopyButton (named), default alias CopyButtonDefault, and CopyButtonProps; also adds extra re-exports for ContractActionButton/props.
Clipboard utility
frontend/src/utils/v1/clipboard.ts
New typed clipboard API exposing writeToClipboard, feature detection (isClipboardWriteSupported), fallback to execCommand, structured result types (ClipboardWriteResult), and AppError construction for failures.
v1 utils index
frontend/src/utils/v1/index.ts
Re-exported clipboard utilities via export * from './clipboard';.
Tests: component & util
frontend/tests/components/v1/CopyButton.test.tsx, frontend/tests/utils/v1/clipboard.test.ts
New test suites covering success, fallback, unsupported environments, error propagation to global error store, label/state transitions, and reset timing using fake timers.

Sequence Diagram

sequenceDiagram
    actor User
    participant Button as CopyButton
    participant Clipboard as writeToClipboard
    participant API as Navigator.clipboard
    participant Fallback as execCommand
    participant Store as ErrorStore
    participant Callback as onCopy/onCopyError

    User->>Button: Click
    Button->>Button: status = "copying"
    Button->>Clipboard: writeToClipboard(text)
    alt Clipboard API available
        Clipboard->>API: writeText()
        alt success
            API-->>Clipboard: resolves
            Clipboard-->>Button: {ok:true, method:'clipboard-api'}
            Button->>Button: status = "copied"
            Button->>Callback: onCopy()
            Button->>Button: schedule reset (feedbackDurationMs)
        else failure
            API-->>Clipboard: rejects
            Clipboard->>Fallback: attempt execCommand copy
            alt execCommand success
                Fallback-->>Clipboard: {ok:true, method:'exec-command'}
                Clipboard-->>Button: success
                Button->>Callback: onCopy()
            else execCommand failure
                Fallback-->>Clipboard: {ok:false, reason:'write-failed', error}
                Clipboard-->>Button: failure
                Button->>Store: setError(error)
                Button->>Callback: onCopyError(error)
            end
        end
    else Clipboard API unavailable
        Clipboard->>Fallback: attempt execCommand copy
        alt execCommand available & success
            Fallback-->>Clipboard: {ok:true, method:'exec-command'}
            Clipboard-->>Button: success
            Button->>Callback: onCopy()
        else neither available or failure
            Clipboard-->>Button: {ok:false, reason:'unsupported' or 'write-failed', error}
            Button->>Store: setError(error)
            Button->>Callback: onCopyError(error)
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐇 I hopped to copy, quick and bright,

Clicked the button, sent the byte,
Fallback ready, error kept small,
Status changed — idle → copied — all,
A rabbit's cheer for code that works tonight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main changes: adding a reusable clipboard utility and copy button component.
Linked Issues check ✅ Passed All requirements from issue #426 are fully implemented: clipboard utility with fallback behavior, CopyButton component with success/failure feedback, and comprehensive tests covering success/failure/fallback paths.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the clipboard utility and CopyButton component. The re-exports in index files are necessary for the public API and are in scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
frontend/tests/components/v1/CopyButton.test.tsx (1)

41-105: Consider adding component-level regressions for fallback + callback rejection.

Add tests for:

  1. Clipboard API unavailable but execCommand succeeds (button still reaches copied state), and
  2. onCopy rejecting (button does not remain stuck in copying/disabled state).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/tests/components/v1/CopyButton.test.tsx` around lines 41 - 105, Add
two tests to cover fallback and rejection: (1) simulate Clipboard API missing by
deleting Navigator.prototype.clipboard and mock Document.prototype.execCommand
to return true, then render <CopyButton ... />, click the button and assert it
reaches the "copied" UI (idleLabel -> copiedLabel and copy-button-status shows
success) and any success callback is invoked; reference the CopyButton component
and use Document.prototype.execCommand mock for the fallback path. (2) Simulate
onCopy rejecting by passing an onCopy mock that returns a rejected promise,
render <CopyButton ... />, click the button and assert the button does not
remain disabled/stuck in a copying state (check it returns to idleLabel and is
enabled) and any onCopyError is called if provided; reference the onCopy and
onCopyError props and ensure timers (vi.useFakeTimers / vi.advanceTimersByTime)
are used if feedbackDurationMs is involved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/v1/CopyButton.tsx`:
- Around line 95-106: The handler can get stuck in "copying" because await
onCopy?.() runs before setStatus('copied'); wrap the consumer callback so the
component state is updated regardless: either call setStatus('copied') and
scheduleReset() before awaiting onCopy, or (preferable) run await onCopy?.()
inside a try/catch and put setStatus('copied') and scheduleReset() in a finally
block so they always run; if the consumer throws, call onCopyError and
setError/setGlobalError with the error inside the catch to preserve existing
error handling (refer to onCopy, onCopyError, setStatus, scheduleReset,
setError, setGlobalError).

---

Nitpick comments:
In `@frontend/tests/components/v1/CopyButton.test.tsx`:
- Around line 41-105: Add two tests to cover fallback and rejection: (1)
simulate Clipboard API missing by deleting Navigator.prototype.clipboard and
mock Document.prototype.execCommand to return true, then render <CopyButton ...
/>, click the button and assert it reaches the "copied" UI (idleLabel ->
copiedLabel and copy-button-status shows success) and any success callback is
invoked; reference the CopyButton component and use
Document.prototype.execCommand mock for the fallback path. (2) Simulate onCopy
rejecting by passing an onCopy mock that returns a rejected promise, render
<CopyButton ... />, click the button and assert the button does not remain
disabled/stuck in a copying state (check it returns to idleLabel and is enabled)
and any onCopyError is called if provided; reference the onCopy and onCopyError
props and ensure timers (vi.useFakeTimers / vi.advanceTimersByTime) are used if
feedbackDurationMs is involved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 39cee79e-3168-423a-a2c0-376dbea160a0

📥 Commits

Reviewing files that changed from the base of the PR and between 254d183 and 576b33f.

📒 Files selected for processing (6)
  • frontend/src/components/v1/CopyButton.tsx
  • frontend/src/components/v1/index.ts
  • frontend/src/utils/v1/clipboard.ts
  • frontend/src/utils/v1/index.ts
  • frontend/tests/components/v1/CopyButton.test.tsx
  • frontend/tests/utils/v1/clipboard.test.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
frontend/src/components/v1/CopyButton.tsx (1)

95-106: ⚠️ Potential issue | 🟠 Major

Callback failures can leave button stuck in copying state.

If onCopy?.() throws at line 96, execution stops before setStatus('copied') (line 97), leaving the button disabled. Same issue exists for onCopyError at line 105, though state is already updated by then.

Proposed fix: update state before awaiting callbacks
   if (result.ok) {
-    await onCopy?.();
     setStatus('copied');
     scheduleReset();
+    try {
+      await onCopy?.();
+    } catch {
+      // Consumer callback failure shouldn't break button UX
+    }
     return;
   }

   setStatus('idle');
   setError(result.error);
   setGlobalError(result.error);
-  await onCopyError?.(result.error);
+  try {
+    await onCopyError?.(result.error);
+  } catch {
+    // Consumer callback failure shouldn't bubble
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/v1/CopyButton.tsx` around lines 95 - 106, The
CopyButton handler can get stuck in "copying" if user-supplied callbacks throw;
update state before awaiting callbacks and guard callback calls with try/catch
to prevent thrown errors from aborting state updates. Specifically, in the copy
handler inside the CopyButton component setStatus('copied') and call
scheduleReset() before awaiting onCopy, and in the error path setStatus('idle')
and setError/setGlobalError before awaiting onCopyError; additionally wrap await
onCopy(...) and await onCopyError(...) in try/catch (or call them without
awaiting) so exceptions don’t prevent the prior state changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/v1/CopyButton.tsx`:
- Around line 1-139: This file defines two conflicting CopyButton
implementations (the top exported function CopyButton and a later const
CopyButton FC with a different API); remove the OLD const FC implementation (the
later CopyButton definition and its related export and props
variant/onCopySuccess) so only the new function CopyButton (with props text,
idleLabel, copyingLabel, copiedLabel, feedbackDurationMs, onCopy, onCopyError,
onClick) remains exported; after removal, ensure there are no leftover duplicate
exports or references to the old props (variant/onCopySuccess/children) and run
tests to let consumer migration to the new API happen in a separate PR.

---

Duplicate comments:
In `@frontend/src/components/v1/CopyButton.tsx`:
- Around line 95-106: The CopyButton handler can get stuck in "copying" if
user-supplied callbacks throw; update state before awaiting callbacks and guard
callback calls with try/catch to prevent thrown errors from aborting state
updates. Specifically, in the copy handler inside the CopyButton component
setStatus('copied') and call scheduleReset() before awaiting onCopy, and in the
error path setStatus('idle') and setError/setGlobalError before awaiting
onCopyError; additionally wrap await onCopy(...) and await onCopyError(...) in
try/catch (or call them without awaiting) so exceptions don’t prevent the prior
state changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 763cc16b-b125-4d8b-88bf-6355a4ae8d8c

📥 Commits

Reviewing files that changed from the base of the PR and between 576b33f and 5deb303.

📒 Files selected for processing (5)
  • frontend/src/components/v1/CopyButton.tsx
  • frontend/src/components/v1/index.ts
  • frontend/src/utils/v1/clipboard.ts
  • frontend/tests/components/v1/CopyButton.test.tsx
  • frontend/tests/utils/v1/clipboard.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/tests/components/v1/CopyButton.test.tsx
  • frontend/tests/utils/v1/clipboard.test.ts
  • frontend/src/components/v1/index.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Frontend feature: add copy-to-clipboard utility with toast feedback

1 participant