Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 30 additions & 18 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,15 +335,24 @@ because it lives in core rather than in one of the two packages that need it.
#### Validation Behavior
- **The convention governs browser *constraint validation*, not accessibility annotations** (#199).
Those are different things, and conflating them is what left required fields silent to screen
readers. `Required()` still routes validation server-side and the browser still runs none — but a
required field must be *identified*, which is WCAG 2.1 3.3.2 (Level A)
- `Required()` therefore DOES set MudBlazor's `Required` on both render paths, which emits
`aria-required="true"`, the `*` asterisk, and the HTML5 `required` attribute. MudBlazor derives all
three from one flag and they cannot be separated: `MudInput` splats `UserAttributes` and then writes
its own `required`/`aria-required` afterwards, so a caller-supplied value is always overwritten
(measured on 9.8.0). The HTML5 attribute is inert here because the form renders `novalidate`.
⛔ Do not "restore the convention" by dropping this — that is #190, which #199 reversed, and it
reintroduces a Level A accessibility failure. `.WithNativeRequired(false)` is the per-field opt-out
readers. `Required()` routes validation server-side and the browser runs none — but a required
field must still be *identified*, which is WCAG 2.1 3.3.2 (Level A)
- **`Required()` adds validation but NOT the HTML5 `required` attribute** — the convention in its
literal form, restored by #263. It was amended between #199 and #263 only because MudBlazor 9.8.0
fused the two: `MudInput` splatted `UserAttributes` and then wrote its own `required` **and**
`aria-required` afterwards, both off one `Required` flag, so last-write-wins meant a caller could
never supply the annotation on its own. [MudBlazor#13613](https://github.com/MudBlazor/MudBlazor/pull/13613)
moved the ARIA writes *above* the splat and left `required` below it; shipped in **9.9.0**
- `Required()` therefore now contributes **`aria-required="true"` through `UserAttributes`** and
leaves MudBlazor's `Required` parameter alone. ⛔ Do not "simplify" this back to setting `Required`
— that reintroduces the HTML5 attribute this convention forbids. And do not drop the annotation
either: that is #190, and it is a Level A failure. Both halves are pinned by `AriaRequiredTests`
- ⚠️ **The visible asterisk is gone for a plain `.Required(...)` field**, and that is a decision, not
a regression to fix. MudBlazor draws it from the same `Required` parameter as the HTML5 attribute,
and #13613 did not separate *those* two. `.WithNativeRequired()` is the documented way back to
native semantics — asterisk and HTML5 attribute together
- 🔒 **Version floor: MudBlazor ≥ 9.9.0.** On anything older, 9 of the 35 `AriaRequiredTests` fail
(measured by reverting the pin). Do not lower `Directory.Packages.props` below it
- Browser validation disabled via a `novalidate` attribute **rendered on the form** by
`FormCraftComponent` (#206). It is a real attribute in the markup, so it applies during
prerender/SSR, targets this component's own form rather than the first one on the page, and needs
Expand All @@ -352,15 +361,18 @@ because it lives in core rather than in one of the two packages that need it.
ran on the server pass, failed silently, and was blocked outright by a strict CSP
- All validation through FluentValidation
- Validation messages from server, not browser
- MudBlazor components DO set `Required` for a `.Required(...)` field since #199 (see above), on
**every** field type that can carry it: text, numeric, date, select, multi-select, autocomplete,
lookup, LOV and boolean, on both render paths. ⛔ Keep it uniform when adding a field type. Once
required fields carry an asterisk, absence stops meaning "not annotated" and starts meaning
"optional", so a new renderer that skips this actively mis-signals rather than merely omitting
- **Checkboxes take a different route.** `MudCheckBox`/`MudSwitch` emit no `aria-required`, so
FormCraft passes it via `UserAttributes` — which lands there because nothing downstream re-emits
it. Do **not** copy that trick to `MudInput`-based fields: there MudBlazor's own later write always
wins, which is the whole reason `Required` had to be the mechanism (see `EffectiveNativeRequired`)
- MudBlazor components announce `aria-required` for a `.Required(...)` field on **every** field type
that can carry it: text, numeric, date, select, multi-select, autocomplete, lookup, LOV and
boolean. ⛔ Keep it uniform when adding a field type — a required field that says nothing is a
Level A failure, and one that says `aria-required="false"` states the opposite of the truth. The
value comes from `MudBlazorFieldComponentBase.AriaRequired`; bind it alongside `Required` in the
new component's `.razor`
- **Every field type now takes the `UserAttributes` route** that checkboxes always did. Since 9.9.0
a caller-supplied `aria-required` wins on `MudInput`-based fields too, so there is one mechanism
rather than two. ⚠️ `AriaRequired` returns `"true"`/`"false"` and **never `null`**: a null
component parameter is still captured into `UserAttributes` as a present key, and Blazor deletes
an attribute whose last write is null — returning `null` for optional fields drops
`aria-required` entirely instead of leaving it `"false"` (measured; it turned five assertions red)
- **File upload is covered too, but NOT via `Required` on `MudFileUpload`** (#262). Its
`<input type="file">` carries `tabindex="-1"` at `opacity-0` behind a custom drop zone, so
annotating that input satisfies a DOM assertion while reaching no user who navigates by focus.
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
-->
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.11" />
<!-- UI Framework packages -->
<PackageVersion Include="MudBlazor" Version="9.8.0" />
<PackageVersion Include="MudBlazor" Version="9.9.0" />
<!--
Fluent UI Blazor v5 is still an RC (#260). v4 loses support in November 2026 and v5 renamed
most of the input surface, so the adapter targets v5 directly rather than being born
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -475,14 +475,19 @@ static void Configure<TOwner>(FieldBuilder<TOwner, string> field)
standaloneRender.Find("input").GetAttribute("type").ShouldBe("password");
itemRender.Find("input").GetAttribute("type").ShouldBe("password");

// Required is compared here for a .Required(...) field, which since #199 renders TRUE on both
// paths so the field is announced to assistive technology. This assertion used to read
// ShouldBeFalse (#190); the value flipped, the guard did not. Its bite is unchanged and
// symmetric — Presentation() above compares the two paths, and this line pins WHICH of the
// two agreed values they settled on, so levelling both back down to silence fails here
// rather than passing as a vacuous agreement. The explicit .WithNativeRequired() opt-in is
// guarded by the test below (#204), and the opt-OUT by AriaRequiredTests.
standalone.Required.ShouldBeTrue();
// Required is compared here for a .Required(...) field, which since #263 renders FALSE on
// both paths: the announcement moved to aria-required via UserAttributes, and MudBlazor's
// Required parameter — which drags the HTML5 attribute and the asterisk along with it — is
// now reserved for the explicit .WithNativeRequired() opt-in.
//
// The value has now flipped twice (#190 false, #199 true, #263 false) while the guard stayed
// put, so it is worth being explicit about what it still guards. On its own this line no
// longer distinguishes "correctly announced" from "levelled back down to silence" — both
// read false here. The pair below does: aria-required must be "true" on the standalone path
// AND equal on the item path. So read the two together — this line pins that the native
// decoration is off, the next pins that the field is still announced anyway. Silence fails
// the second even though it passes the first.
standalone.Required.ShouldBeFalse();

// And the accessibility attribute itself, on both paths (#199). The parameter comparison
// above cannot see this: Required is what FormCraft sets, aria-required is what MudBlazor
Expand Down Expand Up @@ -867,9 +872,11 @@ static void Configure<TOwner>(FieldBuilder<TOwner, int> field)
standalone.Adornment.ShouldBe(Adornment.End);
standalone.AdornmentIcon.ShouldBe(Icons.Material.Filled.Numbers);
standalone.Variant.ShouldBe(Variant.Filled);
standalone.Required.ShouldBeTrue();
standalone.Required.ShouldBeFalse();

// The accessibility attribute a screen reader reads, on both paths (#199)
// The accessibility attribute a screen reader reads, on both paths (#199, #263). Since #263
// this is the assertion carrying the weight — see the longer note on the text-field parity
// test above — because the Required parameter reads false for silence and success alike.
standaloneRender.Find("input").GetAttribute("aria-required").ShouldBe("true");
itemRender.Find("input").GetAttribute("aria-required")
.ShouldBe(standaloneRender.Find("input").GetAttribute("aria-required"));
Expand Down
Loading
Loading