From 022a6bd2a6f89b4f5b7b2315f7739df0b02402df Mon Sep 17 00:00:00 2001 From: isra el Date: Sun, 19 Jul 2026 02:00:11 +0300 Subject: [PATCH] fix: repair form labelling and navigation landmarks None of the three password fields declared autoComplete, so password managers could not tell them apart and would fill the saved password into the wrong box. They now declare current-password and new-password. The delete-account dialog, the most destructive action in the app, had a label whose htmlFor pointed at an id the textarea never had, and an email confirmation field with no label at all. Both are now wired, and the controls moved out of DialogDescription, which Radix targets with aria-describedby: opening the dialog previously read the labels and placeholders back as one long description string. Validation errors are now tied to their inputs via aria-describedby and announced with role=alert instead of appearing silently. confirmPassword required 4 characters while newPassword required 8, so a five-character confirmation reported "Passwords must match" rather than the actual length problem. Five icon-only buttons had no accessible name: copy API key, copy device ID, the API key menu, and both promo code copy buttons. Four of the five nav landmarks were unlabelled, leaving them indistinguishable in a screen reader's landmark list. Added a skip-to-content link, since keyboard users otherwise tab the whole sidebar on every page. The delete-account label guard was confirmed to fail against the previous markup. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../(app)/dashboard/(components)/api-keys.tsx | 7 +- .../(components)/black-friday-modal.tsx | 4 +- .../(components)/change-password-form.tsx | 56 ++++++++++++-- .../(components)/delete-account-form.tsx | 73 ++++++++++++------- .../dashboard/(components)/device-list.tsx | 2 + .../(components)/generate-api-key.tsx | 8 +- web/app/(app)/dashboard/layout.tsx | 21 +++++- web/components/shared/app-header.tsx | 2 +- web/components/shared/footer.tsx | 5 +- web/e2e/account.spec.ts | 47 ++++++++++++ 10 files changed, 183 insertions(+), 42 deletions(-) diff --git a/web/app/(app)/dashboard/(components)/api-keys.tsx b/web/app/(app)/dashboard/(components)/api-keys.tsx index 117cf1b..687fae8 100644 --- a/web/app/(app)/dashboard/(components)/api-keys.tsx +++ b/web/app/(app)/dashboard/(components)/api-keys.tsx @@ -236,7 +236,12 @@ export default function ApiKeys() {
- diff --git a/web/app/(app)/dashboard/(components)/black-friday-modal.tsx b/web/app/(app)/dashboard/(components)/black-friday-modal.tsx index 2b7b51f..4a41dc2 100644 --- a/web/app/(app)/dashboard/(components)/black-friday-modal.tsx +++ b/web/app/(app)/dashboard/(components)/black-friday-modal.tsx @@ -117,7 +117,7 @@ export default function BlackFridayModal() {
BLACKFRIDAY40 -
@@ -139,7 +139,7 @@ export default function BlackFridayModal() {
BLACKFRIDAY50 -
diff --git a/web/app/(app)/dashboard/(components)/change-password-form.tsx b/web/app/(app)/dashboard/(components)/change-password-form.tsx index 599f821..277b9e7 100644 --- a/web/app/(app)/dashboard/(components)/change-password-form.tsx +++ b/web/app/(app)/dashboard/(components)/change-password-form.tsx @@ -20,9 +20,11 @@ const changePasswordSchema = z newPassword: z .string() .min(8, { message: 'Password must be at least 8 characters long' }), + // Matches newPassword's floor. At 4 a five-character confirmation failed + // with "Passwords must match" instead of the real length problem. confirmPassword: z .string() - .min(4, { message: 'Please confirm your password' }), + .min(1, { message: 'Please confirm your password' }), }) .superRefine((data, ctx) => { if (data.newPassword !== data.confirmPassword) { @@ -84,14 +86,30 @@ export default function ChangePasswordForm() { >
+ {/* Without autoComplete, password managers cannot tell these three + boxes apart and fill the saved password into the wrong one. */} {changePasswordForm.formState.errors.oldPassword && ( -

+

)} @@ -102,11 +120,25 @@ export default function ChangePasswordForm() { {changePasswordForm.formState.errors.newPassword && ( -

+

)} @@ -117,11 +149,25 @@ export default function ChangePasswordForm() { {changePasswordForm.formState.errors.confirmPassword && ( -

+

)} diff --git a/web/app/(app)/dashboard/(components)/delete-account-form.tsx b/web/app/(app)/dashboard/(components)/delete-account-form.tsx index 04467bc..c43aaf9 100644 --- a/web/app/(app)/dashboard/(components)/delete-account-form.tsx +++ b/web/app/(app)/dashboard/(components)/delete-account-form.tsx @@ -120,60 +120,77 @@ export default function DeleteAccountForm() { Delete Account + {/* Description holds prose only. The form controls used to live + inside it, and Radix points the dialog's aria-describedby here, + so opening the dialog read the labels, placeholders and all + back as one long description string. */}

Are you sure you want to delete your account? This action:

-
    +
    • Cannot be undone
    • Will permanently delete all your data
    • Will cancel all active subscriptions
    • Will remove access to all services
    + + +
    +
    + {/* htmlFor pointed at an id this field never had, so the label + was decorative and the textarea was announced unlabelled. */}