Skip to content

Add screen-reader copy success announcements#6

Merged
chiscookeke11 merged 1 commit into
mainfrom
codex/add-screen-reader-announcement-for-clipboard-copy
May 28, 2026
Merged

Add screen-reader copy success announcements#6
chiscookeke11 merged 1 commit into
mainfrom
codex/add-screen-reader-announcement-for-clipboard-copy

Conversation

@chiscookeke11
Copy link
Copy Markdown
Owner

@chiscookeke11 chiscookeke11 commented May 28, 2026

Motivation

  • Copy-to-clipboard UI currently shows visual toasts but does not reliably announce success to screen reader users, and visible toast text can duplicate audio output.
  • Provide a reusable, non-visual announcement pattern so copy success is programmatically communicated without duplicating visible toast speech.

Description

  • Add a reusable hook useCopySuccessAnnouncement with COPY_SUCCESS_TOAST_ARIA_PROPS to manage timed, repeat-safe live-region announcements (src/hooks/useCopySuccessAnnouncement.ts).
  • Add a visually-hidden live-region component CopySuccessAnnouncement that renders the announcement for screen readers (src/components/common/CopySuccessAnnouncement.tsx).
  • Wire the hook and component into existing copy flows in TransactionHashRow, TruncatedAddress, TransactionFailureDrawer, CreatorProfileHeader, and EmptyTransactionTimelineState so copy actions call announceCopySuccess and expose the live region without showing extra visible text.
  • Update the toast helper showToast (src/utils/toast.util.tsx) to accept react-hot-toast options so copy-success toasts can be emitted with `aria-live=

Codex Task

Summary by Sourcery

Introduce a reusable, screen-reader-friendly copy success announcement pattern and apply it across existing copy-to-clipboard flows.

New Features:

  • Add a useCopySuccessAnnouncement hook and CopySuccessAnnouncement component to provide timed, repeat-safe live region messages for copy success events.

Bug Fixes:

  • Ensure copy-to-clipboard actions reliably announce success to screen reader users without duplicating spoken toast content.

Enhancements:

  • Refactor TransactionHashRow, TruncatedAddress, EmptyTransactionTimelineState, TransactionFailureDrawer, and CreatorProfileHeader to use the shared copy success announcement pattern instead of inline aria-live spans.
  • Extend the toast helper to accept react-hot-toast options, enabling control over ariaProps for copy-success toasts.

Tests:

  • Add unit tests for the useCopySuccessAnnouncement hook and TransactionHashRow component.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 28, 2026

Reviewer's Guide

Implements a reusable, timed screen-reader announcement pattern for copy-to-clipboard actions, wires it into key copy flows, and updates the toast utility to support accessible aria options without duplicating spoken content.

Sequence diagram for copy success screen-reader announcement flow

sequenceDiagram
    actor User
    participant Component as CopyComponent
    participant Hook as useCopySuccessAnnouncement
    participant LiveRegion as CopySuccessAnnouncement
    participant ToastHelper as showToast
    participant ToastLib as react_hot_toast
    participant Clipboard as navigator.clipboard

    User->>Component: Click copy button
    Component->>Clipboard: writeText(value)
    Clipboard-->>Component: Promise resolved
    Component->>Hook: announceCopySuccess(message)
    Hook-->>Hook: setAnnouncement("")
    Note over Hook: 25ms delay
    Hook-->>Hook: setAnnouncement(message)
    Hook-->>LiveRegion: announcement prop updated
    LiveRegion-->>User: [aria-live polite announcement]

    Component->>ToastHelper: success("Copied to clipboard", { ariaProps: COPY_SUCCESS_TOAST_ARIA_PROPS })
    ToastHelper->>ToastLib: toast.success(message, options)
    ToastLib-->>User: Visible toast (aria-live off)

    Note over Hook: After clearAfterMs
    Hook-->>Hook: setAnnouncement("")
    Hook-->>LiveRegion: message cleared
Loading

File-Level Changes

Change Details Files
Introduce reusable hook and screen-reader-only component to manage copy success announcements via a shared live region.
  • Create useCopySuccessAnnouncement hook that manages an announcement string with delayed set/clear timers to ensure screen readers re-announce repeated messages.
  • Export COPY_SUCCESS_ANNOUNCEMENT and COPY_SUCCESS_TOAST_ARIA_PROPS to provide a default message and aria settings for copy-related toasts.
  • Add CopySuccessAnnouncement component that renders a polite, atomic sr-only live region bound to the hook’s announcement message.
src/hooks/useCopySuccessAnnouncement.ts
src/components/common/CopySuccessAnnouncement.tsx
Refactor existing copy-to-clipboard UIs to use the new announcement hook/component and remove inline live-region spans that duplicated toast messages.
  • In TransactionHashRow, wire useCopySuccessAnnouncement into the copy handler, call announceCopySuccess on success, and replace the inline sr-only span with CopySuccessAnnouncement.
  • In TruncatedAddress, call announceCopySuccess when copying the address and replace the inline sr-only status span with CopySuccessAnnouncement.
  • In EmptyTransactionTimelineState, introduce useCopySuccessAnnouncement for transaction-hash copy actions and use CopySuccessAnnouncement at the bottom while keeping a separate sr-only span only for error announcements.
  • In TransactionFailureDrawer, call announceCopySuccess for both errorCode and txHash copy actions, remove field-specific sr-only spans, and render a single CopySuccessAnnouncement near the copy controls.
  • In CreatorProfileHeader, announce copy success for the profile link and render CopySuccessAnnouncement adjacent to the share/copy button.
src/components/common/TransactionHashRow.tsx
src/components/common/TruncatedAddress.tsx
src/components/common/EmptyTransactionTimelineState.tsx
src/components/common/TransactionFailureDrawer.tsx
src/components/common/CreatorProfileHeader.tsx
Extend toast helper to support react-hot-toast options, enabling accessible aria configuration for copy success toasts.
  • Update showToast.message/success/error/loading signatures to accept optional ToastOptions and forward them to react-hot-toast.
  • Ensure each toast helper still removes existing toasts before showing a new one.
  • Use COPY_SUCCESS_TOAST_ARIA_PROPS when showing copy-related success toasts so they don’t interfere with the dedicated live region.
src/utils/toast.util.tsx
src/components/common/TransactionFailureDrawer.tsx
src/components/common/CreatorProfileHeader.tsx
Add test scaffolding for the new hook and TransactionHashRow behavior (files added but contents not shown in diff).
  • Introduce a test file for TransactionHashRow to validate copy/announcement behavior.
  • Introduce a test file for useCopySuccessAnnouncement to validate timer-based announcement lifecycle.
src/components/common/__tests__/TransactionHashRow.test.tsx
src/hooks/__tests__/useCopySuccessAnnouncement.test.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@chiscookeke11 chiscookeke11 merged commit 57a3f8d into main May 28, 2026
1 check passed
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • You now mount a separate CopySuccessAnnouncement + useCopySuccessAnnouncement pair in multiple components; consider lifting this to a shared parent (or context/provider) so you have a single live region per page and less duplicated wiring.
  • All the copy flows pass hard-coded English strings into announceCopySuccess; if you plan to localize or standardize these messages, it may be cleaner to centralize them as constants or derive them from context instead of inlining per component.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- You now mount a separate `CopySuccessAnnouncement` + `useCopySuccessAnnouncement` pair in multiple components; consider lifting this to a shared parent (or context/provider) so you have a single live region per page and less duplicated wiring.
- All the copy flows pass hard-coded English strings into `announceCopySuccess`; if you plan to localize or standardize these messages, it may be cleaner to centralize them as constants or derive them from context instead of inlining per component.

## Individual Comments

### Comment 1
<location path="src/hooks/useCopySuccessAnnouncement.ts" line_range="10" />
<code_context>
+	ToastOptions['ariaProps']
+> = {
+	role: 'status',
+	'aria-live': 'off',
+};
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `aria-live="off"` on a `role="status"` region is likely both semantically wrong and a TypeScript type mismatch with `ToastOptions['ariaProps']`.

`ToastOptions['ariaProps']` usually types `aria-live` as `'polite' | 'assertive' | undefined`, so `'off'` may not be allowed and could trigger TS errors. It also conflicts with `role="status"`, which implies a polite live region. If you want this toast not to announce, either drop both `role` and `aria-live`, or (if types permit) keep `aria-live="off"` without `role="status"`, and reserve `role="status"` for the dedicated announcement element.
</issue_to_address>

### Comment 2
<location path="src/components/common/TransactionHashRow.tsx" line_range="24-27" />
<code_context>
 	const [copied, setCopied] = useState(false);
+	const { announcement, announceCopySuccess } = useCopySuccessAnnouncement();

 	const handleCopy = async (e: React.MouseEvent) => {
 		e.stopPropagation();
 		await navigator.clipboard.writeText(hash);
+		announceCopySuccess('Transaction hash copied.');
 		setCopied(true);
 		setTimeout(() => setCopied(false), 2000);
</code_context>
<issue_to_address>
**issue (bug_risk):** Copy handler doesn’t handle clipboard failures but still announces success to the user.

If `navigator.clipboard.writeText(hash)` throws (permissions, unsupported env, etc.), we’ll still announce success and set `copied` to `true`, which is misleading. Consider wrapping this in a try/catch, only calling `announceCopySuccess` and updating `copied` on success, and handling the error path similarly to `TransactionFailureDrawer` (e.g., error toast).
</issue_to_address>

### Comment 3
<location path="src/components/common/TruncatedAddress.tsx" line_range="32-34" />
<code_context>
 	const [copied, setCopied] = useState(false);
+	const { announcement, announceCopySuccess } = useCopySuccessAnnouncement();

 	const handleCopy = async () => {
 		await navigator.clipboard.writeText(address);
+		announceCopySuccess('Address copied.');
 		setCopied(true);
 		setTimeout(() => setCopied(false), 2000);
</code_context>
<issue_to_address>
**issue (bug_risk):** Address copy handler also assumes clipboard writes always succeed but announces success unconditionally.

Since `navigator.clipboard.writeText(address)` can fail, this should be wrapped in a try/catch. On error, avoid setting `copied` to `true` and either skip the success announcement or show an error toast instead, so users don’t get misleading feedback.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

ToastOptions['ariaProps']
> = {
role: 'status',
'aria-live': 'off',
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Using aria-live="off" on a role="status" region is likely both semantically wrong and a TypeScript type mismatch with ToastOptions['ariaProps'].

ToastOptions['ariaProps'] usually types aria-live as 'polite' | 'assertive' | undefined, so 'off' may not be allowed and could trigger TS errors. It also conflicts with role="status", which implies a polite live region. If you want this toast not to announce, either drop both role and aria-live, or (if types permit) keep aria-live="off" without role="status", and reserve role="status" for the dedicated announcement element.

Comment on lines 24 to +27
const handleCopy = async (e: React.MouseEvent) => {
e.stopPropagation();
await navigator.clipboard.writeText(hash);
announceCopySuccess('Transaction hash copied.');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Copy handler doesn’t handle clipboard failures but still announces success to the user.

If navigator.clipboard.writeText(hash) throws (permissions, unsupported env, etc.), we’ll still announce success and set copied to true, which is misleading. Consider wrapping this in a try/catch, only calling announceCopySuccess and updating copied on success, and handling the error path similarly to TransactionFailureDrawer (e.g., error toast).

Comment on lines 32 to +34
const handleCopy = async () => {
await navigator.clipboard.writeText(address);
announceCopySuccess('Address copied.');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Address copy handler also assumes clipboard writes always succeed but announces success unconditionally.

Since navigator.clipboard.writeText(address) can fail, this should be wrapped in a try/catch. On error, avoid setting copied to true and either skip the success announcement or show an error toast instead, so users don’t get misleading feedback.

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.

1 participant