Phase 3: add themed specs (frontend, SEO, lead capture)
This commit is contained in:
680
specification/38-lead-capture-spec.md
Normal file
680
specification/38-lead-capture-spec.md
Normal file
@@ -0,0 +1,680 @@
|
||||
# 38 — Lead Capture Specification
|
||||
## HER — Home Enhancement and Renovation
|
||||
|
||||
**Status**: Phase 3 — implementation-ready
|
||||
**Implements**: FR-1, FR-2, FR-3, FR-4, FR-5, FR-6, FR-7 | AC-1, AC-6 | ADR-002
|
||||
**Authority**: This document is the authoritative spec for all form field definitions, validation rules, error states, spam protection, and API integration. `33-frontend-spec.md` §5.6 governs page layout and visual styling; where the two documents overlap, this document takes precedence for behavior and copy.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Form Fields](#1-form-fields)
|
||||
2. [Validation Rules](#2-validation-rules)
|
||||
3. [Form UX States](#3-form-ux-states)
|
||||
4. [UX Copy](#4-ux-copy)
|
||||
5. [AWS SES Integration](#5-aws-ses-integration)
|
||||
6. [Spam Protection](#6-spam-protection)
|
||||
7. [Accessibility](#7-accessibility)
|
||||
8. [Mobile Considerations](#8-mobile-considerations)
|
||||
9. [Thank-You / Confirmation](#9-thank-you--confirmation)
|
||||
10. [Requirements Traceability](#10-requirements-traceability)
|
||||
|
||||
---
|
||||
|
||||
## 1. Form Fields
|
||||
|
||||
### 1.1 Field Inventory
|
||||
|
||||
The form collects five visible required fields plus one hidden honeypot field. All visible fields use the `FormField` component (see `33-frontend-spec.md` §6.7).
|
||||
|
||||
| # | Field name | Label | HTML type | `name` attr | `id` attr | `autocomplete` | Required |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 1 | Full Name | `Full name` | `text` | `name` | `field-name` | `name` | Yes |
|
||||
| 2 | Service Address | `Service address` | `text` | `address` | `field-address` | `off` | Yes |
|
||||
| 3 | Phone | `Phone number` | `tel` | `phone` | `field-phone` | `tel` | Yes |
|
||||
| 4 | Email | `Email address` | `email` | `email` | `field-email` | `email` | Yes |
|
||||
| 5 | Project Description | `Describe the work` | `textarea` | `description` | `field-description` | `off` | Yes |
|
||||
| — | Honeypot | *(none — hidden)* | `text` | `website` | `field-website` | `off` | No |
|
||||
|
||||
> **Note on `autocomplete="off"` for address**: Browser autofill for `street-address` fills a billing/home address, which may differ from the *service location*. Using `off` prevents misplaced autofill. This is intentional.
|
||||
|
||||
### 1.2 Field Details
|
||||
|
||||
#### Full Name
|
||||
- **Label**: `Full name`
|
||||
- **Placeholder**: `e.g. Jane Smith`
|
||||
- **Help text**: *(none)*
|
||||
|
||||
#### Service Address
|
||||
- **Label**: `Service address`
|
||||
- **Placeholder**: `e.g. 123 Market St, San Francisco, CA`
|
||||
- **Help text** (visible below label, above input): `"Where is the work located? City or full address."`
|
||||
- **Style**: Help text rendered as `<p class="field-hint">` in DM Sans 400, `--text-sm`, `--col-slate`. Sits between label and input, `--space-1` margin below.
|
||||
|
||||
#### Phone Number
|
||||
- **Label**: `Phone number`
|
||||
- **Placeholder**: `e.g. (415) 555-0123`
|
||||
- **Help text**: *(none)*
|
||||
- **Input mode**: `inputmode="tel"`
|
||||
|
||||
#### Email Address
|
||||
- **Label**: `Email address`
|
||||
- **Placeholder**: `e.g. jane@example.com`
|
||||
- **Help text**: *(none)*
|
||||
- **Input mode**: `inputmode="email"`
|
||||
|
||||
#### Project Description
|
||||
- **Label**: `Describe the work`
|
||||
- **Placeholder**: `What needs to be fixed, replaced, or improved? Include any relevant details — size, urgency, or special requirements.`
|
||||
- **Help text**: *(none; placeholder is the primary guide)*
|
||||
- **Element**: `<textarea>`, resizable vertically only (`resize: vertical`)
|
||||
- **Min height**: 120px on mobile, 160px on desktop (`md` and above)
|
||||
|
||||
#### Honeypot (hidden)
|
||||
- **Label**: None — never rendered in the visible DOM
|
||||
- **Name / ID**: `name="website"`, `id="field-website"`
|
||||
- **Hidden via CSS** (see §6 for exact CSS; do not use `type="hidden"` — bots fill visible fields, not `type="hidden"` ones)
|
||||
- **Expected value at submission**: empty string
|
||||
- **Behavior if non-empty**: silently discard on client; Lambda also discards and returns `{ "ok": true }` (see §5)
|
||||
|
||||
### 1.3 Field Order
|
||||
|
||||
The canonical field order — used as DOM order, mobile stacking order, and keyboard tab order — is:
|
||||
|
||||
```
|
||||
Full Name → Service Address → Phone → Email → Project Description → [Submit]
|
||||
```
|
||||
|
||||
**Desktop layout note**: `33-frontend-spec.md` §5.6 specifies a two-column desktop layout where Name and Phone appear side-by-side in Row 1. This visual grouping is achieved with CSS Grid `order` properties, *not* by reordering the DOM. The DOM order defined above must be preserved for correct tab/focus sequence.
|
||||
|
||||
Desktop CSS grid layout (at `md` breakpoint and above):
|
||||
|
||||
```
|
||||
[ Full Name (50%) ] [ Phone (50%) ] ← visual row 1 (CSS order: Name=1, Phone=3)
|
||||
[ Service Address (100%) ] ← visual row 2 (CSS order: Address=2)
|
||||
[ Email Address (100%) ] ← visual row 3 (CSS order: Email=4)
|
||||
[ Project Description (100%) ] ← visual row 4 (CSS order: Description=5)
|
||||
[ Submit → ] ← visual row 5
|
||||
```
|
||||
|
||||
DOM order remains: Name, Address, Phone, Email, Description — so keyboard navigation flows logically even with visual rearrangement.
|
||||
|
||||
### 1.4 Submit Button
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| Label | `Send My Request` |
|
||||
| Component | `Button` (primary variant — see `33-frontend-spec.md` §6.1) |
|
||||
| Mobile width | Full-width (100%) |
|
||||
| Desktop width | Auto, min-width 200px, right-aligned within form container |
|
||||
| Disabled state | While submitting: `aria-disabled="true"`, shows spinner (see §3) |
|
||||
|
||||
---
|
||||
|
||||
## 2. Validation Rules
|
||||
|
||||
### 2.1 Validation Strategy
|
||||
|
||||
- **Client-side**: HTML5 native attributes (`required`, `minlength`, `maxlength`, `pattern`) plus JavaScript for on-blur validation and submit-time validation. Do not rely on browser default validation UI — suppress with `novalidate` on the `<form>` element and handle all error rendering in JavaScript.
|
||||
- **Server-side (Lambda)**: Re-validate all fields independently. Never trust client-only validation.
|
||||
- **Error display**: Inline error messages rendered below each field, inside a `<p role="alert">` element that is always present in the DOM (even when empty). Field errors appear on blur (after first touch) and on submit attempt.
|
||||
|
||||
### 2.2 Per-Field Validation Rules
|
||||
|
||||
| Field | Required | Min length | Max length | Pattern | Error message |
|
||||
|---|---|---|---|---|---|
|
||||
| Full Name | Yes | 2 | 100 | *(none beyond length)* | `"Please enter your name."` |
|
||||
| Service Address | Yes | 3 | 200 | *(none beyond length)* | `"Please enter the service address or city."` |
|
||||
| Phone | Yes | — | — | US phone (see §2.3) | `"Please enter a valid phone number."` |
|
||||
| Email | Yes | — | — | Standard email (see §2.3) | `"Please enter a valid email address."` |
|
||||
| Project Description | Yes | 10 | 2000 | *(none beyond length)* | `"Please describe your project (at least 10 characters)."` |
|
||||
| Honeypot | No | — | — | Must be empty | *(no error shown — silent discard)* |
|
||||
|
||||
### 2.3 Pattern Definitions
|
||||
|
||||
**Phone** (`name="phone"`):
|
||||
```
|
||||
^\+?[\d\s\-().]{7,20}$
|
||||
```
|
||||
Allows: digits, spaces, hyphens, parentheses, plus sign. Minimum 7 characters (shortest valid phone), maximum 20. Accepts US and international formats:
|
||||
- `(415) 555-0123`
|
||||
- `415-555-0123`
|
||||
- `4155550123`
|
||||
- `+1 415 555 0123`
|
||||
|
||||
**Email** (`name="email"`):
|
||||
Use the `type="email"` browser built-in pattern, supplemented by JS check that the value contains at least one `@` and at least one `.` after the `@`. Do not write an overly strict regex — the AWS SES delivery failure is the ultimate validator for undeliverable addresses.
|
||||
|
||||
### 2.4 Validation Timing
|
||||
|
||||
| Trigger | Behavior |
|
||||
|---|---|
|
||||
| First focus (untouched field) | No validation; field is in default state |
|
||||
| Blur (after first touch) | Validate field; show error if invalid |
|
||||
| Input (after field has been touched and errored) | Re-validate on each keystroke; clear error as soon as valid |
|
||||
| Submit button click | Validate all fields; show all errors simultaneously; move focus to first invalid field |
|
||||
| Successful submission | No field validation (form is replaced or disabled) |
|
||||
|
||||
### 2.5 Character Counter (Description Field)
|
||||
|
||||
Display a live character counter below the `description` textarea showing `N / 2000 characters`. Counter updates on every keystroke.
|
||||
|
||||
- **Style**: DM Sans 400, `--text-xs`, `--col-slate`, right-aligned below textarea.
|
||||
- **Warning state** (≥ 1800 characters): color changes to `#B5622A` (`--col-copper`).
|
||||
- **Over-limit state** (> 2000 characters): color `#C0392B` (error red), counter reads `2000 / 2000` — the `maxlength` HTML attribute enforces the hard cap and prevents additional input.
|
||||
|
||||
---
|
||||
|
||||
## 3. Form UX States
|
||||
|
||||
The form operates as a client-side state machine with six distinct states.
|
||||
|
||||
### 3.1 State Definitions
|
||||
|
||||
#### Default
|
||||
- All fields empty, no errors shown.
|
||||
- Submit button enabled, label `"Send My Request"`.
|
||||
- Character counter shows `0 / 2000`.
|
||||
- Form element: `aria-busy="false"`.
|
||||
|
||||
#### Touched / Dirty (Per-Field)
|
||||
- Validation runs on blur for each field independently.
|
||||
- If invalid: inline error message appears below the field (`role="alert"`), field border changes to error red (`#C0392B`), `aria-invalid="true"` set on the input.
|
||||
- If valid: error message cleared, border returns to default (`--col-chalk`), `aria-invalid="false"`.
|
||||
- Other fields are unaffected.
|
||||
|
||||
#### Submitting
|
||||
- Triggered when user clicks submit and all fields pass client-side validation.
|
||||
- Submit button: spinner replaces button text, `aria-disabled="true"`, visually disabled (opacity 0.7, `cursor: not-allowed`). Do not use `disabled` attribute — this removes the element from tab order.
|
||||
- All form fields: `disabled` attribute added (prevents editing during in-flight request).
|
||||
- Form element: `aria-busy="true"`.
|
||||
- No duplicate submissions possible while in this state.
|
||||
- Spinner: 20×20px animated SVG ring, `--col-linen` color on copper button background.
|
||||
|
||||
#### Success
|
||||
- Triggered on HTTP 200 response with `{ "ok": true }`.
|
||||
- The form element is hidden (`display: none` or removed from DOM).
|
||||
- The `FormFeedback (success)` component is rendered in its place (see §4.1 for exact copy).
|
||||
- Focus is moved programmatically to the success message heading (`<h2>` or `<h3>` within the feedback block).
|
||||
- The success component has `role="status"` and `aria-live="polite"` (see §7).
|
||||
|
||||
#### Error — Network / Server
|
||||
- Triggered on: HTTP non-200 response, network failure, `{ "ok": false }` in response body, or unhandled exception.
|
||||
- Form fields are re-enabled (remove `disabled`).
|
||||
- Submit button returns to normal state.
|
||||
- Form element: `aria-busy="false"`.
|
||||
- `FormFeedback (error)` banner is rendered above the submit button (see §4.2 for exact copy). The form itself remains visible and filled — user does not lose their input.
|
||||
- Focus is moved programmatically to the error banner heading.
|
||||
- The error banner has `role="alert"` and `aria-live="assertive"` (see §7).
|
||||
|
||||
#### Partial Error — Field-Level from Server
|
||||
- Triggered when Lambda returns `{ "ok": false, "error": "..." }` with a field-level message (e.g., a field failed server-side validation).
|
||||
- The specific field is highlighted with error styling and `aria-invalid="true"`.
|
||||
- The field-level error message is rendered in that field's error slot.
|
||||
- The network/server error banner is also shown (degraded trust signal).
|
||||
- Focus is moved to the first affected field.
|
||||
|
||||
### 3.2 State Transition Diagram
|
||||
|
||||
```
|
||||
Default
|
||||
│
|
||||
├─(user types/tabs)──→ Touched/Dirty (per field)
|
||||
│
|
||||
├─(submit, valid)────→ Submitting
|
||||
│ │
|
||||
│ ├─(200, ok:true)──→ Success [terminal]
|
||||
│ │
|
||||
│ └─(error)─────────→ Error (Network/Server)
|
||||
│ │
|
||||
│ └─(user edits & resubmits)──→ Submitting
|
||||
│
|
||||
└─(submit, invalid)──→ Touched/Dirty (all fields) + focus → first error
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. UX Copy
|
||||
|
||||
All copy is final and must be used verbatim. Placeholders in brackets (`[phone]`, `[email]`) must be replaced with Diana's actual contact details at implementation.
|
||||
|
||||
### 4.1 Page Intro Lead
|
||||
|
||||
Rendered as the lead paragraph (`<p>`) in the `/contact/` page hero, below the H1. (See `33-frontend-spec.md` §5.6 Section 2.)
|
||||
|
||||
> "Tell Diana about your project. She responds within 1–2 business days."
|
||||
|
||||
Styling: DM Sans 400, `--text-lg`, `--col-ink`.
|
||||
|
||||
### 4.2 Success Message (FormFeedback — success variant)
|
||||
|
||||
Shown in place of the form after a successful submission. Component: `FormFeedback (success)` (`33-frontend-spec.md` §6.8).
|
||||
|
||||
**Heading**:
|
||||
> "Your request is on its way!"
|
||||
|
||||
**Body**:
|
||||
> "Diana will review your request and get back to you within 1–2 business days. If your project is urgent, you can also reach her directly at [phone] or [email]."
|
||||
|
||||
- `[phone]` renders as a `<a href="tel:[phone]">[phone]</a>` link.
|
||||
- `[email]` renders as a `<a href="mailto:[email]">[email]</a>` link.
|
||||
- Both links use `--col-copper` color, `text-decoration: underline`.
|
||||
|
||||
Implementation note — contact detail placeholders:
|
||||
- Phone: `(415) XXX-XXXX` (replace at implementation — see §5.6 §4 "Secondary Contact Info" in `33-frontend-spec.md`)
|
||||
- Email: `diana@example.com` (replace at implementation)
|
||||
|
||||
### 4.3 Network / Server Error Message (FormFeedback — error variant)
|
||||
|
||||
Shown as a banner above the submit button while the form remains visible. Component: `FormFeedback (error)` (`33-frontend-spec.md` §6.8).
|
||||
|
||||
**Heading**:
|
||||
> "Something went wrong."
|
||||
|
||||
**Body**:
|
||||
> "We couldn't send your request. Please try again, or contact Diana directly at [phone] or [email]."
|
||||
|
||||
- Same link treatment as §4.2.
|
||||
- Form fields remain filled and editable.
|
||||
|
||||
### 4.4 Field-Level Error Copy (reference)
|
||||
|
||||
See §2.2 for the authoritative error message text for each field. Reproduced here for convenience:
|
||||
|
||||
| Field | Error message |
|
||||
|---|---|
|
||||
| Full Name | `"Please enter your name."` |
|
||||
| Service Address | `"Please enter the service address or city."` |
|
||||
| Phone | `"Please enter a valid phone number."` |
|
||||
| Email | `"Please enter a valid email address."` |
|
||||
| Description | `"Please describe your project (at least 10 characters)."` |
|
||||
|
||||
---
|
||||
|
||||
## 5. AWS SES Integration
|
||||
|
||||
Implements ADR-002. Resolves OQ-3 (endpoint URL and recipient email are deferred to implementation).
|
||||
|
||||
### 5.1 Endpoint
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| URL | TBD — provided at implementation (see ADR-002, OQ-3) |
|
||||
| Method | `POST` |
|
||||
| Content-Type | `application/json` |
|
||||
| Auth | None (public endpoint) |
|
||||
| CORS | API Gateway must allow the production site origin (e.g. `https://her-home.com`) |
|
||||
|
||||
Store the endpoint URL as a build-time environment variable or a configuration constant (e.g. `FORM_ENDPOINT_URL`). Do not hardcode the URL in form markup.
|
||||
|
||||
### 5.2 Request Payload
|
||||
|
||||
The client sends a single JSON object. All string values are trimmed of leading/trailing whitespace before sending.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "string",
|
||||
"address": "string",
|
||||
"phone": "string",
|
||||
"email": "string",
|
||||
"description": "string",
|
||||
"honeypot": "string"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `name` | string | Trimmed, non-empty |
|
||||
| `address` | string | Trimmed, non-empty |
|
||||
| `phone` | string | Trimmed, non-empty |
|
||||
| `email` | string | Trimmed, lowercase, non-empty |
|
||||
| `description` | string | Trimmed, non-empty, max 2000 chars |
|
||||
| `honeypot` | string | Always sent; value is `""` for legitimate submissions |
|
||||
|
||||
### 5.3 Lambda Responsibilities
|
||||
|
||||
The Lambda function is the authoritative gatekeeper and must enforce all rules independently of client validation.
|
||||
|
||||
**Processing order**:
|
||||
|
||||
1. **Honeypot check**: If `honeypot` field is a non-empty string, log the discarded submission (for monitoring), return HTTP 200 `{ "ok": true }`. Do not signal to the client that the submission was discarded.
|
||||
|
||||
2. **Server-side field validation**: Verify all five required fields (`name`, `address`, `phone`, `email`, `description`) are non-empty strings after trimming. If any fail, return HTTP 200 `{ "ok": false, "error": "Missing required fields." }`.
|
||||
|
||||
3. **SES send**: Call AWS SES `SendEmail` with the formatted email (see §5.4).
|
||||
|
||||
4. **Success response**: On successful SES send, return HTTP 200 `{ "ok": true }`.
|
||||
|
||||
5. **SES error**: On SES failure, log the error (CloudWatch), return HTTP 200 `{ "ok": false, "error": "Failed to send email. Please try again." }`.
|
||||
|
||||
### 5.4 Email Formatting
|
||||
|
||||
**Recipient**: Diana's inbox (address configured in Lambda environment variable `SES_RECIPIENT` — set at implementation)
|
||||
**Sender**: A verified SES sender identity (e.g. `noreply@her-home.com`) — configured at implementation
|
||||
**Reply-To**: The submitting user's email address (so Diana can reply directly from her email client)
|
||||
|
||||
**Subject line**:
|
||||
```
|
||||
New Quote Request from [name]
|
||||
```
|
||||
|
||||
**Plain-text body**:
|
||||
```
|
||||
New quote request received via HER website.
|
||||
|
||||
Name: [name]
|
||||
Phone: [phone]
|
||||
Email: [email]
|
||||
Address: [address]
|
||||
|
||||
Project Description:
|
||||
[description]
|
||||
|
||||
---
|
||||
Submitted at: [ISO 8601 timestamp in UTC]
|
||||
```
|
||||
|
||||
**HTML email**: Optional enhancement — not required for v1. Plain-text body is sufficient.
|
||||
|
||||
### 5.5 Response Handling (Client)
|
||||
|
||||
| Scenario | Client action |
|
||||
|---|---|
|
||||
| `200 { "ok": true }` | Show success state (§3.1 Success) |
|
||||
| `200 { "ok": false, "error": "..." }` | Show error banner (§3.1 Error) |
|
||||
| `429 Too Many Requests` (AWS WAF rate limit) | Show error banner with copy: `"Too many requests. Please wait a moment and try again."` |
|
||||
| Network error (no response) | Show error banner |
|
||||
| Any non-200, non-429 HTTP status | Show error banner |
|
||||
|
||||
### 5.6 HTTP Status Code Policy
|
||||
|
||||
The Lambda **always returns HTTP 200** to the client, with success/failure detail in the JSON body. This policy:
|
||||
|
||||
- Prevents browser retry behavior triggered by 5xx responses.
|
||||
- Keeps error handling logic entirely in JavaScript, not in HTTP status handling.
|
||||
- Makes CORS error handling simpler.
|
||||
|
||||
**Exception**: AWS WAF may return `429 Too Many Requests` before the request reaches Lambda. The client must handle this status code explicitly (see §5.5).
|
||||
|
||||
### 5.7 Timeout Handling
|
||||
|
||||
- Client-side fetch timeout: **10 seconds**. If no response is received within 10 seconds, abort the request and show the error banner.
|
||||
- Implementation: use `AbortController` with a 10-second timeout signal passed to `fetch()`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Spam Protection
|
||||
|
||||
### 6.1 Honeypot Field (Primary)
|
||||
|
||||
The honeypot is the primary spam protection mechanism in v1. It requires no third-party service and adds no friction for real users.
|
||||
|
||||
**Field definition**:
|
||||
- `name="website"`, `id="field-website"`, `type="text"`
|
||||
- No `<label>` element rendered in the DOM.
|
||||
- Never autofocused or tab-accessible.
|
||||
|
||||
**CSS hiding** (applied via a utility class, e.g. `.visually-trap`):
|
||||
```css
|
||||
.visually-trap {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
tabindex: -1; /* set as HTML attribute, not CSS */
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
```
|
||||
|
||||
> **Do not use `display: none` or `visibility: hidden`** — many bots skip fields with these styles. CSS-based hiding that leaves the element in the normal document flow is more effective.
|
||||
|
||||
**Client-side behavior**: Before submitting, check `document.getElementById('field-website').value`. If non-empty, do not call the API — immediately transition to success state (silent discard).
|
||||
|
||||
**Server-side behavior**: Lambda checks `honeypot !== ""` → return `{ "ok": true }` without sending email.
|
||||
|
||||
### 6.2 AWS WAF Rate Limiting (Recommended)
|
||||
|
||||
Configure an AWS WAF Web ACL on the API Gateway with a basic rate-based rule:
|
||||
|
||||
| Parameter | Recommended value |
|
||||
|---|---|
|
||||
| Rule type | Rate-based |
|
||||
| Rate limit | 5 requests per IP per 5 minutes |
|
||||
| Action | Block (returns 429) |
|
||||
| Scope | API Gateway stage |
|
||||
|
||||
This is a **recommendation for implementation**, not a blocking requirement for this spec. The client must handle 429 responses gracefully (see §5.5).
|
||||
|
||||
### 6.3 No CAPTCHA (v1)
|
||||
|
||||
No CAPTCHA or challenge-based verification is used in v1, per FR-2 (form must be lightweight). If spam volume becomes a problem post-launch, the recommended escalation path is:
|
||||
|
||||
1. Tighten AWS WAF rate rules.
|
||||
2. Add Cloudflare Turnstile (invisible challenge, no user interaction required for most users).
|
||||
3. As a last resort, add hCaptcha or reCAPTCHA v3.
|
||||
|
||||
---
|
||||
|
||||
## 7. Accessibility
|
||||
|
||||
All form accessibility requirements conform to WCAG 2.1 AA. These requirements supplement `33-frontend-spec.md` §9.
|
||||
|
||||
### 7.1 Labels
|
||||
|
||||
- Every visible input and textarea has a corresponding `<label>` element.
|
||||
- Labels are associated via matching `for` (on label) and `id` (on input) attributes.
|
||||
- Labels are always visible — never hidden, never replaced by placeholder-only patterns.
|
||||
- Label text is DM Sans 500, `--text-sm`, `--col-graphite` (see `FormField` component).
|
||||
|
||||
### 7.2 Error Message Regions
|
||||
|
||||
- Each field has a sibling `<p>` element for its error message (e.g. `id="error-name"`), present in the DOM at all times.
|
||||
- When no error: element is empty (`""`) and has `aria-hidden="true"`.
|
||||
- When error present: element contains the error text, has `role="alert"`, and `aria-hidden` is removed (or set `"false"`).
|
||||
- Error messages are injected — not toggled with CSS `display` — to ensure screen reader announcement.
|
||||
|
||||
### 7.3 `aria-describedby` on Fields
|
||||
|
||||
Each input/textarea carries `aria-describedby` pointing to both the help text element (if present) and the error element, even when empty:
|
||||
|
||||
```html
|
||||
<input
|
||||
id="field-address"
|
||||
name="address"
|
||||
type="text"
|
||||
aria-describedby="hint-address error-address"
|
||||
aria-invalid="false"
|
||||
...
|
||||
/>
|
||||
<p id="hint-address" class="field-hint">Where is the work located? City or full address.</p>
|
||||
<p id="error-address" role="alert" aria-hidden="true"></p>
|
||||
```
|
||||
|
||||
### 7.4 `aria-invalid`
|
||||
|
||||
- Default state: `aria-invalid="false"` on all fields.
|
||||
- On validation failure: `aria-invalid="true"`.
|
||||
- On validation pass (after error): `aria-invalid="false"` restored.
|
||||
|
||||
### 7.5 Submitting State
|
||||
|
||||
- Form element: `aria-busy="true"` while request is in-flight.
|
||||
- Submit button: `aria-disabled="true"` (not `disabled` — button stays focusable and announces its state).
|
||||
- Button label updated to `"Sending…"` (visible text) while in submitting state.
|
||||
|
||||
### 7.6 Success State
|
||||
|
||||
- Success message container: `role="status"`, `aria-live="polite"`.
|
||||
- On render, focus is programmatically moved to the success heading element.
|
||||
- Success heading must be a heading element (`<h2>` or `<h3>`) — not a `<p>` or `<div>` — so it is discoverable by screen reader heading navigation.
|
||||
|
||||
### 7.7 Error Banner State
|
||||
|
||||
- Error banner container: `role="alert"`, `aria-live="assertive"`.
|
||||
- On render, focus is programmatically moved to the error banner heading element.
|
||||
- The banner must be in the DOM before the error occurs (with empty content), or be inserted immediately before focus is moved — do not rely on `aria-live` alone for focus; use explicit `focus()`.
|
||||
|
||||
### 7.8 Focus Management Summary
|
||||
|
||||
| State transition | Focus moves to |
|
||||
|---|---|
|
||||
| Submit with validation errors | First invalid field's `<input>` or `<textarea>` |
|
||||
| Submitting → Success | Success message heading |
|
||||
| Submitting → Error (network/server) | Error banner heading |
|
||||
| Submitting → Partial field error (server) | First errored field's `<input>` |
|
||||
|
||||
### 7.9 Keyboard Navigation
|
||||
|
||||
- Tab order follows DOM order: Name → Address → Phone → Email → Description → Submit.
|
||||
- All interactive elements are reachable via Tab and operable via Enter/Space.
|
||||
- No keyboard traps.
|
||||
- Submit button is reachable via Tab and activates on Enter.
|
||||
|
||||
---
|
||||
|
||||
## 8. Mobile Considerations
|
||||
|
||||
All requirements here supplement `33-frontend-spec.md` §3 (breakpoints) and ADR-003 (mobile-first).
|
||||
|
||||
### 8.1 Layout
|
||||
|
||||
- All fields are full-width (100%) on mobile (below `md` breakpoint).
|
||||
- Fields stack vertically in DOM order: Name → Address → Phone → Email → Description → Submit.
|
||||
- Submit button is full-width on mobile.
|
||||
- Gap between stacked fields: `--space-3` (24px).
|
||||
|
||||
### 8.2 `inputmode` Attributes
|
||||
|
||||
| Field | `inputmode` value | Effect |
|
||||
|---|---|---|
|
||||
| Phone | `tel` | Numeric keypad with `+`, `-`, `(`, `)` on iOS and Android |
|
||||
| Email | `email` | Keyboard with `@` and `.com` shortcut keys |
|
||||
| All others | *(not set — default)* | Standard text keyboard |
|
||||
|
||||
### 8.3 `autocomplete` Attributes
|
||||
|
||||
| Field | `name` attr | `autocomplete` attr |
|
||||
|---|---|---|
|
||||
| Full Name | `name` | `name` |
|
||||
| Phone | `phone` | `tel` |
|
||||
| Email | `email` | `email` |
|
||||
| Service Address | `address` | `off` |
|
||||
| Description | `description` | `off` |
|
||||
| Honeypot | `website` | `off` |
|
||||
|
||||
> **Address `autocomplete="off"` rationale**: Prevents browser/OS autofill from inserting the user's home or billing address into a field intended for the *service location*. A homeowner in Oakland requesting work at a rental in San Francisco must not have Oakland autofilled. See §1.2.
|
||||
|
||||
### 8.4 Textarea Sizing
|
||||
|
||||
| Breakpoint | Min height |
|
||||
|---|---|
|
||||
| Mobile (default) | 120px |
|
||||
| Desktop (`md` and above) | 160px |
|
||||
|
||||
Textarea is resizable vertically only (`resize: vertical`). Max height is not constrained (user may expand freely).
|
||||
|
||||
### 8.5 Touch Targets
|
||||
|
||||
- All inputs and the submit button have a minimum tap target height of 44px, per `33-frontend-spec.md` §9.3.
|
||||
- Submit button padding: at minimum `--space-2` (16px) top/bottom, `--space-4` (32px) left/right.
|
||||
- Field labels tap targets include the input below them (clicking label focuses input via `for`/`id` association).
|
||||
|
||||
### 8.6 Viewport and Keyboard Behavior
|
||||
|
||||
- The `<meta name="viewport" content="width=device-width, initial-scale=1">` tag must be present (standard; prevents double-tap zoom breaking on iOS).
|
||||
- On mobile, when a field is focused, the virtual keyboard pushes the viewport up. The form container must not use `overflow: hidden` in a way that clips the active field below the keyboard.
|
||||
- Do not use `position: fixed` on the form or its container on mobile.
|
||||
|
||||
---
|
||||
|
||||
## 9. Thank-You / Confirmation
|
||||
|
||||
### 9.1 Options
|
||||
|
||||
Two approaches are available. The choice is flagged as **Open Question OQ-7** (see below).
|
||||
|
||||
#### Option A — In-Page Success State (No Redirect)
|
||||
|
||||
After successful submission, the form is hidden and the `FormFeedback (success)` component renders in its place on `/contact/`. No navigation occurs.
|
||||
|
||||
**Pros**: Simpler implementation, no extra page to maintain, success copy is already on the page.
|
||||
**Cons**: If the user hits browser Back and then Forward, they may see the form in its original empty state. Duplicate submission via Back → Re-submit is prevented by client state (form resets to Success state on remount, or history state is used).
|
||||
|
||||
#### Option B — Redirect to `/thank-you/`
|
||||
|
||||
After `{ "ok": true }`, the client performs `window.location.href = '/thank-you/'`. The `/thank-you/` page renders the success message as a full page.
|
||||
|
||||
**Pros**: Eliminates back-button resubmission (form is gone from history); clean URL for conversion tracking if analytics are added later.
|
||||
**Cons**: Requires a separate HTML page; slightly more complex routing.
|
||||
|
||||
**`/thank-you/` page requirements** (if Option B is chosen):
|
||||
- Full standalone page with nav and footer.
|
||||
- H1: `"Your request is on its way!"`
|
||||
- Body copy: same as §4.2 success message.
|
||||
- `noindex` meta tag: `<meta name="robots" content="noindex, follow">`.
|
||||
- Included in `sitemap.xml` as `changefreq="never"` with no `<priority>` set — or excluded entirely if the sitemap is filtered to indexable pages only (see `37-seo-content-spec.md`).
|
||||
- No form on this page.
|
||||
|
||||
### 9.2 Open Question — OQ-7
|
||||
|
||||
> **OQ-7**: Should successful form submission stay on `/contact/` (Option A — in-page success state) or redirect to `/thank-you/` (Option B — separate page)?
|
||||
>
|
||||
> **Recommendation**: Option A for v1, given that there is no analytics in v1 (ADR-005) and the redirect's primary benefit (conversion tracking) does not apply. Revisit if analytics are added later.
|
||||
>
|
||||
> **Decision needed from**: Diana / project owner.
|
||||
> **Blocking**: Implementation of `38-lead-capture-spec.md`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Requirements Traceability
|
||||
|
||||
### 10.1 Functional Requirements
|
||||
|
||||
| Requirement | Description | How this spec satisfies it |
|
||||
|---|---|---|
|
||||
| **FR-1** | Quote form accessible within 1–2 taps from any page | Form lives at `/contact/`; nav CTA "Get a Quote" links there from all pages (persistent nav) |
|
||||
| **FR-2** | Form must be lightweight (no file uploads) | Five text fields only; no file input; no CAPTCHA in v1 (§6.3) |
|
||||
| **FR-3** | Form collects Name, Address, Phone, Email, Description | All five fields defined in §1.1 with types, labels, and validation |
|
||||
| **FR-4** | Clear confirmation and next-steps after submission | Success state (§3.1) and success copy (§4.2) specify exact heading + body with timeline and fallback contact |
|
||||
| **FR-5** | Submissions delivered via existing AWS service | API Gateway + Lambda + SES integration fully specified in §5 |
|
||||
| **FR-6** | Prominently expose form, email, and phone | Form is the primary channel; email and phone appear in success copy (§4.2) and error copy (§4.3) |
|
||||
| **FR-7** | Secondary channels (email, phone) in header/footer and post-submission | Phone and email are clickable links in both success and error messages (§4.2, §4.3); header/footer governed by `33-frontend-spec.md` |
|
||||
|
||||
### 10.2 Acceptance Criteria
|
||||
|
||||
| Criterion | How this spec satisfies it |
|
||||
|---|---|
|
||||
| **AC-1** | Visitor can reach the quote form in 1–2 taps from any page | Fulfilled by persistent nav CTA (FR-1 above); this spec defines what happens after the user arrives at `/contact/` |
|
||||
| **AC-6** | Secondary contact channels visible in header/footer and post-form confirmation | Success copy (§4.2) and error copy (§4.3) include phone and email as clickable links; requirement is met on form submission regardless of which page section is displayed |
|
||||
|
||||
### 10.3 Architecture Decision Records
|
||||
|
||||
| ADR | Decision | Relevance to this spec |
|
||||
|---|---|---|
|
||||
| **ADR-001** | Static website delivery | Form submission requires a dynamic backend; satisfied by API Gateway + Lambda (no server-side rendering required on the static site) |
|
||||
| **ADR-002** | Form backend: AWS SES via API Gateway + Lambda | Directly implemented in §5; endpoint URL and SES recipient deferred to implementation per ADR-002 status |
|
||||
| **ADR-003** | Mobile-first, performance-first design | §8 specifies mobile layout, `inputmode`, `autocomplete`, touch targets; no heavy JS frameworks; fetch API only |
|
||||
| **ADR-005** | No analytics in v1 | Influences §9 recommendation: in-page success state (Option A) preferred since redirect's conversion-tracking benefit doesn't apply without analytics |
|
||||
|
||||
### 10.4 Open Questions (from `99-open-questions.md`)
|
||||
|
||||
| OQ | Status | Impact on this spec |
|
||||
|---|---|---|
|
||||
| **OQ-3** | Resolved (endpoint URL deferred) | API endpoint URL is a build-time constant (§5.1); recipient email is a Lambda env var (§5.4) — both set at implementation |
|
||||
| **OQ-7** | New — raised by this spec | Thank-you page vs. in-page success (§9); must be decided before implementation |
|
||||
|
||||
### 10.5 Cross-Document References
|
||||
|
||||
| Document | Relationship |
|
||||
|---|---|
|
||||
| `33-frontend-spec.md` §5.6 | Defines `/contact/` page layout, hero copy, secondary contact section, and desktop two-column grid; this spec governs behavior and copy within the form |
|
||||
| `33-frontend-spec.md` §6.7 `FormField` | Component used by all visible fields |
|
||||
| `33-frontend-spec.md` §6.8 `FormFeedback` | Component used for success and error states |
|
||||
| `33-frontend-spec.md` §6.1 `Button` | Submit button component |
|
||||
| `33-frontend-spec.md` §9 Accessibility | Form accessibility requirements; §7 of this spec extends them |
|
||||
| `37-seo-content-spec.md` | `noindex` handling for `/thank-you/` if Option B is chosen |
|
||||
| `99-open-questions.md` | OQ-3 (resolved), OQ-7 (new — added by this spec) |
|
||||
Reference in New Issue
Block a user