Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions frontend/src/components/stream-creation/CancelConfirmModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
* already-withdrawn funds) before the user commits.
*/

import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import { Button } from "@/components/ui/Button";
import { useModalDialog } from "@/hooks/useModalDialog";
import toast from "react-hot-toast";

interface CancelConfirmModalProps {
Expand All @@ -33,14 +34,7 @@ export const CancelConfirmModal: React.FC<CancelConfirmModalProps> = ({
}) => {
const [isSubmitting, setIsSubmitting] = useState(false);

// Escape key support
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape" && !isSubmitting) onClose();
};
window.addEventListener("keydown", handleEscape);
return () => window.removeEventListener("keydown", handleEscape);
}, [onClose, isSubmitting]);
const dialogRef = useModalDialog({ onClose, isCloseDisabled: isSubmitting });

const remaining = deposited - withdrawn;

Expand All @@ -58,11 +52,17 @@ export const CancelConfirmModal: React.FC<CancelConfirmModalProps> = ({
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
role="dialog"
aria-modal="true"
aria-labelledby="cancel-confirm-modal-title"
onClick={(e) => {
if (e.target === e.currentTarget && !isSubmitting) onClose();
}}
>
<div className="glass-card relative w-full max-w-md mx-4 rounded-2xl border border-red-500/30 p-8">
<div
ref={dialogRef}
className="glass-card relative w-full max-w-md mx-4 rounded-2xl border border-red-500/30 p-8"
>
{/* Header */}
<div className="flex items-start gap-4 mb-6">
{/* Warning icon */}
Expand All @@ -73,7 +73,7 @@ export const CancelConfirmModal: React.FC<CancelConfirmModalProps> = ({
</svg>
</div>
<div>
<h2 className="text-xl font-bold">Cancel Stream?</h2>
<h2 id="cancel-confirm-modal-title" className="text-xl font-bold">Cancel Stream?</h2>
<p className="text-sm text-slate-400 mt-1">
This action is permanent and cannot be undone.
</p>
Expand Down
24 changes: 11 additions & 13 deletions frontend/src/components/stream-creation/StreamCreationWizard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";
import React, { useState } from "react";
import { hasValidPrecision } from "@/lib/amount";
import { useModalDialog } from "@/hooks/useModalDialog";
import { Stepper } from "../ui/Stepper";
import { Button } from "../ui/Button";
import { RecipientStep } from "./RecipientStep";
Expand Down Expand Up @@ -116,6 +117,8 @@ export const StreamCreationWizard: React.FC<StreamCreationWizardProps> = ({

const router = useRouter();

const dialogRef = useModalDialog({ onClose });

const [walletBalance, setWalletBalance] = useState<string | null>(null);
const [walletBalanceLoading, setWalletBalanceLoading] = useState(false);
const [walletBalanceError, setWalletBalanceError] = useState<string | null>(null);
Expand Down Expand Up @@ -440,30 +443,25 @@ export const StreamCreationWizard: React.FC<StreamCreationWizardProps> = ({
}
};

// Handle Escape key to close
React.useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
window.addEventListener('keydown', handleEscape);
return () => window.removeEventListener('keydown', handleEscape);
}, [onClose]);

return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
role="dialog"
aria-modal="true"
aria-labelledby="stream-creation-wizard-title"
onClick={(e) => {
// Close on backdrop click
if (e.target === e.currentTarget) {
onClose();
}
}}
>
<div className="glass-card relative z-10 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto rounded-2xl border border-glass-border p-8">
<div
ref={dialogRef}
className="glass-card relative z-10 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto rounded-2xl border border-glass-border p-8"
>
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold">Create Payment Stream</h2>
<h2 id="stream-creation-wizard-title" className="text-2xl font-bold">Create Payment Stream</h2>
<button
type="button"
onClick={onClose}
Expand Down
28 changes: 15 additions & 13 deletions frontend/src/components/stream-creation/TopUpModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
* Collects an amount, shows a confirmation summary, and calls onConfirm.
*/

import React, { useRef, useEffect, useState } from "react";
import React, { useRef, useState } from "react";
import { Button } from "@/components/ui/Button";
import { hasValidPrecision, validateAmountInput } from "@/utils/amount";
import { useModalDialog } from "@/hooks/useModalDialog";
import toast from "react-hot-toast";

interface TopUpModalProps {
Expand All @@ -32,16 +33,11 @@ export const TopUpModal: React.FC<TopUpModalProps> = ({
const [isSubmitting, setIsSubmitting] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);

// Auto-focus and Escape key support
useEffect(() => {
inputRef.current?.focus();

const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape" && !isSubmitting) onClose();
};
window.addEventListener("keydown", handleEscape);
return () => window.removeEventListener("keydown", handleEscape);
}, [onClose, isSubmitting]);
const dialogRef = useModalDialog({
onClose,
isCloseDisabled: isSubmitting,
initialFocusRef: inputRef,
});

// Token decimals - using 7 as default (Stellar standard)
const TOKEN_DECIMALS = 7;
Expand Down Expand Up @@ -80,14 +76,20 @@ export const TopUpModal: React.FC<TopUpModalProps> = ({
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
role="dialog"
aria-modal="true"
aria-labelledby="topup-modal-title"
onClick={(e) => {
if (e.target === e.currentTarget && !isSubmitting) onClose();
}}
>
<div className="glass-card relative w-full max-w-md mx-4 rounded-2xl border border-glass-border p-8">
<div
ref={dialogRef}
className="glass-card relative w-full max-w-md mx-4 rounded-2xl border border-glass-border p-8"
>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold">Top Up Stream</h2>
<h2 id="topup-modal-title" className="text-xl font-bold">Top Up Stream</h2>
<button
type="button"
onClick={onClose}
Expand Down
93 changes: 0 additions & 93 deletions frontend/src/components/ui/Banner.tsx

This file was deleted.

32 changes: 0 additions & 32 deletions frontend/src/hooks/useCancelStream.ts

This file was deleted.

84 changes: 84 additions & 0 deletions frontend/src/hooks/useModalDialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useEffect, useRef, type RefObject } from "react";

const FOCUSABLE_SELECTOR = [
"a[href]",
"button:not([disabled])",
"input:not([disabled])",
"select:not([disabled])",
"textarea:not([disabled])",
'[tabindex]:not([tabindex="-1"])',
].join(",");

interface UseModalDialogOptions {
onClose: () => void;
isCloseDisabled?: boolean;
initialFocusRef?: RefObject<HTMLElement | null>;
}

export function useModalDialog({
onClose,
isCloseDisabled = false,
initialFocusRef,
}: UseModalDialogOptions) {
const dialogRef = useRef<HTMLDivElement>(null);
const previouslyFocusedRef = useRef<HTMLElement | null>(null);

useEffect(() => {
previouslyFocusedRef.current = document.activeElement as HTMLElement | null;

if (initialFocusRef?.current) {
initialFocusRef.current.focus();
} else {
const focusable = dialogRef.current?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);
focusable?.[0]?.focus();
}

document.body.style.overflow = "hidden";

const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
event.preventDefault();
if (!isCloseDisabled) {
onClose();
}
return;
}

if (event.key !== "Tab" || !dialogRef.current) {
return;
}

const focusableElements = Array.from(
dialogRef.current.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),
);

if (focusableElements.length === 0) {
event.preventDefault();
return;
}

const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];

if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
}

if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
};

document.addEventListener("keydown", handleKeyDown);

return () => {
document.removeEventListener("keydown", handleKeyDown);
document.body.style.overflow = "";
previouslyFocusedRef.current?.focus();
};
}, [onClose, isCloseDisabled, initialFocusRef]);

return dialogRef;
}
Loading
Loading