polish: field-level form errors, announced states, and a way back from legal pages - #227
Conversation
… back Six small fixes that each left the page looking right and behaving wrongly: - Contact form validates per field. The panel answered every failure with all three rules; now each field gets aria-invalid and its own message underneath, the summary lists only what failed, and editing a field withdraws its error. The form is noValidate so the browser's off-brand bubbles no longer pre-empt this; the handler applies the Worker's email regex itself. - Copying the email address fills a role=status live region. The button's label changed but a name change on the focused control isn't announced. - Active nav item carries aria-current="location" (desktop and mobile sheet). - Hero experience stat is computed for the first render, not in an effect, so the stat strip no longer shows "—" for a frame on every load. - /privacy and /terms use min-h-dvh and gain a "Back to site" link. - Nav and footer external links say "(opens in a new tab)", as the hero's résumé button already did. Co-authored-by: Cursor <cursoragent@cursor.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
aswin-portfolio | a5b3cce | Commit Preview URL Branch Preview URL |
Sep 03 2026, 06:53 AM |
Aswincloud-Bot
left a comment
There was a problem hiding this comment.
Auto-approved: @Aswinmcw is a member of @Aswincloud/admins.
There was a problem hiding this comment.
🟡 Changes recommended
There are confirmed correctness issues in the updated state management (stale closure updates) and experience calculation (timezone-dependent month computation) that can produce incorrect UI behavior.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR polishes several UI/UX and accessibility behaviors across the site, primarily improving form validation feedback, assistive-technology announcements, and navigation semantics, plus small layout fixes for legal pages.
Changes:
- Adds field-level contact form validation with per-field
aria-invalid/aria-describedby, dynamic error withdrawal on edit, and a live region announcement for “copy email”. - Improves navigation/link accessibility via
aria-current="location"for active sections and “(opens in a new tab)” labeling for external links. - Removes first-render flicker for the hero “experience” stat by computing the hook value synchronously, and updates legal pages to use
min-h-dvhplus a “Back to site” link.
File summaries
| File | Description |
|---|---|
| src/hooks/useExperienceCalculator.js | Synchronous initial experience value + daily refresh; refactor into a formatting helper. |
| src/components/TermsConditions.jsx | Adds “Back to site” link and switches min-h-screen → min-h-dvh. |
| src/components/sections/Footer.jsx | Adds “opens in a new tab” context to link aria-labels. |
| src/components/sections/ContactSection.jsx | Implements per-field validation/errors, error summary narrowing, noValidate, and copy-email live region. |
| src/components/PrivacyPolicy.jsx | Adds “Back to site” link and switches min-h-screen → min-h-dvh. |
| src/components/Navigation.jsx | Adds aria-current="location" for active nav items; adds “opens in a new tab” labels for résumé links. |
| src/tests/uiAffordances.test.jsx | Adds tests for aria-current, copy-email live region, and field-level validation behaviors. |
| src/tests/hooks.test.js | Adds test ensuring experience hook has a non-empty first-render value. |
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const handleChange = e => { | ||
| setFormData({ ...formData, [e.target.name]: e.target.value }); | ||
| const { name, value } = e.target; | ||
| setFormData({ ...formData, [name]: value }); | ||
|
|
||
| // Editing a field withdraws its error rather than re-validating on every |
| const formatExperience = (now = new Date()) => { | ||
| const startDate = new Date(EXPERIENCE_START); | ||
|
|
||
| const diffInMonths = | ||
| (currentDate.getFullYear() - startDate.getFullYear()) * 12 + | ||
| (currentDate.getMonth() - startDate.getMonth()); | ||
| const diffInMonths = | ||
| (now.getFullYear() - startDate.getFullYear()) * 12 + (now.getMonth() - startDate.getMonth()); |
| useEffect(() => { | ||
| // Keep it current across a tab left open past midnight on the 1st. | ||
| const interval = setInterval(() => setExperience(formatExperience()), 24 * 60 * 60 * 1000); | ||
| return () => clearInterval(interval); | ||
| }, []); |
Summary
Six small UI/UX and accessibility fixes from the review, each of which rendered fine and behaved wrongly:
aria-invalid, a red border, and its own message underneath; the summary panel lists only the fields that failed; editing a field withdraws its error and the panel disappears once the last one is fixed. The form isnoValidateso the browser's grey default bubbles no longer pre-empt this — the handler applies the Worker's email regex itself, so nothing that passes here can 400. NativeminLength/maxLengthattributes stay (maxLength still caps typing).role="status"region now carries the confirmation for the two seconds the icon does.aria-current="location"on desktop and in the mobile sheet; it was colour-and-pill only.useExperienceCalculatorstarted at''and filled in from an effect, so the stat strip showed "—" for a frame on every load. It's now auseStateinitialiser; the daily interval remains for tabs left open across a month boundary.min-h-screen→min-h-dvh(the background fell short of the viewport bottom on phones once the toolbar collapsed) and a "← Back to site" link above the heading — the wordmark went home but nothing on the page said so.labelInName.spec.jsstays green.Not done: the 2× logos item from the review was wrong —
imageAssets.test.jsalready sizes them at 96px for a 48px box, which is 2×.Test plan
npm run lint,npm run format:checknpm run test:coverage— 373 passing (was 366); new tests coveraria-currentafter a nav click, the copy live region, field-levelaria-invalid/aria-describedby, error withdrawal on edit, the email regex path, and the hook's first-render valuenpx playwright test— 37 passing, including the WCAG 2.5.3 label-in-name sweep at both widths and the 400/502 contact pathsaria-current="location"on Projects after click; status region reads "Email address copied to clipboard" (with clipboard permission);/privacyback link lands on/at scroll 0Made with Cursor