Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@TWilson023
Copy link
Collaborator

@TWilson023 TWilson023 commented Dec 19, 2025

Summary by CodeRabbit

  • Improvements

    • Payout confirmation now adapts prompts for touch vs. pointer devices and preserves the hold-to-confirm animation.
    • Confirmation UI moved into a popover for clearer focus and guidance.
  • New Features

    • A cancellation flow tracks repeated cancels to proactively surface the popover and assist users.
    • Confirmation button label now supports rich content (not limited to plain text).

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link
Contributor

vercel bot commented Dec 19, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
dub Ready Ready Preview Dec 19, 2025 7:57pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 19, 2025

Walkthrough

Detects touch devices and adapts confirmation prompt text; refactors the hold-to-confirm flow by wrapping the confirm button in a Popover, adding a cancel counter and handleCancel logic, and changing ConfirmPayoutsButton's text prop type from string to ReactNode.

Changes

Cohort / File(s) Summary
Confirm Payouts Sheet UI
apps/web/ui/partners/confirm-payouts-sheet.tsx
Added isTouchDevice detection and conditional prompt text; introduced cancelCounter state and handleCancel to track canceled holds; refactored ConfirmPayoutsButton to render inside a Popover; preserved hold-to-confirm animation/progress logic; changed text prop type from string to ReactNode.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10–15 minutes

  • Verify pointer event routing through handleCancel and correct resets of cancelCounter.
  • Confirm all call sites supply compatible ReactNode values for text.

Possibly related PRs

Suggested reviewers

  • steven-tey

Poem

🐰 I nibble on code and hop with delight,
A Popover here and prompts that feel right,
Hold if you must, or click if you please,
Counters and nodes make confirmations breeze —
Hooray for payouts, snug as a byte! ✨

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Improve payout confirmation UX' accurately reflects the main changes, which focus on enhancing user experience through device-aware interaction patterns and visual feedback.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch confirm-payout

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
Contributor

@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 (2)
apps/web/ui/partners/confirm-payouts-sheet.tsx (2)

889-891: Clarify the Popover close behavior.

The setOpenPopover={() => {}} pattern prevents the Popover from closing via ESC key or outside clicks, forcing users to click the content text to dismiss it. This deviates from standard Popover UX and may confuse users.

Consider either:

  1. Using a proper setOpenPopover handler if you want controlled closing
  2. Documenting why normal close behavior is intentionally disabled
🔎 Alternative: Allow standard Popover close behavior
  <Popover
    openPopover={cancelCounter >= 2}
-   setOpenPopover={() => {}}
+   setOpenPopover={(open) => {
+     if (!open) setCancelCounter(0);
+   }}
    content={
      <div
        className="text-content-subtle select-none px-2 py-0.5 text-xs"
-       onClick={() => setCancelCounter(0)}
      >
        Keep holding the button to confirm
      </div>
    }

914-914: TODO: Keyboard control implementation.

The hold-to-confirm interaction currently lacks keyboard support, which creates an accessibility barrier for keyboard-only users.

Would you like me to help implement keyboard control (e.g., holding Space or Enter key) or open an issue to track this accessibility improvement?

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b78876d and 3e0145f.

📒 Files selected for processing (1)
  • apps/web/ui/partners/confirm-payouts-sheet.tsx (7 hunks)
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
📚 Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.

Applied to files:

  • apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-08-25T21:03:24.285Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx:1-1
Timestamp: 2025-08-25T21:03:24.285Z
Learning: In Next.js App Router, Server Components that use hooks can work without "use client" directive if they are only imported by Client Components, as they get "promoted" to run on the client side within the Client Component boundary.

Applied to files:

  • apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-09-18T16:33:17.719Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2858
File: apps/web/ui/partners/partner-application-tabs.tsx:1-1
Timestamp: 2025-09-18T16:33:17.719Z
Learning: When a React component in Next.js App Router uses non-serializable props (like setState functions), adding "use client" directive can cause serialization warnings. If the component is only imported by Client Components, it's better to omit the "use client" directive to avoid these warnings while still getting client-side execution through promotion.

Applied to files:

  • apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-09-17T17:40:35.470Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/resources/program-help-and-support.tsx:95-121
Timestamp: 2025-09-17T17:40:35.470Z
Learning: In the Dub UI Switch component, providing a truthy `disabledTooltip` prop automatically disables the switch and prevents user interaction, so an explicit `disabled` prop is not needed when using `disabledTooltip`. The component computes `switchDisabled = disabledTooltip ? true : disabled || loading` and passes this to the underlying Radix Switch primitive.

Applied to files:

  • apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-09-17T17:40:35.470Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/resources/program-help-and-support.tsx:95-121
Timestamp: 2025-09-17T17:40:35.470Z
Learning: In the Dub UI Switch component, providing a truthy `disabledTooltip` prop automatically disables the switch and prevents user interaction, so an explicit `disabled` prop is not needed when using `disabledTooltip`.

Applied to files:

  • apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-10-08T21:33:23.553Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2936
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/add-hostname-modal.tsx:28-34
Timestamp: 2025-10-08T21:33:23.553Z
Learning: In the dub/ui Button component, when the `disabledTooltip` prop is set to a non-undefined value (e.g., a string), the button is automatically disabled. Therefore, it's not necessary to also add the same condition to the `disabled` prop—setting `disabledTooltip={permissionsError || undefined}` is sufficient to disable the button when there's a permissions error.

Applied to files:

  • apps/web/ui/partners/confirm-payouts-sheet.tsx
🧬 Code graph analysis (1)
apps/web/ui/partners/confirm-payouts-sheet.tsx (2)
packages/utils/src/functions/currency-formatter.ts (1)
  • currencyFormatter (7-22)
packages/ui/src/shimmer-dots.tsx (1)
  • ShimmerDots (52-188)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

Copy link
Contributor

@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: 2

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e0145f and 45f7384.

📒 Files selected for processing (1)
  • apps/web/ui/partners/confirm-payouts-sheet.tsx (7 hunks)
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
📚 Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.

Applied to files:

  • apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-11-24T16:36:36.196Z
Learnt from: CR
Repo: dubinc/dub PR: 0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-11-24T16:36:36.196Z
Learning: Applies to packages/hubspot-app/app/cards/**/*.{js,jsx,ts,tsx} : The global `window` object is not available in card components

Applied to files:

  • apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-11-24T16:36:36.196Z
Learnt from: CR
Repo: dubinc/dub PR: 0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-11-24T16:36:36.196Z
Learning: Applies to packages/hubspot-app/app/settings/**/*.{js,jsx,ts,tsx} : The global `window` object is not available in settings components

Applied to files:

  • apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-08-25T21:03:24.285Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx:1-1
Timestamp: 2025-08-25T21:03:24.285Z
Learning: In Next.js App Router, Server Components that use hooks can work without "use client" directive if they are only imported by Client Components, as they get "promoted" to run on the client side within the Client Component boundary.

Applied to files:

  • apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-09-18T16:33:17.719Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2858
File: apps/web/ui/partners/partner-application-tabs.tsx:1-1
Timestamp: 2025-09-18T16:33:17.719Z
Learning: When a React component in Next.js App Router uses non-serializable props (like setState functions), adding "use client" directive can cause serialization warnings. If the component is only imported by Client Components, it's better to omit the "use client" directive to avoid these warnings while still getting client-side execution through promotion.

Applied to files:

  • apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-09-17T17:40:35.470Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/resources/program-help-and-support.tsx:95-121
Timestamp: 2025-09-17T17:40:35.470Z
Learning: In the Dub UI Switch component, providing a truthy `disabledTooltip` prop automatically disables the switch and prevents user interaction, so an explicit `disabled` prop is not needed when using `disabledTooltip`. The component computes `switchDisabled = disabledTooltip ? true : disabled || loading` and passes this to the underlying Radix Switch primitive.

Applied to files:

  • apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-09-17T17:40:35.470Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/resources/program-help-and-support.tsx:95-121
Timestamp: 2025-09-17T17:40:35.470Z
Learning: In the Dub UI Switch component, providing a truthy `disabledTooltip` prop automatically disables the switch and prevents user interaction, so an explicit `disabled` prop is not needed when using `disabledTooltip`.

Applied to files:

  • apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-10-08T21:33:23.553Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2936
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/add-hostname-modal.tsx:28-34
Timestamp: 2025-10-08T21:33:23.553Z
Learning: In the dub/ui Button component, when the `disabledTooltip` prop is set to a non-undefined value (e.g., a string), the button is automatically disabled. Therefore, it's not necessary to also add the same condition to the `disabled` prop—setting `disabledTooltip={permissionsError || undefined}` is sufficient to disable the button when there's a permissions error.

Applied to files:

  • apps/web/ui/partners/confirm-payouts-sheet.tsx
🧬 Code graph analysis (1)
apps/web/ui/partners/confirm-payouts-sheet.tsx (2)
packages/utils/src/functions/currency-formatter.ts (1)
  • currencyFormatter (7-22)
packages/ui/src/shimmer-dots.tsx (1)
  • ShimmerDots (52-188)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (5)
apps/web/ui/partners/confirm-payouts-sheet.tsx (5)

33-33: LGTM: Import additions are appropriate.

The new imports support the Popover wrapper, type flexibility (ReactNode), and client-side touch detection logic.

Also applies to: 53-60


738-739: Conditional text logic is correct.

The adaptive "Press" vs "Click" wording improves UX for touch devices. Ensure the touch detection is fixed per the previous comment.


807-807: LGTM: Type broadened appropriately.

Changing text from string to ReactNode enables the complex inline JSX structure for the hold-to-confirm animation.


848-848: LGTM: Cancel counter logic is sound.

Tracking cancellation attempts and resetting on submission enables the progressive hint UX (popover appears after 2+ cancellations).

Also applies to: 857-857


881-887: LGTM: Cancel handler is well-guarded.

The handler appropriately checks holding state and success state before incrementing the counter.

Comment on lines +643 to +647
const [isTouchDevice, setIsTouchDevice] = useState(false);

useEffect(() => {
setIsTouchDevice(window.matchMedia("(pointer: coarse)").matches);
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Hydration mismatch risk and missing media query listener.

The SSR crash is fixed by using useEffect, but two issues remain:

  1. Hydration mismatch: The initial state is false, so SSR and the first client render will show "Click" even on touch devices. After useEffect runs, it switches to "Press", potentially causing a React hydration warning and a visible text flicker.
  2. No adaptive behavior: Without a media query change listener, the UI won't update if the user switches input methods (e.g., connecting a mouse to a tablet).
🔎 Proposed fix: Initialize with `undefined` and add change listener
- const [isTouchDevice, setIsTouchDevice] = useState(false);
+ const [isTouchDevice, setIsTouchDevice] = useState<boolean | undefined>(undefined);

  useEffect(() => {
-   setIsTouchDevice(window.matchMedia("(pointer: coarse)").matches);
+   const mediaQuery = window.matchMedia("(pointer: coarse)");
+   setIsTouchDevice(mediaQuery.matches);
+
+   const handleChange = (e: MediaQueryListEvent) => setIsTouchDevice(e.matches);
+   mediaQuery.addEventListener("change", handleChange);
+   return () => mediaQuery.removeEventListener("change", handleChange);
  }, []);

Then update the text to handle the undefined state during SSR:

  text={
    amount && amount > 0
-     ? `${isTouchDevice ? "Press" : "Click"} and hold to confirm ${currencyFormatter(amount)} payout`
-     : `${isTouchDevice ? "Press" : "Click"} and hold to confirm payout`
+     ? `${isTouchDevice === undefined ? "Press or click" : isTouchDevice ? "Press" : "Click"} and hold to confirm ${currencyFormatter(amount)} payout`
+     : `${isTouchDevice === undefined ? "Press or click" : isTouchDevice ? "Press" : "Click"} and hold to confirm payout`
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [isTouchDevice, setIsTouchDevice] = useState(false);
useEffect(() => {
setIsTouchDevice(window.matchMedia("(pointer: coarse)").matches);
}, []);
const [isTouchDevice, setIsTouchDevice] = useState<boolean | undefined>(undefined);
useEffect(() => {
const mediaQuery = window.matchMedia("(pointer: coarse)");
setIsTouchDevice(mediaQuery.matches);
const handleChange = (e: MediaQueryListEvent) => setIsTouchDevice(e.matches);
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, []);
🤖 Prompt for AI Agents
In apps/web/ui/partners/confirm-payouts-sheet.tsx around lines 643-647, the
state for isTouchDevice is initialized to false causing a hydration mismatch and
there is no media query listener to react to input-method changes; change the
initial state to undefined (or null) so SSR and first client render are
consistent, create a window.matchMedia("(pointer: coarse)") media query in
useEffect, set the state from mq.matches on mount, add an mq.addEventListener or
mq.addListener change handler to update the state when the media query changes,
and remove the listener in the cleanup; update the UI rendering to handle the
undefined state (render a neutral fallback or nothing) to avoid showing "Click"
or "Press" until the client determination is available.

Comment on lines +890 to +984
<Popover
openPopover={cancelCounter >= 2}
setOpenPopover={() => {}}
content={
<div
className="text-content-subtle select-none px-2 py-0.5 text-xs"
onClick={() => setCancelCounter(0)}
>
Keep holding the button to confirm
</div>
}
disabled={disabled}
disabledTooltip={disabledTooltip}
/>
side="top"
>
<div className="w-full">
<Button
type="button"
variant="primary"
className={cn(
"relative overflow-hidden",
isSuccess && "border-green-500 bg-green-500",
"active:scale-[0.98]",
)}
textWrapperClassName="!overflow-visible select-none"
{...(!disabled &&
!disabledTooltip && {
// TODO: Handle keyboard control
onPointerDown: () => (holding.current = true),
onPointerUp: handleCancel,
onPointerLeave: handleCancel,
onPointerCancel: handleCancel,
})}
text={
<>
<div
ref={loadingBar}
className={cn(
"pointer-events-none absolute inset-y-0 left-0 overflow-hidden",
!isSuccess && "bg-[linear-gradient(90deg,#fff1,#fff4)]",
)}
>
<ShimmerDots
className="inset-[unset] inset-y-0 left-0 w-[600px] opacity-30"
color={[1, 1, 1]}
/>
</div>
<div className="relative text-center">
<div
className={cn(
"truncate transition-[transform,opacity] duration-300",
roundedProgress >= 0.5 && "-translate-y-4 opacity-0",
)}
>
{text}
</div>
<div
className={cn(
"pointer-events-none absolute inset-0 transition-[transform,opacity] duration-300",
roundedProgress < 0.5 && "translate-y-4 opacity-0",
roundedProgress >= 1 && "-translate-y-4 opacity-0",
)}
aria-hidden
>
Preparing payout...
</div>
<div
className={cn(
"pointer-events-none absolute inset-0 flex items-center justify-center transition-[transform,opacity] duration-300",
roundedProgress < 1 &&
"-translate-x-1 translate-y-4 opacity-0",
roundedProgress >= 1 &&
isSuccess &&
"-translate-y-4 translate-x-3 opacity-0",
)}
aria-hidden
>
<PaperPlane className="size-4" />
</div>
<div
className={cn(
"pointer-events-none absolute inset-0 flex items-center justify-center transition-[transform,opacity] duration-300",
(roundedProgress < 1 || !isSuccess) &&
"translate-y-4 opacity-0",
)}
aria-hidden
>
Payout sent
</div>
</div>
</>
}
disabled={disabled}
disabledTooltip={disabledTooltip}
/>
</div>
</Popover>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keyboard accessibility is missing.

The TODO comment on line 916 indicates that keyboard control is not implemented. This means keyboard-only users cannot use the hold-to-confirm feature, which is a significant accessibility barrier.

Consider implementing keyboard support:

  • Space/Enter key pressed → start holding
  • Space/Enter key released → trigger handleCancel
  • Add onKeyDown and onKeyUp handlers similar to the pointer events
  • Ensure focus states are clearly visible
🔎 Example implementation for keyboard support
  {...(!disabled &&
    !disabledTooltip && {
      // TODO: Handle keyboard control
      onPointerDown: () => (holding.current = true),
      onPointerUp: handleCancel,
      onPointerLeave: handleCancel,
      onPointerCancel: handleCancel,
+     onKeyDown: (e) => {
+       if (e.key === ' ' || e.key === 'Enter') {
+         e.preventDefault();
+         holding.current = true;
+       }
+     },
+     onKeyUp: (e) => {
+       if (e.key === ' ' || e.key === 'Enter') {
+         e.preventDefault();
+         handleCancel();
+       }
+     },
    })}

@steven-tey steven-tey merged commit 7843dc7 into main Dec 19, 2025
7 of 8 checks passed
@steven-tey steven-tey deleted the confirm-payout branch December 19, 2025 20:10
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.

3 participants