Skip to content

polish: field-level form errors, announced states, and a way back from legal pages - #227

Merged
Aswinmcw merged 1 commit into
mainfrom
polish/a11y-and-consistency
Sep 3, 2026
Merged

polish: field-level form errors, announced states, and a way back from legal pages#227
Aswinmcw merged 1 commit into
mainfrom
polish/a11y-and-consistency

Conversation

@Aswinmcw

@Aswinmcw Aswinmcw commented Sep 3, 2026

Copy link
Copy Markdown
Member

Summary

Six small UI/UX and accessibility fixes from the review, each of which rendered fine and behaved wrongly:

  • Contact form validates per field. The error panel used to recite all three rules for any failure ("Name: 2–100, Email: valid, Message: 10–1000") and left the visitor to guess which one they'd broken. Each field now gets 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 is noValidate so 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. Native minLength/maxLength attributes stay (maxLength still caps typing).
  • Copy-email is announced. The button's label already switched to "Email copied", but a name change on the control you just pressed isn't read out. An always-present role="status" region now carries the confirmation for the two seconds the icon does.
  • Active nav item has aria-current="location" on desktop and in the mobile sheet; it was colour-and-pill only.
  • Hero experience stat computed synchronously. useExperienceCalculator started at '' and filled in from an effect, so the stat strip showed "—" for a frame on every load. It's now a useState initialiser; the daily interval remains for tabs left open across a month boundary.
  • Legal pages: min-h-screenmin-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.
  • "(opens in a new tab)" on the nav Résumé link (both variants) and the footer's Source Code, Résumé, GitHub and LinkedIn links, matching the hero's résumé button. Labels still contain the visible text, so labelInName.spec.js stays green.

Not done: the 2× logos item from the review was wrong — imageAssets.test.js already sizes them at 96px for a 48px box, which is 2×.

Test plan

  • npm run lint, npm run format:check
  • npm run test:coverage — 373 passing (was 366); new tests cover aria-current after a nav click, the copy live region, field-level aria-invalid/aria-describedby, error withdrawal on edit, the email regex path, and the hook's first-render value
  • npx playwright test — 37 passing, including the WCAG 2.5.3 label-in-name sweep at both widths and the 400/502 contact paths
  • Verified in headless Chromium: three invalid fields flagged individually, summary lists three items, editing name drops it to two; aria-current="location" on Projects after click; status region reads "Email address copied to clipboard" (with clipboard permission); /privacy back link lands on / at scroll 0

Made with Cursor

… 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>
Copilot AI lite review requested due to automatic review settings September 3, 2026 06:53
@Aswinmcw
Aswinmcw requested review from a team and Aswin-coder as code owners September 3, 2026 06:53
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 3, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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 Aswincloud-Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Auto-approved: @Aswinmcw is a member of @Aswincloud/admins.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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-dvh plus 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-screenmin-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-screenmin-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.

Comment on lines 209 to +213
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
Comment on lines +17 to +21
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());
Comment on lines +38 to 42
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);
}, []);
Merged via the queue into main with commit b95f2a2 Sep 3, 2026
15 checks passed
@Aswinmcw
Aswinmcw deleted the polish/a11y-and-consistency branch September 3, 2026 06:58
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