Skip to content

feat(mudblazor): make ShrinkLabel configurable per field and per form (#177) - #178

Merged
phmatray merged 8 commits into
devfrom
feat/177-configurable-shrink-label
Aug 11, 2026
Merged

phmatray merged 8 commits into
devfrom
feat/177-configurable-shrink-label

Conversation

@phmatray

@phmatray phmatray commented Aug 11, 2026 •

Copy link
Copy Markdown
Owner

Implements #177.

Closes #177.

Follow-up to #146: ShrinkLabel="true" is hardcoded at 12 sites, so Variant.Text renders a permanently-pinned label over a borderless input. This makes it configurable through the same channel Variant already uses — field-level .WithShrinkLabel(bool) over form-level DefaultShrinkLabel, resolving to true when unset so no existing form changes appearance.

// per field
.AddField(x => x.Name, field => field
    .WithLabel("Name")
    .WithVariant(Variant.Text)
    .WithShrinkLabel(false))     // label floats on focus/value

// or for the whole form
<FormCraftComponent TModel="MyModel" Model="@model" Configuration="@config"
                    DefaultVariant="Variant.Text"
                    DefaultShrinkLabel="false" />

Plan

  • Task 1: Field-level .WithShrinkLabel(...), honored by the text field
  • Task 2: Form-level DefaultShrinkLabel + precedence
  • Task 3: Roll out to the remaining 10 .razor components
  • Task 4: Collection item fields (RenderTreeBuilder path)
  • Task 5: Decide the LOV field's ShrinkLabel (flagged — behavior change)
  • Task 6: Documentation

Known limitation — ShrinkLabel="false" is not always observable

Worth reading before reviewing, because it bounds what this PR actually delivers.

MudBlazor does not use ShrinkLabel as the last word. MudInput decides the mud-shrink class by OR-ing it with several other conditions:

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

So .WithShrinkLabel(false) is only visible on an empty field with no placeholder and no start adornment. A field configured with .WithPlaceholder(...) keeps its label pinned whatever you pass.

This is MudBlazor's rule, not something this PR can fix without silently discarding the user's placeholder — so it is documented instead: on WithShrinkLabel, on DefaultShrinkLabel, and as an [!IMPORTANT] callout in the README next to the usage example. ShrinkLabelRenderingTests asserts the rendered mud-shrink class in both directions so the boundary is a tested fact rather than folklore.

Correction: the LOV field has no visual change

An earlier revision of this description and of the README claimed LOV labels "started floating while every sibling pinned it" and were "now aligned". That was wrong, and the code review caught it.

Measured by reverting the one-line change and re-running the rendering test against the base commit: LOV labels were already pinned before this PR, because the LOV input always supplies its own "Click to select..." placeholder and MudBlazor's placeholder rule shrinks the label on its own. The rendering is byte-identical with and without the change.

The ShrinkLabel="@EffectiveShrinkLabel" pass-through is kept — it makes the component consistent with its 11 siblings and stops a future reader from "fixing" the asymmetry again — but it is now marked in the razor as unable to affect that component's rendering, and ShrinkLabelRenderingTests.LovField_Label_Is_Always_Pinned_Regardless_Of_ShrinkLabel locks that in.

Net result: this PR changes no existing form's appearance at all. The default resolves to true everywhere, and the two pre-existing parity assertions (RenderPipelineParityTests.cs:237 and :257, both ShrinkLabel.ShouldBeTrue()) stay green unmodified — they are the back-compat guard.

Follow-ups

Found while working, deliberately not fixed here to keep this PR scoped:

  • LovBuilder.WithDisplay cannot be called with a bare lambda. It declares both WithDisplay(Expression<Func<TItem, string>>) and WithDisplay(Func<TItem, string>), so lov.WithDisplay(c => c.Name) fails with CS0121: the call is ambiguous — the natural call site, and the exact form used in the library's own XML-doc examples. Callers must write (Expression<Func<CityDto, string>>)(c => c.Name), which the tests here do with a comment explaining why. Fixing it means renaming one overload or dropping the Func<> one (a breaking change), so it wants its own issue.
  • Consider whether a Variant.Text field should suppress a placeholder. Given the OR rule above, a placeholder and a floating label are mutually exclusive in MudBlazor. FormCraft could surface that trade-off explicitly rather than letting ShrinkLabel quietly lose. Deliberately out of scope here — it changes rendering for existing forms.

MudBlazor ORs ShrinkLabel with has-value / has-placeholder / has-start-adornment
before deciding the mud-shrink class, so ShrinkLabel=false is only observable on an
empty field with none of those. The parameter-level tests could not see this because
they assert the value passed in, not the rendering.

- Add ShrinkLabelRenderingTests asserting the rendered mud-shrink class, pinning both
  where the setting works and where MudBlazor overrides it.
- Document the caveat on WithShrinkLabel, DefaultShrinkLabel and in the README.
- Correct the README claim of a LOV visual change: measured against the base commit,
  LOV labels were already pinned via its own placeholder, so this PR changes nothing
  visually. Keep the ShrinkLabel pass-through for consistency, with a comment saying
  it cannot affect that component's rendering.
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.

Make ShrinkLabel configurable on MudBlazor field components

1 participant