Skip to content

Warn when ShrinkLabel=false is silently overridden by a placeholder or start adornment #181

Closed

Description

@phmatray

Problem / motivation

#177 shipped .WithShrinkLabel(false) and DefaultShrinkLabel so a Variant.Text field can let its label float. While verifying it, we found the setting is silently inert on a large class of fields.

MudBlazor's MudInput does not treat ShrinkLabel as the deciding vote. It ORs it with three other conditions before emitting the mud-shrink class:

HasNativeHtmlPlaceholder() || !string.IsNullOrEmpty(ReadText) ||
Adornment == Adornment.Start || !string.IsNullOrWhiteSpace(Placeholder) || ShrinkLabel

So .WithShrinkLabel(false) is only observable on an empty field with no placeholder and no start adornment. Measured in a real browser against the demo app:

Field configuration mud-shrink emitted Label
default yes pinned ✅ expected
Variant.Text + WithShrinkLabel(false) no floating ✅ the feature works
…the same plus .WithPlaceholder(...) yes pinned ❌ setting ignored
…the same but the field has a value yes pinned (correct — a filled field must shrink)

The placeholder case is the problem. A placeholder is extremely common, so the natural configuration — a Variant.Text field with a helpful placeholder — silently discards the new setting with no warning, no exception, and no visible clue. On the demo app's LOV page, all 8 fields render pinned for exactly this reason.

#178 chose to document this rather than change behaviour, because any fix alters rendering for existing forms. That was the right call for a feature PR, but it leaves a real trap: the API accepts a value it cannot honour. This issue is to decide what to do about it.

Note the underlying tension is genuine, not a bug to be squashed: with ShrinkLabel=false the label sits inside the input — exactly where the placeholder text is. A floating label and a placeholder are mutually exclusive by construction. MudBlazor resolves the collision by pinning the label; the question is whether FormCraft should surface that trade-off instead of letting it resolve silently.

Proposed solution

Make the conflict visible at configuration time rather than invisible at render time — without changing what any existing form renders.

Add a build-time diagnostic: when a field ends up with ShrinkLabel == false and a non-empty placeholder (or a start adornment), log a warning through the logger FormCraftComponent already injects, naming the field and explaining that MudBlazor will keep the label pinned and why. Rendering is untouched, so no form changes appearance; the developer simply stops being lied to.

This is deliberately the least invasive of the credible options — see the brainstorm for why silently dropping the placeholder was rejected.

Alternatives considered

  • Drop the placeholder when ShrinkLabel is false. Makes the setting always work, but silently discards content the developer explicitly configured, and changes rendering for existing forms. Trading one silent surprise for a worse one.
  • Throw on the conflicting combination. Unambiguous, but a hard failure over a cosmetic conflict is disproportionate and would break forms that are fine today.
  • Leave it documented only (the status quo after feat(mudblazor): make ShrinkLabel configurable per field and per form (#177) #178). Zero risk, but the trap stays: the API keeps accepting a value it cannot honour.

Area

FormCraft.ForMudBlazor — field rendering


Related: #177 (the feature that surfaced this), #178 (the PR that documented rather than fixed it)

🧠 Brainstorm

Problem / context

The purpose of ShrinkLabel=false is to let a label sit inside the input and animate upward on focus — the Material "floating label" affordance that makes Variant.Text look right. That affordance needs the inside of the input to be free. A placeholder occupies exactly that space, so MudBlazor pins the label whenever one is present, and ShrinkLabel loses.

Evidence gathered while verifying #177:

  • ShrinkLabelRenderingTests asserts the rendered mud-shrink class in both directions, including WithShrinkLabel_False_Should_Not_Unpin_The_Label_When_A_Placeholder_Is_Set.
  • Browser check of the demo app: a Variant.Text field with .WithShrinkLabel(false) and no placeholder renders floating; add a placeholder and it renders pinned.
  • The demo's LOV page renders all 8 fields pinned, every one of them because it carries a placeholder.

Worth being precise about which parts of the OR are actually problems:

Condition Silently overrides ShrinkLabel=false? Is that wrong?
field has a value (ReadText) yes No — a filled field must shrink its label or they overlap. Correct, and not worth signalling.
has a placeholder yes Yes — developer-configured, common, and nothing says the two conflict.
has a start adornment yes Yes, same reason, though rarer.
native HTML placeholder yes same as placeholder

So the actionable surface is placeholder and start adornment, not the value case.

Approaches

A. Diagnostic warning at configuration/render time.
Detect ShrinkLabel == false together with a non-empty placeholder or a start adornment, and log a warning naming the field.
Pros: zero rendering change, so no existing form moves; the developer learns the moment they hit it; cheap; reversible.
Cons: a log line is easy to miss; needs a logger in the render path (one is already injected into FormCraftComponent); risks noise if a form legitimately wants both and accepts the outcome.

B. Suppress the placeholder when ShrinkLabel is false.
Make FormCraft not pass Placeholder when the field asked for a floating label.
Pros: the setting always does what it says; no new concept to learn.
Cons: silently throws away content the developer wrote. Anyone who has Variant.Text + placeholder + DefaultShrinkLabel="false" today loses their placeholder on upgrade. It replaces an invisible no-op with an invisible deletion — strictly worse, because the discarded thing is user content rather than a styling hint.

C. Throw / fail the build on the conflicting combination.
Pros: impossible to miss.
Cons: disproportionate for a cosmetic conflict, and it would break existing forms that render acceptably today. FormCraft's .editorconfig policy caps severities at warning precisely to avoid this class of hard stop.

D. Do nothing beyond the docs already added in #178.
Pros: no risk, no work.
Cons: the API still accepts a value it cannot honour, and the next person re-discovers it the slow way — which is exactly how this issue came to exist.

Recommendation

A, scoped to the placeholder and start-adornment cases only (never the has-a-value case, which is correct behaviour and would be pure noise).

It is the only option that improves the developer's information without changing anybody's rendering. B is the tempting one — "just make the setting work" — but it wins that by deleting user-authored content, which is a worse failure than the one being fixed. C over-reacts. D leaves the trap.

If the warning proves noisy in practice, the natural follow-on is an opt-out on the field (.WithShrinkLabel(false, suppressWarning: true)) rather than a retreat to D.

📋 Spec

Goal

Stop ShrinkLabel=false from being silently inert: when the configuration cannot be honoured because of a placeholder or start adornment, say so — without changing what any form renders.

Scope

  • Detect, per field, EffectiveShrinkLabel == false together with a non-empty placeholder or a start adornment.
  • Log one warning per offending field, naming the field, the conflicting property, and the consequence.
  • Cover both render paths: MudBlazorFieldComponentBase (component path) and CollectionFieldComponent (imperative RenderTreeBuilder path).
  • Tests asserting the warning fires exactly when expected — and does not fire for the has-a-value case.

Non-goals

  • Any rendering change. No markup, class, or attribute differs after this issue; only log output does. This is the invariant that makes the change safe to ship in a patch.
  • Suppressing the placeholder (rejected approach B).
  • Throwing (rejected approach C).
  • The has-a-value case — a filled field must shrink its label; warning about it would be wrong and noisy.
  • Changing MudBlazor's own OR rule, which is upstream.

Behaviour

flowchart TD
    A[field resolves ShrinkLabel] --> B{EffectiveShrinkLabel == false?}
    B -->|no| Z[render, no diagnostic]
    B -->|yes| C{placeholder non-empty<br/>OR start adornment?}
    C -->|no| D[setting will be honoured<br/>render, no diagnostic]
    C -->|yes| E["log WARNING naming the field<br/>render UNCHANGED (label stays pinned)"]
Loading

Message shape — must name the field and the fix, not just the symptom:

Field 'Email' sets ShrinkLabel=false but also has a Placeholder. MudBlazor keeps the label pinned when a placeholder is present, so the label will not float. Remove the placeholder to get a floating label, or drop ShrinkLabel=false.

Key types / files

  • FormCraft.ForMudBlazor/Fields/MudBlazorFieldComponentBase.cs — EffectiveShrinkLabel lives here; the natural detection point for the component path.
  • FormCraft.ForMudBlazor/Features/CollectionField/CollectionFieldComponent.razor.cs — GetItemFieldShrinkLabel / AddCommonFieldAttributes for collection item fields.
  • FormCraft.ForMudBlazor/Features/FormContainer/FormCraftComponent.razor.cs — already injects IServiceProvider and resolves a logger; reuse that rather than adding a new dependency.
  • FormCraft.ForMudBlazor.UnitTests/Fields/ShrinkLabelRenderingTests.cs — the existing rendering assertions this builds on.

Validation rules

  • ShrinkLabel=false + placeholder → exactly one warning, naming the field; markup unchanged (still mud-shrink).
  • ShrinkLabel=false + start adornment → one warning.
  • ShrinkLabel=false, no placeholder, no adornment → no warning; markup has no mud-shrink.
  • ShrinkLabel=false on a field with a value but no placeholder → no warning (correct behaviour, not a conflict).
  • Default configuration (ShrinkLabel unset) → no warning, ever.
  • Warning fires once per field per render pass, not once per keystroke.

Edge cases

  • LOV fields always supply "Click to select...", so they would warn on every ShrinkLabel=false form. Since a LOV label can never float, suppress the warning for that component specifically, or the diagnostic becomes noise the moment anyone sets DefaultShrinkLabel="false" form-wide.
  • Form-level DefaultShrinkLabel="false" on a form of placeholder-bearing fields would emit one warning per field — potentially many. Consider aggregating to a single warning listing the fields.
  • File-upload components render buttons with no ShrinkLabel; exclude them.
  • No logger registered — degrade silently, never throw from a diagnostic path.

Assumptions

  • No repo issue-form template exists (.github/ISSUE_TEMPLATE/ is absent), so the fields above follow the conventional feature-request shape.
  • Target is a patch/minor: rendering does not change. Base branch is dev.
  • The MudBlazor OR rule quoted above is treated as upstream behaviour to adapt to, not to work around.

🛠️ Implementation plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: warn when ShrinkLabel=false cannot be honoured (placeholder or start adornment present), while leaving every form's rendering byte-identical.

Architecture: all changes in FormCraft.ForMudBlazor — the UI-agnostic FormCraft core is not touched. Configuration flows FieldBuilder → AdditionalAttributes → renderer; this adds an observation at the renderer, never a mutation.

Tech stack: .NET 8 / 9 / 10 multi-target, Blazor, MudBlazor, xUnit + bUnit + Shouldly (MudBlazorTestBase).

Global constraints:

  • Base branch dev; commit as Philippe Matray <phmatray@gmail.com>; conventional commits.
  • TreatWarningsAsErrors=true — the build fails on any warning.
  • Rendering must not change. ShrinkLabelRenderingTests and RenderPipelineParityTests must stay green unmodified — editing them means the change went wrong.
  • A diagnostic must never throw, and must degrade silently when no logger is registered.
  • dotnet test --filter is silently ignored here (MTP0001) — it runs the whole suite. Use the full run.

Task 1: Detect the conflict and warn on the component path

Files: modify FormCraft.ForMudBlazor/Fields/MudBlazorFieldComponentBase.cs; create FormCraft.ForMudBlazor.UnitTests/Fields/ShrinkLabelDiagnosticsTests.cs.

Interfaces: a protected helper on MudBlazorFieldComponentBase<TModel,TValue> that inspects EffectiveShrinkLabel, the field's Placeholder and its Adornment, and logs once per render when the combination is unhonourable.

  • Step 1: Write failing tests in ShrinkLabelDiagnosticsTests.cs using a capturing ILoggerProvider: (a) ShrinkLabel=false + placeholder → one warning naming the field; (b) ShrinkLabel=false, no placeholder → no warning; (c) default config → no warning.
  • Step 2: Run dotnet test -c Release → FAIL (no diagnostic exists).
  • Step 3: Implement the detection + warning in MudBlazorFieldComponentBase, resolving the logger from the injected IServiceProvider and no-op'ing when none is registered.
  • Step 4: Run dotnet test -c Release → PASS, and confirm ShrinkLabelRenderingTests + RenderPipelineParityTests are still green with no edits.
  • Step 5: Commit: feat(mudblazor): warn when ShrinkLabel=false is overridden by a placeholder.

Task 2: Cover the start-adornment case and suppress the LOV false positive

Files: modify FormCraft.ForMudBlazor/Fields/MudBlazorFieldComponentBase.cs, FormCraft.ForMudBlazor/Fields/LovField/MudBlazorLovFieldComponent.razor (or its code-behind); modify ShrinkLabelDiagnosticsTests.cs.

Interfaces: an opt-out the LOV component sets, so a component whose label can never float does not warn.

  • Step 1: Write failing tests: (a) ShrinkLabel=false + start adornment → warning; (b) a LOV field with ShrinkLabel=false → no warning despite its built-in placeholder; (c) a field with a value but no placeholder → no warning.
  • Step 2: Run the suite → FAIL.
  • Step 3: Extend the detection to start adornments, and give the LOV component the opt-out (its label is structurally always pinned — see the comment already in that razor file).
  • Step 4: Run the suite → PASS.
  • Step 5: Commit: feat(mudblazor): cover start adornments and exempt LOV from the ShrinkLabel warning.

Task 3: Collection item fields

Files: modify FormCraft.ForMudBlazor/Features/CollectionField/CollectionFieldComponent.razor.cs (GetItemFieldShrinkLabel / AddCommonFieldAttributes); modify ShrinkLabelDiagnosticsTests.cs.

Interfaces: the same detection reached from the imperative RenderTreeBuilder path, sharing one implementation with Task 1 rather than duplicating the rule.

  • Step 1: Write a failing test: a collection item field with ShrinkLabel=false and a placeholder → one warning naming the item field.
  • Step 2: Run the suite → FAIL.
  • Step 3: Call the shared detection from the collection path; do not re-implement the predicate (one rule, one implementation).
  • Step 4: Run the suite → PASS.
  • Step 5: Commit: feat(mudblazor): warn for collection item fields whose ShrinkLabel is overridden.

Task 4: Aggregate form-wide noise and document

Files: modify FormCraft.ForMudBlazor/Features/FormContainer/FormCraftComponent.razor.cs; modify README.md; modify ShrinkLabelDiagnosticsTests.cs.

Interfaces: none new.

  • Step 1: Write a failing test: a form with DefaultShrinkLabel="false" and 5 placeholder-bearing fields emits one aggregated warning listing them, not five.
  • Step 2: Run the suite → FAIL.
  • Step 3: Aggregate per render pass in FormCraftComponent.
  • Step 4: Update the README's existing [!IMPORTANT] ShrinkLabel callout to mention that FormCraft now warns when the setting cannot be honoured.
  • Step 5: Run dotnet build -c Release and dotnet test -c Release → both green.
  • Step 6: Commit: feat(mudblazor): aggregate the ShrinkLabel diagnostic per form and document it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority:lowNice to havestatus:triagedClassified and ready for analysis/worktype:featureNew feature or enhancement request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions