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
16 changes: 16 additions & 0 deletions docs/accessibility/form-labels-remaining.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Form labels still to associate (#540)

The create and edit payment link forms are done. These forms still have labels that are not
tied to their inputs, found with a search for `<label` without `htmlFor`:

- `src/app/dashboard/settings/page.tsx`: Email
- `src/app/dashboard/wallets/new/NewWalletClient.tsx`: wallet name, description, password fields
- `src/app/dashboard/wallets/[id]/api/page.tsx`, `assets/page.tsx`, `sponsorship/page.tsx`,
`whitelist/page.tsx`
- `src/components/addresses/CustomerReferenceField.tsx`
- `src/components/auth/ForgotPasswordForm.tsx`
- `src/components/settings/ChangePasswordForm.tsx`
- `src/components/wallets/ChangeWalletPasswordModal.tsx`, `EditWalletDetails.tsx`,
`RecoverWalletModal.tsx`, `WalletModals.tsx`

Labels that wrap their input are already associated and are not listed.
29 changes: 25 additions & 4 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useId, useRef, useState } from "react";
import Link from "next/link";
import { useAuth } from "@/lib/useAuth";
import { displayName } from "@/lib/auth";
Expand Down Expand Up @@ -302,24 +302,45 @@ function WalletCard({

function ManageMenu({ walletId }: { walletId: string }) {
const [open, setOpen] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
const menuId = useId();

function close() {
setOpen(false);
triggerRef.current?.focus();
}

return (
<div className="relative">
<div
className="relative"
onKeyDown={(e) => {
if (e.key === "Escape" && open) close();
}}
>
<button
ref={triggerRef}
type="button"
onClick={() => setOpen((v) => !v)}
aria-haspopup="true"
aria-expanded={open}
aria-controls={menuId}
className="flex items-center gap-1 rounded-lg border border-border px-3 py-1.5 text-xs text-foreground hover:border-border-strong"
>
Manage <span className="text-muted">⋮</span>
</button>

{open && (
<>
{/* click-away */}
{/* click-away for mouse users; Escape covers keyboard users */}
<div
aria-hidden="true"
className="fixed inset-0 z-10"
onClick={() => setOpen(false)}
/>
<div className="absolute right-0 z-20 mt-2 w-44 overflow-hidden rounded-xl border border-border bg-popover backdrop-blur-md">
<div
id={menuId}
className="absolute right-0 z-20 mt-2 w-44 overflow-hidden rounded-xl border border-border bg-popover backdrop-blur-md"
>
<Link
href={`/dashboard/wallets/${walletId}`}
className="flex items-center gap-2 px-4 py-3 text-sm text-foreground hover:bg-hover"
Expand Down
8 changes: 7 additions & 1 deletion src/app/dashboard/wallets/[id]/transactions/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,16 @@ export default function TransactionsPage({

<div className="flex items-center justify-between">
{/* asset filter tabs */}
<div className="flex gap-1 rounded-lg border border-border bg-surface-raised p-1">
<div
role="group"
aria-label="Filter by asset"
className="flex gap-1 rounded-lg border border-border bg-surface-raised p-1"
>
{assetTabs.map((key) => (
<button
key={key}
type="button"
aria-pressed={assetFilter === key}
onClick={() => setAssetFilter(key)}
className={`rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
assetFilter === key
Expand Down
3 changes: 2 additions & 1 deletion src/components/dashboard/DashboardShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ export function DashboardShell({
onClick={onLogout}
className="text-sm text-muted transition-colors hover:text-foreground"
title="Log out"
aria-label="Log out"
>
⏻
<span aria-hidden="true">⏻</span>
</button>
</div>
</header>
Expand Down
18 changes: 12 additions & 6 deletions src/components/payment-links/CreatePaymentLinkForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, type FormEvent } from "react";
import { useId, useState, type FormEvent } from "react";
import { Modal } from "@/components/dashboard/Modal";
import { ActionButton } from "@/components/dashboard/WalletUI";
import { uploadImage, validateImage } from "@/lib/uploads";
Expand Down Expand Up @@ -31,6 +31,7 @@ export function CreatePaymentLinkForm({
const [imageUrl, setImageUrl] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const formId = useId();

async function handleImageChange(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
Expand Down Expand Up @@ -84,7 +85,7 @@ export function CreatePaymentLinkForm({
<Modal title="Create payment link" onClose={onClose}>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="text-sm font-medium text-foreground">
<label htmlFor={`${formId}-image`} className="text-sm font-medium text-foreground">
Image (optional)
</label>
<div className="mt-1 flex items-center gap-3">
Expand All @@ -98,6 +99,7 @@ export function CreatePaymentLinkForm({
</div>
<div className="flex-1">
<input
id={`${formId}-image`}
type="file"
accept="image/*"
onChange={handleImageChange}
Expand All @@ -115,19 +117,21 @@ export function CreatePaymentLinkForm({
</div>
</div>
<div>
<label className="text-sm font-medium text-foreground">Name</label>
<label htmlFor={`${formId}-name`} className="text-sm font-medium text-foreground">Name</label>
<input
id={`${formId}-name`}
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. Product payment"
className="mt-1 w-full rounded-lg border border-border bg-surface-sunken px-3 py-2 text-sm text-foreground outline-none focus:border-burgundy/50"
/>
</div>
<div>
<label className="text-sm font-medium text-foreground">
<label htmlFor={`${formId}-description`} className="text-sm font-medium text-foreground">
Description (optional)
</label>
<textarea
id={`${formId}-description`}
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Brief description of this payment link"
Expand All @@ -136,8 +140,9 @@ export function CreatePaymentLinkForm({
/>
</div>
<div>
<label className="text-sm font-medium text-foreground">Amount (USD)</label>
<label htmlFor={`${formId}-amount`} className="text-sm font-medium text-foreground">Amount (USD)</label>
<input
id={`${formId}-amount`}
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Leave empty for flexible amount"
Expand All @@ -148,10 +153,11 @@ export function CreatePaymentLinkForm({
</p>
</div>
<div>
<label className="text-sm font-medium text-foreground">
<label htmlFor={`${formId}-redirect`} className="text-sm font-medium text-foreground">
Redirect URL (optional)
</label>
<input
id={`${formId}-redirect`}
value={redirectUrl}
onChange={(e) => setRedirectUrl(e.target.value)}
placeholder="https://your-site.com/thank-you"
Expand Down
19 changes: 12 additions & 7 deletions src/components/payment-links/EditPaymentLinkForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, type FormEvent } from "react";
import { useId, useState, type FormEvent } from "react";
import { ActionButton } from "@/components/dashboard/WalletUI";
import { asAuthToken, asWalletId } from "@/lib/brands";
import { formatStroops } from "@/lib/amount";
Expand Down Expand Up @@ -33,6 +33,7 @@ export function EditPaymentLinkForm({
const [uploading, setUploading] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const formId = useId();

async function handleImageChange(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
Expand Down Expand Up @@ -78,7 +79,7 @@ export function EditPaymentLinkForm({
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="text-sm font-medium text-foreground">Image (optional)</label>
<label htmlFor={`${formId}-image`} className="text-sm font-medium text-foreground">Image (optional)</label>
<div className="mt-1 flex items-center gap-3">
<div className="flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-lg border border-border bg-surface-sunken">
{isTrustedImageUrl(imageUrl) ? (
Expand All @@ -90,6 +91,7 @@ export function EditPaymentLinkForm({
</div>
<div className="flex-1">
<input
id={`${formId}-image`}
type="file"
accept="image/*"
onChange={handleImageChange}
Expand All @@ -109,21 +111,23 @@ export function EditPaymentLinkForm({
</div>
</div>
<div>
<label className="text-sm font-medium text-foreground">Name</label>
<input value={name} onChange={(e) => setName(e.target.value)} className={inputCls} />
<label htmlFor={`${formId}-name`} className="text-sm font-medium text-foreground">Name</label>
<input id={`${formId}-name`} value={name} onChange={(e) => setName(e.target.value)} className={inputCls} />
</div>
<div>
<label className="text-sm font-medium text-foreground">Description (optional)</label>
<label htmlFor={`${formId}-description`} className="text-sm font-medium text-foreground">Description (optional)</label>
<textarea
id={`${formId}-description`}
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={2}
className={inputCls}
/>
</div>
<div>
<label className="text-sm font-medium text-foreground">Amount (USD)</label>
<label htmlFor={`${formId}-amount`} className="text-sm font-medium text-foreground">Amount (USD)</label>
<input
id={`${formId}-amount`}
value={link.amount_usdc_stroops !== null ? formatStroops(link.amount_usdc_stroops) : "Flexible"}
disabled
className={`${inputCls} opacity-60`}
Expand All @@ -133,8 +137,9 @@ export function EditPaymentLinkForm({
</p>
</div>
<div>
<label className="text-sm font-medium text-foreground">Redirect URL (optional)</label>
<label htmlFor={`${formId}-redirect`} className="text-sm font-medium text-foreground">Redirect URL (optional)</label>
<input
id={`${formId}-redirect`}
value={redirectUrl}
onChange={(e) => setRedirectUrl(e.target.value)}
placeholder="https://your-site.com/thank-you"
Expand Down