From d80402a8cac851a7eff4ab0b64d70709cde0a743 Mon Sep 17 00:00:00 2001 From: Mohamed Abdallah <30629771+MohamedAbdallah-14@users.noreply.github.com> Date: Wed, 6 May 2026 15:09:30 +0300 Subject: [PATCH] craft: add form-validation so generated forms aren't stuck in 2018 RHF/Formik patterns (#625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(craft): add form-validation + opt-ins on saas-landing, mobile-onboarding Module 5 of 5 in the behavioral craft series proposed in #501. Modules 1-4 merged: state-coverage (#502), animation-discipline (#515), accessibility-baseline (#587), rtl-and-bidi (#595). Picks up where accessibility-baseline.md ends (label + describedby + invalid + role=alert for inline errors) and connects the four layers a real form spans: WHATWG Constraint Validation as the platform floor, validation timing as a state machine on the input, WCAG 3.3.x as the announcement and recovery contract, schema as the cross-stack truth. Sections: input state machine; validation timing (4 rules anchored on :user-invalid Baseline 2023); Constraint Validation API rules (setCustomValidity, requestSubmit vs submit, readonly + #11841, inputmode); error wiring beyond the baseline (adaptive messages, error summary without role=alert, preserve user input on error); schema as cross-stack contract (Standard Schema, server-authoritative, Zod 4 z.email() form); WCAG 3.3.3 / 3.3.4 / 3.3.8 / 3.3.9; native mobile parity (UIKit, SwiftUI, Compose, Flutter, RN); common mistakes. Reviewed in 3 loops with Claude CLI Opus 4.7 xhigh effort: - Loop 1: 6 P0s caught (SwiftUI Form validity claim, SwiftUI announcement primitive, Compose semantics syntax, UIKit UIAlertController, contradictory Baymard stats, 3.3.8 CAPTCHA framing reversed) + 11 P1/P2s; all addressed. - Loop 2: verified P0 fixes; flagged 1 P1 (RN table row scrambled) + 4 P2s; all addressed. - Loop 3: SHIP verdict. Three P2 nits applied (Zod 4 z.email() form, WebAIM Million 2026 stat woven in: 51% page-level, 33.1% input-level). WebAIM Million 2026 numbers verified directly against webaim.org/projects/million/. Skill opt-ins: saas-landing (lead capture form), mobile-onboarding (sign-in screen). Skill bodies do not contain validation-specific instructions that would override craft guidance — opt-in alone is sufficient. README updated. Refs #501. * fix(craft+skills): form-validation review fixes (lefarcen + mrcfps P2s) Both non-blocking findings addressed: - Drop form-validation from saas-landing.craft.requires. The skill body produces a CTA-driven landing page with no JS and no interactive form. Adding form-validation injected ~221 lines of irrelevant prompt pressure and conflicted with the README opt-in rule ("primary artifact contains an interactive form"). mobile-onboarding keeps the opt-in — sign-in screen is a real form. - Reword timing rule 4 (async checks). Previous "never block submit on a network round-trip" was too broad and conflicted with the schema-layer "server is the truth" rule. Split into two paths: background preflight (uniqueness, address lookup) doesn't gate the form; authoritative submit-path server validation must await the server response and surface its field errors. The rule is "don't let a slow background check freeze the form," not "don't ever wait for the server." * fix(craft): form-validation mrcfps round-2 (novalidate trade-off, Flutter RTL) Two non-blocking precision items: - novalidate trade-off: previous wording said keeping required/pattern/type preserves no-JS PE, but a literal server-rendered
disables the browser's submit-blocking and validation UI even when JS is unavailable — losing the no-JS constraint-validation floor. Reworded to spell out the two safe patterns: (A) render without novalidate server-side and have the form library set form.noValidate = true after hydration, or (B) ship novalidate from the start only when the submit path reaches server validation without JS. Either way, keep the constraint attributes. - Flutter announcement example: hardcoded TextDirection.ltr would announce Arabic/Hebrew/Persian validation messages with wrong bidi direction when this craft is combined with rtl-and-bidi. Switched to SemanticsService.announce(message, Directionality.of(context)) with an explicit warning never to hardcode the direction. * fix(craft): form-validation mrcfps round-3 (readonly safety, Compose error message) Two non-blocking precision items: - Non-input readonly fallback: previous text said `aria-readonly` plus hidden mirror input was an option for non-input controls that need to submit. But `aria-readonly` doesn't actually stop a ` +``` + +Use these declaratively for every field that has them: `required`, +`type` (email, url, number, tel), `pattern`, `min`/`max`, +`minlength`/`maxlength`, `step`. Cross-field rules and dynamic +constraints go through `setCustomValidity()` on both `input` and +`change` events — autofill flows historically fired one without the +other on some browsers, so listening on both is the cheap defense. + +Rules of the API: + +- **Empty string clears `setCustomValidity`.** Not `null`, not no-arg. +- **`form.requestSubmit()` honors validation; `form.submit()` skips it.** Never call the second. +- `disabled` controls are barred from validation and not submitted. The HTML spec says `readonly` is also barred, but `readonly` only has defined behavior on `` and `