Skip to content

Unify success-feedback UX: useTransientLabel hook + ActionConfirmationCard cleanup (ERMAIN-435)#805

Draft
bdart wants to merge 6 commits into
mainfrom
feat/unify-success-feedback-da97
Draft

Unify success-feedback UX: useTransientLabel hook + ActionConfirmationCard cleanup (ERMAIN-435)#805
bdart wants to merge 6 commits into
mainfrom
feat/unify-success-feedback-da97

Conversation

@bdart

@bdart bdart commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Resolves ERMAIN-435 — unifies all web success/confirmation feedback onto the shared ~2 s label-swap pattern.

Changes

1. useTransientLabel shared hook (frontend/src/hooks/ui/useTransientLabel.ts)

New hook that encapsulates the recurring useState(false) + setTimeout(2000) pattern:

  • isActive becomes true when trigger() is called, auto-resets after delay ms (default 2000)
  • Timer is cleared on unmount — no leaks on any call site
  • Calling trigger() while already active restarts the timer (debounce-safe)
  • Optional announcement param → srAnnouncement string for a role="status" sr-only live region (answers the A11y question from the issue: yes, the helper includes aria-live support, which callers opt in to)

2. ActionConfirmationCard cleanup

Removes the now-zero-consumer resolved-row machinery introduced before ERMAIN-387:

  • ActionConfirmationStatus type removed (also removed from library/index.ts)
  • status and resolvedLabel props removed
  • useLayoutEffect for pending→resolved focus removed
  • Resolved-row rendering, conditional className/role/aria-label removed
  • announcement live region simplified: now fires once on mount only (still useful for SR users who don't have focus on the card when it appears)

Tests updated: removed 5 tests covering the now-deleted resolved-state behaviour; kept and simplified the mount-announcement test.

3. Web call-site migrations

All six ad-hoc implementations replaced with useTransientLabel:

File Was Notes
MessageContent.tsx React.useState + inline setTimeout Adds srAnnouncement live region
EratoEmailSuggestion.tsx useState + inline setTimeout
DefaultMessageControls.tsx useState + useEffect/clearTimeout Removes the explicit useEffect entirely
ChatShareDialog.tsx useState + window.setTimeout Removes manual setIsCopied(false) on link-disable
ChatMessage.tsx useState + window.setTimeout
customer/examples/MessageControls.example.tsx useState + useEffect/clearTimeout

4. Convention documentation (frontend/src/components/README.md)

Documents the three-tier feedback convention:

  • Transient label swap (useTransientLabel) for micro-action success
  • Toasts reserved for auth/permission + session/decision flows
  • Errors via sticky toast or inline <Alert type="error">

Out of scope

The Outlook add-in (done on ERMAIN-387 branch, PR #804).

Test results

  • TypeScript: ✅ clean (pnpm typecheck)
  • ESLint strict: ✅ 0 warnings (pnpm lint:strict)
  • Tests: ✅ 782 passed, 11 skipped (pnpm test run)

Linear Issue: ERMAIN-435

Open in Web Open in Cursor 

cursoragent and others added 6 commits July 6, 2026 17:58
…dback

Extracts the recurring ad-hoc pattern (useState + setTimeout(2000)) into a
single, reusable hook. The hook:
- resets isActive to false after delay ms (default 2000)
- clears the timer on unmount — no leaks
- restarts the timer if trigger is called while already active
- optionally provides an srAnnouncement string for a role=status live
  region so icon-only swaps are announced to screen readers (ERMAIN-435 A11y point)

Co-authored-by: Daniel <bdart@users.noreply.github.com>
…Label machinery

The status / resolvedLabel / ActionConfirmationStatus type had zero consumers
after ERMAIN-387 (add-in confirm cards now close on resolution instead of
rendering a persistent resolved row).

Removed:
- ActionConfirmationStatus type and status/resolvedLabel props
- isPending conditional rendering (card is always the pending form)
- useLayoutEffect that moved focus on pending→resolved transition
- resolvedRowLabel / resolvedTitle computed strings for resolved state
- announcement live region update on resolution
- conditional className / role / aria-label for resolved variant

Kept:
- mount-time focus grab (focusIfUnclaimed)
- scrollIntoViewOnMount
- polite sr-only announcement on mount for SR users who may not have focus
  on the card when it appears (simplified to fire once on mount)

Co-authored-by: Daniel <bdart@users.noreply.github.com>
Replaces six independent ad-hoc implementations of the ~2s state+setTimeout
copy-feedback pattern with the shared useTransientLabel hook.

Sites migrated:
- MessageContent.tsx (code-block copy button) — adds srAnnouncement live region
- EratoEmailSuggestion.tsx (email copy button)
- DefaultMessageControls.tsx (icon-only copy; removes useEffect+clearTimeout)
- ChatShareDialog.tsx (share-URL copy; removes manual setIsCopied(false) on toggle-off)
- ChatMessage.tsx (error-report copy button)
- customer/examples/MessageControls.example.tsx (example override)

No behaviour changes beyond eliminating timer leaks on unmount and
standardising the 2000 ms reset across all sites.

Co-authored-by: Daniel <bdart@users.noreply.github.com>
Establishes and documents the three-tier success/confirmation UX:
- Transient ~2s label swap (useTransientLabel) for micro-actions
- Toasts reserved for auth/permission and session/decision flows
- Errors use sticky toast or inline Alert

Resolves ERMAIN-435 item 4 (document the convention).

Co-authored-by: Daniel <bdart@users.noreply.github.com>
- Add new 'Copied to clipboard' message (from MessageContent.tsx srAnnouncement)
- Mark removed actionConfirmation.confirmed and actionConfirmation.dismissed
  messages as obsolete (ActionConfirmationCard resolved-row cleanup)

Co-authored-by: Daniel <bdart@users.noreply.github.com>
Co-authored-by: Daniel <bdart@users.noreply.github.com>
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.

2 participants