WssBlazorControls.Demo
10.6.6
dotnet add package WssBlazorControls.Demo --version 10.6.6
NuGet\Install-Package WssBlazorControls.Demo -Version 10.6.6
<PackageReference Include="WssBlazorControls.Demo" Version="10.6.6" />
<PackageVersion Include="WssBlazorControls.Demo" Version="10.6.6" />
<PackageReference Include="WssBlazorControls.Demo" />
paket add WssBlazorControls.Demo --version 10.6.6
#r "nuget: WssBlazorControls.Demo, 10.6.6"
#:package WssBlazorControls.Demo@10.6.6
#addin nuget:?package=WssBlazorControls.Demo&version=10.6.6
#tool nuget:?package=WssBlazorControls.Demo&version=10.6.6
WssBlazorControls
A comprehensive library of form controls for Blazor applications providing consistent, feature-rich input components with built-in validation, accessibility support, and flexible styling options.
Features
- Rich Form Controls: String, Number, Date, Boolean, Select, Radio, Checkbox lists, and TextArea components
- Searchable & Multi-Select: AntDesign-style
EditSelectSearch/EditMultiSelect— type-to-search, tags, virtualized dropdown - AntDesign-style UI Kit: dependency-free Alert, Modal, Drawer, Table, Pagination, Popover, Popconfirm, DateRangePicker, Skeleton, and toasts
- Data Annotations Integration: Full support for validation attributes (Required, Range, MinLength, etc.)
- Validator-Agnostic Core: messages, invalid-state ARIA, and the validation summary work with any
EditContextvalidator; a form-levelRequiredResolverbridges required-star/aria-requiredfor FluentValidation and other stacks - Accessibility First: ARIA attributes, screen reader support, and keyboard navigation
- Flexible Display Modes: Edit mode and read-only views for all controls
- Consistent Styling: CSS classes and customizable appearance
- TypeScript/JavaScript Interop: Enhanced client-side functionality
- Cross-Platform: Works with both Blazor Server and Blazor WebAssembly
Installation
Install the package via NuGet Package Manager:
dotnet add package WssBlazorControls
Or via Package Manager Console:
Install-Package WssBlazorControls
Quick Start
- Add the using statement to your
_Imports.razor:
@using Controls
- Include the CSS in your
App.razororindex.html:
<link href="_content/WssBlazorControls/edit-controls.css" rel="stylesheet" />
<link href="_content/WssBlazorControls/wss-controls.css" rel="stylesheet" />
- Include the JS helpers (next to your Blazor script tag):
<script src="_content/WssBlazorControls/edit-controls.js"></script>
Required by JsInteropEc.FocusFirstInvalidField (focus the first invalid field on a failed
submit). The UI-kit controls load their own JS modules lazily — no extra tags needed for them.
If the script tag isn't linked (e.g. a cross-origin micro-frontend whose host page doesn't serve
_content/WssBlazorControls/), JsInteropEc's methods lazily import the module themselves and
never throw — see FormDefaults.AssetBase to point that fallback import at the
right origin.
- Use the controls in your Blazor components:
@using System.ComponentModel
@using System.ComponentModel.DataAnnotations
<EditForm Model="@model" OnValidSubmit="@HandleSubmit">
<DataAnnotationsValidator />
@* No Label needed: "Name", "Age", and "Birth Date" are derived correctly
from the property names, and the required star comes from [Required]. *@
<EditString @bind-Value="model.Name" />
<EditNumber @bind-Value="model.Age" />
<EditDate @bind-Value="model.BirthDate" />
@* "Is Active" would be wrong, so the constant label lives on the model. *@
<EditBool @bind-Value="model.IsActive" />
@* Label is set in markup only because the text is dynamic at runtime. *@
<EditString @bind-Value="model.Answer" Label="@_currentQuestion" />
<button type="submit">Submit</button>
<ValidationSummary />
</EditForm>
@code {
private string _currentQuestion = "Your favorite color?";
private PersonModel model = new();
private void HandleSubmit()
{
// Handle form submission
}
public class PersonModel
{
[Required]
[StringLength(100)]
public string Name { get; set; } = "";
[Required]
[Range(1, 120)]
public int? Age { get; set; }
public DateTime? BirthDate { get; set; }
[DisplayName("Active Status")]
public bool IsActive { get; set; } = true;
public string? Answer { get; set; }
}
}
Labeling: how to choose
Pick the label source by how the text is determined, in this order of preference:
- Let the label auto-generate from the property name when that's already correct. The name is split on camel-case, so
BirthDate→ "Birth Date". Don't set anything — noLabel, no attribute, and no manual<label>. - Put constant labels on the model with
[DisplayName("...")]when the auto-generated text is wrong or awkward (e.g.IsActive→ "Is Active", but you want "Active Status"). This keeps the label next to the data it describes and reused everywhere the property is rendered. - Set the
Labelparameter in markup only for dynamic / runtime text — a label that varies per instance or isn't known at compile time. A constant string inLabel="..."is the wrong tier; move it to[DisplayName].
Under the hood the highest-priority source wins: the Label parameter overrides [DisplayName], which overrides the auto-generated property name. Preferring tier 1, then 2, then 3 keeps you from reaching for a higher-priority source than the text actually needs.
Available Controls
Input Controls
EditString- Text input with masking and URL supportEditTextArea- Multi-line text inputEditNumber- Numeric input with validationEditDate- Date picker componentEditDatePicker- Form-bound calendar-dropdown date field (the UI-kitDatePickerwith fullEditFormvalidation)EditDateRange- Form-bound date-range field (@bind-Start/@bind-End, per-field validation, backed byDateRangePicker)EditBool- Checkbox for boolean valuesEditBoolNullRadio- Three-state radio for nullable booleansEditFile- Multi-file upload bound to aList<IBrowserFile>(drag-and-drop + click-to-browse, extension filtering, per-file size cap, aggregate size cap, optional max count)
Selection Controls
EditSelect- Dropdown selection for objectsEditSelectEnum- Dropdown for enum valuesEditSelectString- Dropdown for string valuesEditSelectSearch- Searchable single-select (AntDesign-style: type-to-search, clear, virtualized)EditMultiSelect- Multiple / tags select bound to aList<T>(AntDesign-style)EditRadio- Radio buttons for objectsEditRadioEnum- Radio buttons for enumsEditRadioString- Radio buttons for strings
Multi-Selection Controls
EditCheckedStringList- Checkbox list for stringsEditCheckedEnumList- Checkbox list for enums
Support Components
FormLabel- Consistent labeling with tooltips and descriptionsFieldValidationDisplay- Validation message displayValidationView- Validation summary that renders each error as a link jumping to its fieldReadOnlyValue- Read-only value presentationEditDisplay- Static label+value pair (no model binding)FormDefaults- Render-tree-scoped defaults for the controls (see below)
FormDefaults
Wrap your app root (or each micro-frontend's root) in FormDefaults to set control defaults for every form underneath it:
<FormDefaults IsRequiredStarHidden="true" ShowFieldNameInValidation="false" UseStyledCheckbox="true">
<Router AppAssembly="@typeof(App).Assembly">...</Router>
</FormDefaults>
Resolution per setting (highest wins): the form's FormOptions instance value → the cascaded FormDefaults → the static FormOptions.Default* property. Prefer FormDefaults over the statics: the statics are process-wide, so on Blazor Server they're shared by every user/circuit, and when several MFEs share one runtime they're shared across MFEs. FormDefaults scopes to the render tree, which matches app/MFE/circuit boundaries. It's intended as set-once root configuration — the cascade is registered as fixed, so runtime changes to its parameters don't propagate.
UseStyledCheckbox follows this same chain and additionally reaches the UI-kit Table's row-selection checkboxes (which have no FormOptions of their own) — see Custom-Styled Checkbox.
FormDefaults also carries AssetBase (string?), which has no FormOptions counterpart: an absolute URL prefixed onto the RCL's lazy wss-*.js module imports (see the UI Kit section below), for a micro-frontend whose host page doesn't serve/proxy _content/WssBlazorControls/*. Unset (the default) keeps today's relative import path.
EditDisplay vs ReadOnlyValue
Both render text in the edit-readonly-value style, but their use cases are different:
EditDisplay |
ReadOnlyValue |
|
|---|---|---|
| When to use | Standalone label+value pair outside an Edit* control — e.g. a derived value like "15.3 oz / can" that's not bound to a model property |
Always — it's rendered by the Edit* controls in read-only mode, not typically used directly by consumers |
| Owns its label | Yes (Label, Description, Tooltip parameters) |
No — sits inside an Edit* control that owns the FormLabel |
| Model binding | None | None (reads Text after the parent has formatted the value) |
| Validation | None | None (the parent control's FieldValidationDisplay handles it) |
Reach for EditDisplay when you want the same visual treatment as a read-only EditString but without an EditForm / model property behind it.
UI Kit (non-form) controls
A set of dependency-free, AntDesign-style general UI widgets (ported from Standalone.Controls). Unlike the Edit* controls these are not form-bound — they're plain components. They use the wss- CSS prefix and --wss-* theme tokens shipped in wss-controls.css (link it as shown in Quick Start). No service registration is required.
Select<T>- The dropdown engine behindEditSelectSearch/EditMultiSelect; usable standalone (single / multiple / tags, search, virtualized).Prefixrenders leading content (typically an icon) in the trigger;Variant="SelectVariant.Pill"restyles the trigger as a rounded filter button (see below)Alert- Contextual message banner (success / info / warning / error, closable, description)Skeleton- Loading placeholder with shimmer; announcesrole="status"/aria-busywith a visually-hiddenLoadingText(default"Loading") for screen readersPopover- Click-triggered popover (4 placements)Pagination- Controlled pagerModal- Dialog with@bind-Visible, footer, mask-closeDrawer- Slide-in panel (4 placements)Popconfirm- Inline confirm popoverDatePicker- Single-date field with a calendar suffix opening a one-month calendar (month/year quick-select header). Bind with@bind-Value(DateTime?, date-only); picking a day (or typing a date + Enter) commits and closes;Min/Max,Format(defaultMM/dd/yyyy),Placeholder(default "Select date"),AllowClear,Width,FirstDayOfWeek— the single-date sibling ofDateRangePicker, sharing its calendar internals and outside-click/Escape close behaviorDateRangePicker- Composite start → end date field opening a dropdown with an optional preset sidebar and a dual-month calendar (month/year quick-select headers). Bind with@bind-Start/@bind-End(DateTime?, date-only);Presets(a label + a range-resolvingFunc, evaluated at click time — or fixed dates),Min/Maxday bounds,Format(defaultMM/dd/yyyy, drives display, parsing and the placeholders),AllowClear,Width,FirstDayOfWeek(default: current culture). Picking the second day (or a preset) commits and closes; a backwards pair swaps; typed input commits on Enter/blur. Flips above the field / shifts into the viewport when space is tightTable<TItem>- Data table withColumn/PropertyColumn/ActionColumn, row selection, paging (pager placement viaPagerPosition= Top/Bottom/Both and alignment viaPagerAlign), and column sorting (Sortable="true"on aPropertyColumn— non-comparable types degrade to non-sortable; or aSortBycomparison on any column). Columns may be conditionally rendered (@if).RowDetail(aRenderFragment<TItem>) adds expandable rows: a leading chevron column toggles the template as a full-width row beneath each row (e.g. a nested childTable); expansion is keyed byRowKeyidentity so it survives paging/sorting.Column.TitleContentreplaces a plainTitlewith templated header content (e.g. a title plus aLabelTooltipinfo icon)Tabs/Tab- Underline tab strip with an optional bordered count chip per tab (Count); bind with@bind-ActiveKey(astring?). Tabs withChildContentshow the active pane below the strip; content-less tabs act as a bare filter strip. ARIA tabs pattern with automatic activation (arrows move + select with wrapping, roving tabindex; Home/End deliberately unhandled — Blazor can'tpreventDefaultper key)SearchInput- Search field: optional leading addon label chip (AddonLabel/AddonContent), text input (@bind-Value, per-keystroke), and an icon-only search button —OnSearchfires on Enter and on the button. Pill-rounded ends by default (--wss-search-radiusto square them)- Toasts & notifications - two paths with identical rendering: scoped / Server-safe (
IMessageService/INotificationServiceviabuilder.Services.AddWssControlsToasts()+<MessageContainer />/<NotificationContainer />), or registration-free static for WASM (WasmMessageService/WasmNotificationService+<WasmMessageContainer />/<WasmNotificationContainer />). On Blazor Server use the scoped path — the staticWasm*services hold process-static state that would bleed across users.
Icon,Button,Checkbox, andTagare intentionally not part of this library.
Pill filter variant (Select / EditSelectSearch)
Variant="SelectVariant.Pill" turns the Select trigger into a fully-rounded outlined filter button that hugs its content — the "All shipments ⌄" pattern. Pair it with Prefix for a leading icon, and usually ShowSearch="false" / AllowClear="false" so it reads as a button. The dropdown gets softer corners, content-driven width, and conveys the current value by the bold/tinted row alone (no checkmark). Behavior is unchanged: keyboard navigation, type-ahead, outside-click and Escape close.
<Select TValue="string" @bind-Value="_shipmentFilter" Options="_shipmentOptions"
Variant="SelectVariant.Pill" ShowSearch="false" AllowClear="false">
<Prefix><svg ... aria-hidden="true">...</svg></Prefix>
</Select>
Theming: the whole trigger (label, border, chevron, focus ring) derives from one knob — override --wss-select-pill-color at any scope (--wss-select-pill-border / --wss-select-pill-bg are finer-grained overrides). The selected row tint is the kit-wide --wss-color-bg-selected:
.my-filters {
--wss-select-pill-color: #1c4a3f; /* label, border, chevron, focus ring */
--wss-color-bg-selected: #d9e8e2; /* selected dropdown row */
}
Prefix also works on the outlined variant and on EditMultiSelect; EditSelectSearch forwards both Variant and Prefix.
Server-side paging (Table)
The Table's built-in pager (PageSize) is in-memory — it materializes the whole DataSource and slices it client-side, so it can't reflect a server-side total. For server-side paging, compose the Table with the standalone, fully-controlled Pagination: give the Table only the current page (omit PageSize so it renders exactly what you pass), and drive a Pagination yourself.
<Table TItem="Row" DataSource="_pageRows">
<PropertyColumn TItem="Row" TProp="int" Title="Id" Property="@(r => r.Id)" />
<PropertyColumn TItem="Row" TProp="string" Title="Name" Property="@(r => r.Name)" />
</Table>
<div style="display:flex; justify-content:flex-end; margin-top:16px;">
<Pagination Total="_total" PageSize="PageSize" Current="_page" CurrentChanged="GoToPageAsync" />
</div>
@code {
const int PageSize = 20;
List<Row> _pageRows = new();
int _total, _page = 1;
protected override Task OnInitializedAsync() => GoToPageAsync(1);
async Task GoToPageAsync(int page)
{
_page = page;
var result = await Api.GetRows(page, PageSize /*, sortField, sortDir */);
_pageRows = result.Items.ToList(); // a NEW reference — the Table only re-copies when DataSource changes ref
_total = result.TotalCount; // the server's overall count drives the pager
}
}
Pagination is a controlled component (Total / Current / PageSize + CurrentChanged), so it shows the correct page count from the server total and raises CurrentChanged when the user picks a page. Handle sorting the same way — pass the sort field/direction into your request rather than using the Table's built-in Sortable, which only orders the page already loaded. A runnable example (with a simulated server) is in the /uikit gallery.
Component Features
All form controls implement the IEditControl interface and provide:
- Identity Management:
Id,IdPrefixfor unique identification - Display Control:
IsEditMode,IsDisabled,IsHidden - Labeling: auto-generated from the property name,
[DisplayName]for constant labels, or theLabelparameter for dynamic text — see Labeling: how to choose.Descriptionis plain text (HTML-encoded when rendered). - Styling:
ContainerClassfor custom CSS - Validation: required-ness from
[Required], the three-stateIsRequiredparameter, orFormOptions.RequiredResolver— see Validation stacks - Conditional Display:
Hidingmodes andHidingModeenum
Validation stacks (DataAnnotations, FluentValidation, custom)
The runtime validation plumbing is validator-agnostic: validation messages, aria-invalid, aria-errormessage, the invalid icon/red styling, and the ValidationView summary all read from the cascading EditContext, so anything that writes a ValidationMessageStore (DataAnnotations, Blazored.FluentValidation, a hand-rolled validator) works out of the box. Labels are also independent of the validation stack — [DisplayName]/[Display] and the auto-generated property-name fallback keep working.
What is DataAnnotations-specific is required-ness discovery (the required star and aria-required come from reflecting [Required] off the model) and the short-message rewrite (only the stock .NET DataAnnotations message templates are rewritten — e.g. "The X field is required." → "Required"; messages from other validators display verbatim, which is normally what you want since FluentValidation's defaults are already human-readable).
Required-ness resolves per control in this order:
IsRequiredparameter (three-statebool?) — when explicitly set it wins outright:trueforces the star/aria-requiredon (e.g. a RequiredIf condition that's currently active),falseforces them off (e.g. aRequiredAttribute-derived conditional whose condition is off, which would otherwise show a permanent star).[Required]attribute on the model property.FormOptions.RequiredResolver— a form-levelFunc<FieldIdentifier, bool>for stacks that don't use attributes.
FluentValidation bridge
Build the resolver once from your validator's own rules, so the star, aria-required, and the messages all share one source of truth:
<EditForm Model="model">
<FluentValidationValidator /> @* Blazored.FluentValidation *@
<CascadingValue Value="_formOptions">
<EditString @bind-Value="model.Name" />
...
</CascadingValue>
</EditForm>
@code {
FormOptions _formOptions = new();
protected override void OnInitialized()
{
// Fields with a NotNull/NotEmpty rule are "required" — no [Required] attributes needed.
var required = new PersonValidator().CreateDescriptor()
.GetMembersWithValidators()
.Where(m => m.Any(v => v.Validator is INotNullValidator or INotEmptyValidator))
.Select(m => m.Key)
.ToHashSet();
_formOptions.RequiredResolver = f => required.Contains(f.FieldName);
}
}
The resolver is keyed by FieldIdentifier, so if two nested objects have same-named properties, compare f.Model too instead of just the field name. Set the resolver before the form renders — controls consult it on init and on parameter changes.
Two caveats for mixed estates:
ShowFieldNameInValidation="false"(the short "Required"-style messages) only affects rewritten DataAnnotations messages; FluentValidation messages always embed their own property name.- For nested models, plain
DataAnnotationsValidatorvalidates nested fields on edit but skips them on submit — useObjectGraphDataAnnotationsValidator+[ValidateComplexType](requires theMicrosoft.AspNetCore.Components.DataAnnotations.Validationpackage) or FluentValidation, which handles nesting natively.
Examples
Dropdown with Enum
<EditSelectEnum @bind-Value="model.Priority"
Label="Priority Level"
IsRequired="true" />
@code {
public enum Priority
{
Low,
Medium,
High,
Critical
}
public class TaskModel
{
[Required]
public Priority? Priority { get; set; }
}
}
Radio Button Group
<EditRadioString @bind-Value="model.Department"
Label="Department"
Options="@departments" />
@code {
private List<string> departments = new()
{
"Engineering",
"Marketing",
"Sales",
"Support"
};
}
Checkbox List
<EditCheckedStringList @bind-Value="model.Skills"
Label="Technical Skills"
Options="@skills" />
@code {
private List<string> skills = new()
{
"C#",
"JavaScript",
"Blazor",
"ASP.NET Core"
};
}
Custom-Styled Checkbox (border-radius)
No current browser (Chromium or Safari/WebKit) honors border-radius on a native <input type="checkbox"> once accent-color is set (see caniuse: accent-color). When a design spec calls for a shaped checkbox, opt in with UseStyledCheckbox:
<EditBool @bind-Value="model.AcceptedTerms" UseStyledCheckbox="true" />
The real <input> stays in the DOM — focusable, keyboard-operable, full native semantics — but is visually hidden; a sibling element draws the box, checked fill, checkmark, and focus ring in CSS (.edit-checkbox-box in edit-controls.css), styleable like any other element. Defaults to null (falls through to the app-wide switch below, ultimately false), so every existing EditBool renders exactly as before unless something in that chain turns it on.
EditCheckedStringList and EditCheckedEnumList take the same UseStyledCheckbox (bool?) parameter and apply it to every option's checkbox; the UI-kit Table (see UI Kit) takes it too, applied to the header/row selection checkboxes including the indeterminate "mixed" glyph.
Turning it on for a whole app or MFE
Setting UseStyledCheckbox="true" on every control individually doesn't scale. Instead, set it once — either on the cascaded FormOptions for one form/section, or on FormDefaults for everything under an app or micro-frontend root — and leave the per-control parameter unset everywhere:
<FormDefaults UseStyledCheckbox="true">
<Router AppAssembly="@typeof(App).Assembly">...</Router>
</FormDefaults>
Resolution per control (first non-null wins): the control's own UseStyledCheckbox parameter → the cascaded FormOptions.UseStyledCheckbox → the nearest enclosing FormDefaults.UseStyledCheckbox → the process-wide FormOptions.DefaultUseStyledCheckbox static (default false). Table has no FormOptions of its own, so it resolves through FormDefaults then the static only.
Styling and Customization
The library provides default styling through the included CSS file. You can customize the appearance by:
- Overriding CSS classes in your own stylesheets
- Using ContainerClass parameter for component-specific styling
- Applying custom CSS to the
.edit-control-wrapperclass
The AntDesign-style UI-kit controls (Alert, Modal, Table, Select, ...) are themed via --wss-* CSS custom properties in wss-controls.css. They default to the AntDesign 4.x look and bridge to your existing --color-primary / --color-danger / --border-color where those are defined, so they pick up your theme automatically. Override any --wss-* variable to re-theme.
Where to set the variables. The --wss-* / --edit-* tokens can be overridden at any scope — :root, body, a theme class, or a micro-frontend's root container — and derived states (hover borders, focus shadows, focus rings) follow the override, because they derive from the base token at each usage site. The generic --color-primary / --color-danger / --border-color bridge, by contrast, is resolved once, at :root (a CSS custom property substitutes the var()s in its value where the property is declared): a --color-primary set on a nested container is not seen. Rule of thumb: app-wide theme → set --color-* at :root and everything follows; scoped/per-area theme (e.g. an MFE that doesn't own the host page) → set the --wss-* / --edit-* tokens themselves on your container. A directly-set --wss-* token always wins over the --color-* bridge.
The UI-kit components also accept regular class / style / data-* attributes (applied to the component's root element; class and style merge with the component's own), so one-off tweaks don't require CSS variables at all.
<EditString @bind-Value="model.Name"
Label="Name"
ContainerClass="my-custom-style" />
Accessibility
WssBlazorControls is built with accessibility as a priority:
- ARIA attributes for screen readers
- Keyboard navigation support
- Focus management and indicators
- Semantic HTML structure
- High contrast color support
Browser Support
- Modern browsers with WebAssembly support
- Designed for both Blazor Server and Blazor WebAssembly scenarios
- Requires .NET 10.0+
Trimming and AOT
WssBlazorControls is trim- and AOT-compatible: the package ships IsTrimmable/IsAotCompatible metadata, and the trim, AOT, and single-file analyzers run warning-clean on every build (enforced by TreatWarningsAsErrors). A default Blazor WebAssembly publish (dotnet publish -c Release) trims the library automatically.
Why the attribute-driven features survive trimming:
- Labels, tooltips, descriptions, length/range extraction — every control resolves its accessor from
@bind-Value's compiler-synthesizedValueExpression; the expression tree roots the property's getter, so the trimmer keeps the property and the attributes on it. The attribute types themselves ([DisplayName],[Description],[ToolTip],[Range], ...) are referenced by the library and kept. - Enum display names —
[EnumDisplayName]/[Display]lookups only reflect over enum types, whose fields the trimmer always preserves. - Option building — enum option lists use
Enum.GetValuesAsUnderlyingType(no dynamic array creation), safe under WASM AOT.
Consumer notes:
- The generic controls (
EditNumber<T>,EditDate<T>,EditSelect<TValue>,EditRadio*) annotate their type parameter with[DynamicallyAccessedMembers(All)], mirroring the framework'sInputNumber/InputSelect. Normal usage (binding concrete model properties) compiles warning-free; only forwarding an open generic parameter into them propagates the annotation. <DataAnnotationsValidator>is the framework's reflection-based validator and warns under full trimming in your app — models bound through@bind-Valueare rooted in practice, but validation of unbound/nested models is your app's concern.[MinLength]/[MaxLength]attribute constructors are markedRequiresUnreferencedCodeby the BCL (they reflect over aCountproperty for exotic collection types). OnList<T>/ICollection— what the list-bound controls use — the reflection path is never hit; suppress or ignore that IL2026 in app code.TrimMode=fulldeletes a Blazor WASM app whose routable components are only discovered via theRouter's reflection. If you opt into full trimming, root your app assembly:<TrimmerRootAssembly Include="YourApp.Client" />.
The e2e suite can run against a trimmed publish to re-verify all of this: publish FormTesting with -p:TrimMode=full -p:WssFullTrimTest=true, then run the e2e tests with FORMTESTING_E2E_APP pointing at the published FormTesting.dll.
Contributing
Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
- Documentation: Check the demo applications in the repository
- Issues: Report bugs via GitHub Issues
- Feature Requests: Submit enhancement requests via GitHub Issues
Changelog
10.6.6
Fixes / polish (MFE-compatibility follow-up)
.edit-sr-onlynow uses the clip-based visually-hidden pattern (clip-path: inset(50%)+ 1px box +-1pxmargin) instead ofleft: -10000px— the offscreen-position pattern could be un-hidden by a consumer MFE shell's CSS resettingposition/left. Matches.wss-sr-only's existing approach; no visible change for anyone not already relying on the bug.EditString's masked read-only wrapper (the<span>+ eye-toggle<button>shown whenMaskTextis set andIsEditMode=false) now carries aedit-masked-valueclass, styleddisplay: inline-flex; align-items: center; gap: 4px. Consumers previously had to target this wrapper with a:has()hack; style.edit-masked-valuedirectly instead..edit-tooltip-content'sz-indexis now the overridablevar(--edit-tooltip-z-index, 10000)(was a hardcoded100) — tall consumer stacking contexts (drawers, modals) no longer bury the tooltip popover beneath them. Override--edit-tooltip-z-indexat any scope if 10000 still isn't high enough.
Demo (WssBlazorControls.Demo)
- New
DemoEditDatePicker/DemoEditDateRangepages (sidebar viewsDatePicker/DateRange): basic binding, Label/read-only/Min-Maxvariants, fixed-date presets, and[Required]validation sections. Both controls also joined the All Controls kitchen-sink view.
Picker fixes (post-10.6.5 audit of the new calendar pickers)
EditDatePicker/EditDateRangeaccessible names now honor theLabelparameter: the inputaria-labelresolvesInputLabel→Label→ the field's auto-derived label (previouslyLabelwas skipped, so a control withLabelset spoke a different name than it displayed — WCAG 2.5.3).EditDateRangecomposes unique per-input names —StartInputLabel/EndInputLabelwhen set, elseLabel+ " start"/" end", else each field's own auto-name;EndInputLabel's default is no longer the literal "End date" (it now derives from the End field like Start always did) and the parameter is now nullable.- The day grid's roving Tab stop skips disabled days: with
Minin the future (or the bound value outsideMin/Max), the default focus day used to be adisabledbutton, making the whole grid unreachable by keyboard. The stop now falls back to the first enabled day in view. - Prev/next month buttons actually disable at the
Min/Maxview bounds, as documented — previously they only stopped at the representable-date edges and would page into fully-disabled months. - Panel-originated closes (picking a day, Enter, Escape, preset click) return focus to the picker's text input instead of stranding keyboard focus on
<body>when the dropdown unmounts; outside-click closes leave focus where the user clicked. DateRangePicker: arrowing forward past the end of the right panel's month now shifts the view one month (focused month becomes the right panel) instead of leapfrogging two; keyboard focus after a forward month-boundary move lands on the in-month day cell, not the left grid's dimmed adjacent-month duplicate (wss-picker.jsnow prefers the roving-tabindex match).- Both pickers are now explicitly Gregorian-calendar controls under every culture: cultures whose default calendar isn't Gregorian (th-TH Buddhist, ar-SA Hijri) previously got self-contradictory chrome (Hijri month names over a Gregorian grid; a Buddhist-year input beside a Gregorian year select). All picker-internal formatting and typed-input parsing — including
EditDatePicker/EditDateRange's read-only display, which must agree with edit mode — now use the culture's language with the calendar forced to Gregorian. Behavior under Gregorian-default cultures (en-US etc.) is unchanged.
Picker parity fixes (second post-10.6.5 audit)
- Invalid pickers now show the error-red border every other control gets: new
.wss-picker.invalidrules mirror.wss-select.invalid(red border at rest, re-asserted while open/focused; the single-date variant's focus ring also flips to the error shadow). PreviouslyEditDatePicker/EditDateRangeforwarded theinvalidstate class onto the wrapper but no stylesheet rule consumed it, so an invalid picker was pixel-identical to a valid one. EditDateRangein read-only mode now forwards the consumer'sclassplus the Start field's EditContext state classes (modified/invalid/customFieldCssClassProvideroutput) to the read-only value, matching edit mode and every other control's read-only view — previously both were silently dropped.- Both pickers now honor the documented
HidingMode.*NullOrDefaultcontract fordefault(DateTime)(0001-01-01):EditDatePickeroverridesIsValueDefaultlikeEditDate, andEditDateRangetreats a null-or-default pair as empty. Previously adefault(DateTime)value kept the control visible whereEditDatewould hide it.
Fixes (multi-angle audit of the 10.6.3 UI-kit surface + range picker)
Tabs— the strip rendered one render behind for parameter changes on an existingTab: Blazor builds the strip markup before the childTabs' parameters update, and only a new tab triggered the corrective re-render, so aCountchip updated after a data load (or a runtimeTitlechange, or aDisabledflip) kept showing the old value until some later unrelated render — a just-disabled tab also kept its enabled-looking, non-disabledbutton. ATabnow detects display-relevant parameter changes and requests the follow-up render (change-guarded, so fragment-bearing tabs don't loop).Tabs— Home/End are no longer handled by the key switch: Blazor has no per-keypreventDefault, so the browser also scrolled the document to top/bottom before focus yanked it back. Arrows (with wrapping) remain the ARIA tabs navigation; matches the library's established no-JS keyboard policy.SearchInput— the input had no accessible name when the addon was supplied as anAddonContenttemplate (thearia-labelfallback only consideredAddonLabel); withAddonContent+Idand no labels the input'saria-labelledbynow points at the addon chip, whose{Id}-addonid previously dangled unreferenced.- Pill
Selectvariant (Variant="Pill") — the pill's hover/focus/open rules out-ranked the validationinvalidrules (same-specificity, later in file), so a focused, hovered, or open invalid pill select showed pill-colored chrome instead of the error red; dedicated.wss-select-pill.invalidoverrides now keep the error border and ring. The pill focus ring also no longer consults--wss-primary-shadow— it derives purely from the pill color, as documented (computed default unchanged). EditDateRange— the shared wrapper's state classes derived from the Start field only, so an End-only validation error (required End left empty, an "End ≥ Start" rule) rendered a normal border while the error message and the End input'saria-invalidwere live; the wrapper now folds End invalidity into its class (edit and read-only modes both), completing the 10.6.x "invalid pickers get the error border" fix for the range control's most common failure case.
10.6.5
New features
EditDatePicker/EditDateRange— form-bound versions of the UI-kit calendar pickers.EditDatePickerbinds aDateTime?via@bind-Value(anInputBase-derived scalar control, same contract as every otherEdit*);EditDateRangebinds two model properties via@bind-Start/@bind-End, registers both fields with the form, and validates each independently with its own message. Both render the standard label/required-star/validation scaffolding around the calendar dropdown, support read-only mode viaDateFormat, and forward the pickers' parameter surface (Min/Max,Format,Presets, placeholders, accessible-name params). To support them,DatePicker/DateRangePickergained validation-state ARIA passthrough onto their actual inputs (AriaRequired/AriaInvalid/AriaDescribedBy/AriaErrorMessage, doubled asStartAria*/EndAria*on the range picker — the same forwarding shape asSelect's trio) and the range picker gainedEndId(an id for its end input). UseEditDatewhen the browser-native date input is fine; use these when the form wants the AntD-style calendar UX.DatePicker/DateRangePickercalendar round-out: a weekday header row above each day grid (culture-abbreviated names, ordered byFirstDayOfWeek); prev/next month buttons flanking the month/year selects (PrevMonthLabel/NextMonthLabellocalize their accessible names; they disable at theMin/Maxview bounds, andDateRangePickerplaces prev on the left panel and next on the right); roving-tabindex keyboard navigation over the day grid — Arrow keys move by day/week, Home/End jump to the focused week's ends, PageUp/PageDown step a month, and the view follows focus across month edges (page-scroll suppression comes from a new lazily-importedwss-picker.js, gracefully absent without JS);DateRangePickertints the prospective span on hover while the second day is being picked (override via--wss-picker-preview-bg). A staleField="..."attribute on either picker now fails the build (the same inert[Obsolete]guard as the form controls).
Bug fixes
Table— a sortable column withTitleContentrendered the template inside the sort<button>: with interactive template content (the README-advertisedLabelTooltipcomposition) that nested a button inside a button (invalid HTML) and clicking the info icon toggled the sort, and an icon-only template left the sort button with no accessible name. The template now renders in its own clickable area beside a caret-only sort button (header clicks still sort; the button is named fromTitle, falling back to "Sort"), andLabelTooltip's trigger stops click propagation so it never triggers a clickable ancestor. Also, changingUseStyledCheckboxat runtime no longer loses the header checkbox's indeterminate ("mixed") state across the styled/unstyled DOM swap.DateRangePicker— a typed commit made mid-pick (click one day, then type a date and press Enter) left the field displaying pending-pick state that contradicted the bound values, and a later day click resurrected the discarded pick; typed commits now finalize the field. Presets were clamped only on one side ofMin/Max, so a preset lying entirely pastMax(or beforeMin) could commit days the calendar itself disables — both endpoints now clamp into the window. Year selects (both pickers) could offer years beyondDateTime's 1–9999 range and threw an unhandled exception when picked; the offered range and the selection handler now clamp.edit-controls.js'sfocusFirstInvalidFieldDOM query substring-matched[class*=" invalid"], which over-matched an unrelated consumer class likeclass="foo invalid-hint"— it now matches the exact.invalidclass token only (the same false-positive shapeInvalidIcon.razorandEditControlBase.IsInvalidalready fixed forCssClass).JsInteropEc—edit-controls.jswas the one JS assetFormDefaults.AssetBasedidn't yet cover: in a cross-origin MFE whose host page doesn't serve/link_content/WssBlazorControls/edit-controls.js,window.WssEditControlsis undefined, andFocusFirstInvalidField(unlikeFocusById) threw instead of degrading gracefully. All three methods (FocusFirstInvalidField,FocusById,Log) are now best-effort and never throw; when the global is missing they lazilyimport()the module (honoring an optional trailingformDefaultsparameter, resolved through the sameJsModuleUrlmechanism as thewss-*.jsimports) and retry once, degrading quietly if that also fails.wss-overlay.js's Modal/Drawer body-scroll lock and focus-trap stack were module-scoped, which was fine untilFormDefaults.AssetBase(10.6.4) made it routine for two MFEs to import this module from different origin URLs — the browser instantiates a module once per distinct URL, so two "instances" could each believe they alone owned the document. An interleaved open/close across instances could leave the page permanently scroll-locked (or unlock it while a dialog from the other instance was still open), and both instances' document-level Tab/Escape/focus listeners could fight over focus. The scroll-lock counter and the trap stack are now shared viawindow.__wssOverlayScrollLock/window.__wssOverlayTraps(same pattern as the existingwindow.__wssOverlayZz-index counter) — ref-counting and topmost-trap ownership now work correctly across instances. No API change; nothing for consumers to configure.
10.6.4
New feature
FormDefaults.AssetBase— an absolute URL prefixed onto the RCL's lazywss-*.jsmodule imports (Select,Modal,Drawer,Popover,Popconfirm,DatePicker,DateRangePicker,Table). Fixes a 404 for micro-frontends embedded into a host page that doesn't serve/proxy_content/WssBlazorControls/*— the"./"-relative import specifier otherwise resolves against the host document's origin instead of the MFE's own. Unset (the default) preserves today's relative import path. Cascade it from the MFE's own root the same render-tree-scoped way asFormDefaults's other settings — not a shared JS global — so multiple MFEs composed into one page don't stomp on each other's asset base. See FormDefaults.
10.6.3
New features
Tableexpandable rows + templated headers (per the Clark Connect Vendor PO Management Figma spec):RowDetail(aRenderFragment<TItem>) adds a leading chevron column that toggles the template as a full-width row beneath its row — the nested-child-table master/detail pattern; expansion state is keyed byRowKeyidentity (survives paging/sorting, forgotten when a row leaves the data).Column.TitleContentrenders templated header content in place of the plainTitle(works in sortable headers too), enabling headers like "ESD ⓘ" composed withLabelTooltip— whoseAttributesparameter is now optional, so it works standalone outside the Edit* form controls.Tabs/Tab— underline tab strip with an optional bordered per-tab count chip (Count, the "12 Overdue" pattern). Controlled via@bind-ActiveKey(string?); aTabwithChildContentshows the active pane below the strip (propertablist/tab/tabpanelwiring), while content-less tabs act as a bare filter strip. ARIA tabs keyboard pattern with automatic activation: Arrow keys select the neighboring enabled tab (skipping disabled, wrapping) and move focus with a roving tabindex; Home/End jump to the ends. Conditionally rendered tabs keep their declared position (the Table-column collect/promote mechanism). Active chip border derives from the primary color (--wss-tabs-count-active-borderoverride knob).SearchInput— the labeled search field from the same spec: optional leading addon chip (AddonLabel/AddonContent), a per-keystroke@bind-Valueinput, and an icon-only search button;OnSearchfires with the current text on Enter and on the button. Pill-rounded ends by default via--wss-search-radius(override to square). Not a form control — for validated form text useEditString.DatePicker— the single-date sibling ofDateRangePicker(per the Clark Connect Vendor PO Management Figma spec): a text field with a calendar suffix opening a one-month calendar whose header is month/year quick-select dropdowns. Bind with@bind-Value(DateTime?, date-only); picking a day (or typing a date and pressing Enter) commits and closes; Escape and outside clicks close;Min/Maxdisable out-of-range days;Formatdrives display/parsing (defaultMM/dd/yyyy);Placeholderdefaults to "Select date". Shares thewss-picker-*calendar internals andwss-overlay.jslifecycle (viewport flip/clamp, Enter-submit suppression, focus-out close — all degrade gracefully without JS). Its card carries a hairline border + the new--wss-picker-radius-lg(8px) radius, and the focused field shows the spec's primary focus ring. See UI Kit.Selectpill variant +Prefixslot —Variant="SelectVariant.Pill"restyles the trigger as a fully-rounded outlined filter button that hugs its content ("All shipments ⌄"), and the newPrefixRenderFragmentrenders leading content (typically a decorative icon) inside the trigger in any mode/variant. The pill dropdown gains softer corners, content-driven width, roomier rows, and conveys selection by the bold/tinted row alone (checkmark suppressed); the trigger label/border/chevron/focus ring all derive from one override knob,--wss-select-pill-color(plus--wss-select-pill-border/--wss-select-pill-bg).EditSelectSearchforwardsVariant+Prefix;EditMultiSelectforwardsPrefix. Internal DOM note: the selector's value/search stack is now wrapped in awss-select-selection-wrapspan (so a prefix can sit beside it) — geometry and behavior are unchanged, but CSS/tests targeting direct-child structure inside.wss-select-selectormay need the extra level. See Pill filter variant.
10.6.2
New feature
DateRangePicker— an AntDesign-style date-range picker: a composite start → end field that opens a dropdown with an optional preset sidebar and a dual-month calendar whose headers are native month/year quick-select dropdowns. Bind with@bind-Start/@bind-End(DateTime?, date-only); picking the second day of a range (or a preset) commits and closes, a backwards pair swaps, and typed input parses byFormatthen culture, committing on Enter/blur.Presetsresolve their range at click time so relative shortcuts (e.g. "This Week") never go stale in a long-lived page.Min/Maxdisable out-of-range days and clamp presets;FirstDayOfWeekdefaults to the current culture. Not a form control — noInputBase/validation wiring. JS interop (viewport flip/clamp placement, Enter-submit suppression, focus-out close) degrades gracefully: without JS the dropdown opens below the field at the CSS default placement and stays fully clickable. New--wss-picker-*tokens carry its radii and split-border color. See UI Kit.UseStyledCheckboxapp/MFE-wide switch (shipped in this release but missed in the original changelog) —FormOptions.UseStyledCheckbox(bool?) and the render-tree-scopedFormDefaults.UseStyledCheckbox(bool?) resolve the same way asIsRequiredStarHidden/ShowFieldNameInValidation: instance → nearest enclosingFormDefaults→ the process-wideFormOptions.DefaultUseStyledCheckboxstatic (defaultfalse).EditBool.UseStyledCheckbox(shipped 10.6.0) changed frombooltobool?so it participates in this chain instead of being per-control only — existingUseStyledCheckbox="true"/"false"markup is unaffected, only an unset control now inherits the app-wide default instead of always rendering the native checkbox. Two more controls gained the same opt-in:EditCheckedStringList.UseStyledCheckbox/EditCheckedEnumList.UseStyledCheckbox(bool?) apply the custom-drawn box to every option's checkbox, and the UI-kitTable.UseStyledCheckbox(bool?) applies it to the header/row selection checkboxes, including the indeterminate "mixed" glyph —Tablehas noFormOptionsof its own, so it resolves through a cascadedFormDefaultsthen the static only. SeeFormDefaultsand Custom-Styled Checkbox.- Styled checkbox visual restyle (also shipped in this release): the checked glyph is now the exact antd check vector via a themeable CSS mask (was a generic rotated-border "L"), the unchecked border fallback moved from
#cccto#d9d9d9(antdcolorBorder), theTablevariant's box corner radius moved from 2px to 4px to matchEditBool's, and the indeterminate "mixed" state is now an unfilled box with a centered primary-colored square (was a filled box with a white dash) — also fixing a CSS comment bug (/* ... edit-*/ ...) that had been closing theTablebox-wrapper rule early and letting the box escape its cell. The label row forEditBooland eachEditChecked*option is now a flex row (align-items: center, 8px gap) instead of relying on inline whitespace. These restyles apply automatically to every consumer already usingUseStyledCheckbox="true"since 10.6.0 — there is no separate opt-in for the new look.
10.6.0
New feature
EditBool.UseStyledCheckbox(defaultfalse) — opt-in custom-drawn checkbox. No current browser (Chromium or Safari/WebKit) honorsborder-radiuson a native<input type="checkbox">onceaccent-coloris set, so there was previously no way to get a shaped checkbox out ofEditBool. When enabled, the real<input>stays in the DOM (focusable, keyboard-operable, full native semantics) but is visually hidden; a sibling element draws the box, checked fill, checkmark, and focus ring via the plain adjacent-sibling (+) CSS selector (not:has(), so it still works on older Safari). Existing checkboxes are pixel-identical — nothing changes unless you opt in. See Custom-Styled Checkbox.
Bug fixes
width: 100%(or any percentage width) on the editor element ofEditString/EditNumber/EditDate/EditTextAreanow works. Previously the.edit-input-with-iconwrapper shrink-wrapped to the editor's intrinsic size, which made a percentage width on the editor circular per the CSS sizing spec — it silently resolved toautoand the input stayed at its default size. The wrapper is now a flex row that stretches to the control column (so percentages resolve against it), and the red-X invalid icon overlays the editor's trailing edge via a negative flex-item margin instead of absolute positioning — stilldir="rtl"-correct and still immune to being wrapped onto its own line under a width squeeze.EditFile: bareAllowedExtensionsentries without a leading dot ("pdf") are now normalized instead of silently rejecting every file (and emitting an invalidacceptattribute); the label'sforno longer dangles at a missing input once theMaxFilescap unmounts the drop zone; the upload icon now turns red forEditContextvalidation failures (not just client-side rejections); the read-only file list is programmatically associated with the field label. Re-selecting a file that's already added (same name, size, and last-modified) is now skipped and reported — via the newDuplicateFileMessageFormatparameter — instead of occupying a secondMaxFiles/MaxTotalBytesslot for the same logical file.- List-bound controls (
EditMultiSelect,EditFile,EditCheckedStringList,EditCheckedEnumList): aclassattribute is now captured and merged into the rendered field instead of throwing at render time as an unmatched parameter — onto the select engine (EditMultiSelect, matchingEditSelectSearch), the drop zone and read-only file list (EditFile), and every checkbox (EditChecked*). These controls also now emit the sameEditContextfield-state classes as the scalar controls (modified/valid/invalidby default, honoring a customFieldCssClassProvider) instead of onlyinvalid.EditRadionow applies the consumer'sclassto its group fieldset in edit mode (previously it appeared only in the read-only view). EditSelectSearch/EditMultiSelect/Select: a disabled multi-select no longer renders focusable tag-remove buttons that silently no-op; Space now opens a closed non-searchable select (ARIA combobox pattern) — searchable inputs keep Space for typing.EditDisplay: the cascadedFormOptionswas declared but ignored — form-wideIsLabelHiddennow applies, and the newIsLabelHidden/IdPrefixparameters plusFormGroupOptions.Nameid composition bring it in line with the bound controls (twoEditDisplays with the same label in different form groups no longer collide on id).- Styled checkbox (
UseStyledCheckbox): the box background is nowvar(--color-bg, #fff)instead of hardcoded white, so dark-theme consumers have an override hook. Default rendering unchanged. - With
--color-primaryunset, the checked styled-checkbox fill and theEditFiledrop-zone hover border fell back to a stray teal (#277c6c) while the focus rings fell back to blue (#0066cc) — two different colors for one interactive role. All three now share a single--edit-color-primarytoken (blue fallback). Note the token is resolved at:root, like every other bridging token in both stylesheets — set--color-primaryat:rootfor it to be picked up (a value scoped to a nested container is not seen, which previously happened to work for these two rules only). Table: the header checkbox's mixed (indeterminate) state is re-applied afterSelectableis toggled off and back on while a partial selection exists — the recreated checkbox used to come back plain-unchecked.Modal/Drawer: Escape-to-close no longer goes dead when focus is silently dropped to<body>— e.g. the focused default OK button becoming disabled viaConfirmLoading, or a conditionally-rendered focused element unmounting. The focus trap now pulls focus back into the panel and re-targets the Escape at it.JsInteropEc.FocusByIdnow honors its documented best-effort contract (a no-op when JS is unavailable) instead of throwing from a prerenderIJSRuntime.- Theming: scoped token overrides now cascade into derived states.
--wss-color-primary-hover,--wss-primary-shadow,--wss-error-shadow, and--edit-focus-ringused to be derived from their base token at:root, so overriding--wss-color-primary/--edit-color-primary/--wss-color-erroron a nested container (a theme class, an MFE root) changed the base color but left hover borders, focus shadows, and focus rings at the default blue/red. These are now derived at each usage site — a scoped base-token override re-themes the derived states too. All four remain overridable as before (a directly-set value wins over the derivation), the generic--color-primary-hoverbridge is preserved (and now also works scoped, since it too is consulted at the element); computed defaults are unchanged.--wss-color-primary-active(never consumed by any rule) was removed. - UI-kit components accept
class/style/ arbitrary attributes.Alert,Skeleton,Pagination,Modal,Drawer,Popover,Popconfirm,Table, andEditDisplaypreviously threwInvalidOperationExceptionon any unmatched attribute. They now capture unmatched attributes onto their root element (Modal/Drawer: the dialog panel;Popover/Popconfirm: the trigger wrapper):classandstylemerge with the component's own, everything else (data-*,id, ...) is splatted verbatim. Caveat: parameter matching is case-insensitive, so an attribute sharing a parameter's name binds to the parameter instead — e.g.title="..."onModal/Drawer/Popover/Popconfirmsets theirTitle, onSkeletonit's a build error (Skeleton.Titleis abool), andclassonEditDisplaysets itsClass(same knob).
10.5.1
Bug fixes
EditControlListBase<TItem>.ValueExpressionis now[EditorRequired]— a missing/incomplete@bind-Value(e.g. one-wayValue="..."with no binding) is now a build-timeRZ2012diagnostic instead of only the runtimeInvalidOperationExceptioneach list-bound control'sOnInitializedalready threw.- Fixed
.edit-icon-invalid(the validation-error icon overlaid onEditString/EditNumber/EditDate/EditTextArea) wrapping onto its own line under a width squeeze. It's now absolutely positioned (inset-inline-end, so it still overlays the correct edge underdir="rtl") instead of relying on a negative margin to pull it over the input.
Demo
- Added a "Comparison" view to the demo app that renders the same field via WssBlazorControls, hand-rolled Blazor, and React + Ant Design (with and without full accessibility parity) side by side, with reasoned notes on the accessibility and AI-authoring trade-offs of each.
10.5.0
Field is gone — @bind-Value alone is now enough on every control
- Every
Edit*control previously required both@bind-Value="model.Property"andField="@(() => model.Property)"— the second was pure duplication. Razor's@bind-Valuedirective already populates aValueExpression(the same mechanism Microsoft's ownInputText/InputNumberrely on for validation and labeling without a second parameter); the library just wasn't using it. All 17 controls now resolve their accessor fromValueExpressioninstead. - This covers the scalar controls (
EditString,EditNumber,EditDate,EditBool,EditBoolNullRadio,EditSelectEnum,EditSelectString,EditSelect,EditSelectSearch,EditRadio,EditRadioEnum,EditRadioString,EditTextArea) and the list-bound controls (EditCheckedStringList,EditCheckedEnumList,EditFile,EditMultiSelect). The list-bound controls aren'tInputBase-derived, soEditControlListBase<TItem>gained its ownValueExpressionparameter — the compiler synthesizes it from@bind-Valuefor any component with theValue/ValueChanged/ValueExpressionparameter shape, not justInputBasesubclasses. - Migration: delete every
Field="@(() => model.Property)"attribute —@bind-Value="model.Property"alone is sufficient.Fieldstill exists on every control as an inert,[Obsolete(error: true)]-decorated parameter purely so a leftoverField=attribute is a build error (CS0619: 'EditXxx.Field' is obsolete: ...) instead of a silent runtime failure — Blazor otherwise validates unmatched component parameters atSetParametersAsynctime, not compile time, so a stale attribute would build cleanly and only throw the first time that component renders. The error message tells you exactly what to remove; this stub carries no other behavior and is planned for physical removal in a future major version.
Drops net8.0/net9.0 — the package now targets net10.0 only
WssBlazorControlsandWssBlazorControls.Demoare single-targeted atnet10.0; both previously multi-targetednet8.0;net9.0;net10.0. If your app targets net8.0 or net9.0, this version will not install — stay on10.4.xuntil you upgrade the app to net10.0.- CI now installs and runs against a single .NET SDK instead of three; the bUnit suite runs once instead of once per TFM.
- No API or behavioral changes for net10.0 consumers — this is purely a supported-platform reduction.
10.4.0
A library-wide hardening release: six adversarial review rounds (documented across this release's commit history) spanning correctness, accessibility, performance (measured), globalization/RTL, plus trimming/AOT support, touch support, and validation-stack (FluentValidation) support.
Correctness
EditRadio.IsDisabledactually disables itsInputRadiochildren now (a nestedfieldset[disabled]—InputRadioGrouprenders no element, so the old attribute vanished). All three radio controls forwardValueExpressionto their inner group so it notifies/styles the real model field.EditRadio.Fieldis nowrequiredlike every sibling.- A null bound
List<T>no longer crashesEditFile's render or the first checkbox toggle in the checked lists — null is treated as empty and the list is created on first add. EditFilenow buffers each selected file's bytes into memory at pick time instead of holding the framework'sIBrowserFile. Previously, choosing files in more than one batch (or hittingMaxFiles, which unmounts the<InputFile>) left every earlier file throwing onOpenReadStream()— Blazor wipes the browser file map on each change event. Buffered files stay readable for the life of the list, so multi-batch accumulation and the per-file remove buttons behave as the UI implies. A barefile.OpenReadStream()(no size argument) now works regardless of file size — the bytes are already in memory, bounded byMaxFileSizeBytes. Trade-off: selected files occupy memory until cleared, and on Blazor Server the bytes cross the circuit at selection; the aggregate is bounded by the newMaxTotalBytes(default 100 MB across all selected files,0= unlimited), withMaxFileSizeBytes/MaxFilesbounding per-file size and count.EditNumberbinds onchangeinstead ofoninput(browsers reporttype=numberas""mid-typing, flashing "must be a number" on partial input like-or3.), and formats the unsigned/byte types invariantly to match the parse side.EditRadioString: an options list legitimately containing"Other"no longer collides with the built-in other-option sentinel (which silently replaced the model value with the empty other-text). The internal sentinel is now also uniquified against the options list, so no option string whatsoever can collide with it.EditRadioString's "Other" free-text box now honorsIsDisabled— with the Other option selected it used to stay editable (writing to the model per keystroke) while every radio was disabled. MatchesEditRadioEnum.- The list-bound controls re-derive their
FieldIdentifierwhen the model/EditContextis swapped, so validation targets the new model instead of dead state; they also work outside anEditForm(no moreFieldValidationDisplayNRE). - The scalar controls and
EditRadioalso render standalone (no surroundingEditForm) again —IsInvalidnow guards a nullEditContextinstead of dereferencing it, matching the list base. EditSelectStringrenders a leading empty option (NullOptionText) — a null value used to display the first option as selected while the model stayed null. Selecting that blank now clears the model tonull/default(astring?could previously never return from""to null; a non-stringTValuelikeEditSelectString<int?>reported "not valid" instead of clearing). The blank is now opt-out — setNullOptionText="@null"to drop it (e.g. a required field) — and is auto-suppressed for a non-nullable value type (EditSelectString<int>), where a blank would only map to a spuriousdefault.- Select parsing and formatting (
EditSelect/EditSelectString) are invariant-culture, matchingEditNumber—"1.5"no longer parses as15under de-DE, and a bounddouble1.5now renders asvalue="1.5"(was"1,5", which matched no<option>and left the select visually unselected). Table: fully-equal duplicate rows no longer crash the render (de-duplicated row keys); newRowKeyparameter (e.g.x => x.Id) gives rows identity, and selection is key-based. Descending sort survivesint.MinValuefrom subtraction comparators; a column whose parameters never change (title-only spacer) no longer silently vanishes.- Toast auto-dismiss durations are capped below
Task.Delay's ~24.8-day limit instead of throwing into a fire-and-forget task. - Performance/leak hardening:
FieldValidationDisplaymemoizes its per-field value-type reflection (a large form re-reflected every field on every keystroke); the list-bound controls unregister their old field on a model/EditContextswap (and on dispose) so the validation summary's field list can't accumulate dead entries; theEnumHelpersid cache stops calling the lock-acquiringCountonce saturated instead of on every subsequent call.
Overlays
- One Escape no longer closes the whole overlay stack: panels stop keydown propagation, and the Select input does so only while its dropdown is open (so Escape still reaches an enclosing Modal once the dropdown is closed).
- Overlays stack in open order via a JS z-index counter (Modal-vs-Drawer DOM-order ties and Popover-above-a-later-Modal are gone); an open Select sits above its own backdrop (clicking your own search input/tags/clear no longer closes the dropdown); toasts are the always-on-top layer.
- Modal/Drawer: neither a close→reopen race nor disposal while the open animation is in flight can leak the body-scroll lock / document listeners now (a
_disposedguard releases the late focus-trap handle instead of orphaning it); the Modal only dismisses when a mask click both starts and ends on the mask, so a drag crossing the mask/panel boundary in either direction keeps it open, and a press released outside the window can't leave a stale flag that closes a later gesture; the focus trap is document-level and survives focus escaping the panel (nested overlays hand it to the innermost dialog); title-less non-closable dialogs render no empty header and fall back toaria-label="Dialog"/"Drawer". Alert's close button hides the alert itself (OnCloseis a notification, not a requirement).
Select engine
- Enter picks the highlighted option without triggering the enclosing form's implicit submission; arrow keys no longer jump the caret; Enter on a closed combobox opens it; opening highlights the current selection (scrolled into view) and skips disabled options; Tab-away closes the dropdown (its invisible backdrop used to swallow the next click).
Options/Valuesare now explicitly immutable parameters (reference-guarded rebuilds — a parent re-render used to re-copy/re-filter the whole option set per keystroke). Reassign a new instance to refresh.- Tags mode prunes a user-created tag from the options once deselected;
EditMultiSelectthrows a clear exception onMode="Single"(selections silently reverted — useEditSelectSearch).
Accessibility
- Hidden labels (
IsLabelHidden) render a visually-hidden label/legend so controls keep an accessible name — includingEditBool, whose edit branch renders its own label and had been shipping an unnamed checkbox in the hidden-label case; checked-list fieldsets exposerole=group+aria-required/-invalid; each validation message renders in its own element (no more run-together text); dynamicLabelchanges propagate toEditBooland validation messages;label[for]no longer references the non-labelable read-only div;LabelTooltipdismisses on Escape (WCAG 1.4.13) and now stops that Escape from also closing an enclosing Modal/overlay (one Escape, one layer); pagers get distinct landmark names when a Table renders two; the select-all checkbox announces its per-page scope. IsInvalidis read fromEditContextmessages instead of substring-matching"invalid"inCssClass(a consumer class likeinvalid-style-fixrendered a permanent red X).InvalidIconnow takesIsInvalid(bool) instead ofCssClass.[Display(Name = …)]is honored for labels (after[DisplayName]/[EnumDisplayName]), keeping labels consistent with DataAnnotations' own messages — and now resolves throughGetName(), so a localized[Display(Name = …, ResourceType = …)]yields the localized text instead of the raw resource key (both for control labels and enum display names).EditFile: removing a file with the keyboard keeps focus on the control (the file that shifted into the slot, else the new last file, else the drop zone) instead of dropping focus to<body>; a disabled drop zone no longer shows the drag-hover highlight for a drop it will refuse.Popover/Popconfirm: the consumer's own trigger element (typically a<button>inChildContent) is the trigger now — the wrapper span no longer rendersrole="button"/tabindex="0"around it, which nested a button inside a button (two tab stops, invalid ARIA). JS mirrorsaria-haspopup/aria-expandedonto the child and restores focus to it on close; content with nothing focusable (plain text/icon) gets the wrapper promoted to the button role as before. Without JS, a button child still opens/closes via its bubbled click — only the popup ARIA and the plain-content keyboard path need the runtime.
Validation stacks (FluentValidation support)
- New
FormOptions.RequiredResolver(Func<FieldIdentifier, bool>?): a form-level source of required-ness for validation stacks that don't use[Required](e.g. FluentValidation). Fields the resolver marks required get the star andaria-requiredexactly as if attributed. See the new Validation stacks section for the FluentValidation bridge snippet. IsRequiredis now three-state (bool?) on all controls andFormLabel: unset defers to the attribute/resolver;trueforces required (unchanged);falsenow forces optional — previously it was a no-op, so aRequiredAttribute-derived conditional (RequiredIf) whose condition was off showed a permanent star with no way to remove it. Existing markup (IsRequired="true"or a boundbool) compiles unchanged.- The star and
aria-requiredare now computed by one shared resolver (EditControlInit.IsRequired), so the two signals can never disagree;FieldValidationDisplaydropped an unused required-ness field, andEditControlInit.Initno longer returns a redundantIsRequiredtuple member (its value was always recomputed and overwritten).
API changes to note when upgrading
InvalidIcon.CssClass→InvalidIcon.IsInvalid(bool);LabelTooltip.TooltipChangedremoved (never invoked);ValidationView.Modelremoved (never read);EditRadio.Fieldis now required;EditSelectStringgains a leading empty option (opt out withNullOptionText="@null"; its type is nowstring?) and selecting the blank now writesnull/defaultinstead of"";EditNumbercommits on change (not per keystroke);Alertself-dismisses on close;Select/Tablecollection parameters are immutable-by-reference.- New:
Table.RowKey,Pagination.AriaLabel,EditSelect.ReadOnlyText,EditSelectString.NullOptionText,FormLabel.IsForLabelable. Popover/Popconfirmtrigger contract: pass a focusable element (typically a<button>) as the trigger content — it is the single tab stop and carries the popup ARIA. Plain-text trigger content still works but is keyboard-accessible only when JS is available. The trigger child is re-resolved on every sync, so conditionally-swapped trigger content (@if (busy) { spinner } else { button }) keeps its ARIA and close-focus; focusable non-button children (a[tabindex]span, an anchor) get Enter/Space activation, whileinput/select/textareachildren keep their editing semantics (a<button>remains the recommended trigger).- New
FormDefaultscomponent: render-tree-scoped defaults forIsRequiredStarHidden/ShowFieldNameInValidation— wrap an app or MFE root to configure its forms without touching the process-wideFormOptionsstatics (which are shared across circuits on Blazor Server). Resolution:FormOptionsinstance value → cascadedFormDefaults→ static default. Nested instances chain per property (an unset inner setting falls through to the enclosingFormDefaultsbefore the static), so host-page defaults and MFE-root overrides compose. Non-breaking; the statics remain as the final fallback.
Packaging & repo
- The packages now ship XML docs (IntelliSense), SourceLink +
.snupkgsymbols, deterministic CI builds, package validation, and an SPDXMITlicense expression; warnings are errors. GitHub Actions CI builds the solution, runs the bUnit suite across net8/net9/net10, packs both packages, and runs the Playwright E2E suite. The E2E project is now part ofFormTesting.sln. The Quick Start documents the requirededit-controls.jsscript tag.
Trimming / WASM AOT
- The package is now trim- and AOT-compatible (
IsTrimmable/IsAotCompatible+ warning-clean trim/AOT/single-file analyzers, enforced as errors). A default Blazor WASM publish trims the library. See the new Trimming and AOT section above for what survives and the consumer caveats. - Reflection sites were made trim-safe rather than suppressed wholesale: enum option builders use
Enum.GetValuesAsUnderlyingType(AOT-safe, noRequiresDynamicCode);PropertyColumn's comparability probe dropsMakeGenericType(the one lost corner —Nullable<T>whoseTimplements onlyIComparable<T>— degrades to non-sortable,SortByunaffected); the generic value-bearing controls annotateTwith[DynamicallyAccessedMembers(All)]exactly like the framework'sInputNumber/InputSelect; the two by-name lookups (validation value-type, enum field) carry justified suppressions with graceful fallbacks. - Verified end-to-end: the full Playwright e2e suite passes against a
TrimMode=fullpublish of the demo host (labels, required stars, length/range message rewrites,[EnumDisplayName]options, tooltips, visual baselines).
Round-3 review fixes (post-hardening evaluation)
Popover/Popconfirmre-resolve their trigger child on every ARIA sync, so conditionally-swapped trigger content (@if (busy) { spinner } else { button }) no longer strandsaria-haspopup/aria-expandedon a detached element or drops close-focus to<body>, and a wrapper promoted around plain content is demoted again when a real button appears (no more button-in-button after a swap). Focusable non-button trigger children ([tabindex]spans, anchors) gained Enter/Space activation; aDisabledPopconfirm marks an interactive childaria-disabled. The per-render JS interop call is now skipped unless(open, disabled)changed — a Popconfirm-per-row Table no longer pays one SignalR round trip per row per re-render on Blazor Server (afocusinlistener repairs ARIA for children swapped while idle).EditFile: newMaxTotalBytesparameter (default 100 MB,0= unlimited) bounds the aggregate buffered footprint across all selected files — buffering at pick time otherwise let a single large multi-file drop allocate unbounded server memory under the defaultMaxFiles = 0.- Date-typed selects round-trip:
EditSelect<DateOnly>/<DateTime>/<DateTimeOffset>/<TimeOnly>now format to the ISO forms option values are authored in, so picking an option no longer immediately loses the visual selection while the model holds the value. Author your option values in the matching canonical form —DateOnly:2026-06-15·DateTime:2026-06-15T14:30:45·DateTimeOffset:2026-06-15T14:30:45-05:00(UTC is+00:00, notZ) ·TimeOnly:14:30:45. Shorter authored forms (2026-06-15for aDateTime,14:30for aTimeOnly) still parse on pick, but the formatted value won't visually re-match them. EditSelectStringwith a suppressed blank option (non-nullable value types, orNullOptionText="@null") renders a hidden placeholder when the current value matches no option — an untouched default (e.g.0) displays blank instead of silently showing the first option while the model holds something else.- The open
Select's stacking z-index is mirrored into its C#-ownedstyle, so a re-render that changesWidthmid-open no longer clobbers it and drops the selector below its own backdrop (which made clicks on the select's own input close the dropdown). - Two controls bound to the same property now share their validation-summary registration safely: disposing one (e.g. closing an edit modal that duplicates a page field) keeps the surviving control's messages — registrations are owner-tracked and dropped only by the last registrant.
- Nested
FormDefaultschain per property instead of the inner instance shadowing the outer entirely (see theFormDefaultsnote above). Select,Modal,Drawer,Popover, andPopconfirmno longer strand a JS module reference when disposed while their module import is in flight (the same raceTablewas already guarded against).
Round-4 review fixes (post-round-3 evaluation)
EditSelect<DateTimeOffset>now formats whole-second values without the.0000000fraction (2026-06-15T14:30:45-05:00), so authored option values actually match and the visual selection survives a pick; sub-second values keep the full round-trip form. The canonical authored forms per date type are documented in the round-3 entry above.Popover/Popconfirmtrigger ARIA: a consumer-ownedaria-disabledon the trigger child is no longer removed when the component'sDisabledround-trips; when the resolved trigger child changes identity while the old element stays in the DOM, the popup ARIA is stripped off the old element instead of two elements announcing the popup.EditCheckedStringList/EditCheckedEnumListfieldsets no longer emitaria-required/aria-invalid/aria-errormessage— ARIA 1.2 doesn't support them onrole="group"(assistive tech ignored them; checkers flag them). Required state remains on the legend star and the validation message, invalid state on each checkbox'saria-invalid. The radio fieldsets (role="radiogroup", where these attributes are valid) are unchanged.
Round-5 fixes (trim verification, globalization/RTL sweep, measured performance pass)
- RTL support: the direction-sensitive Select geometry (arrow/clear anchoring, search inset, tag/placeholder spacing) and the form controls' trailing invalid-icon/required-star spacing now use CSS logical properties — under
dir="rtl"tags no longer render beneath the opaque clear button (where a tap cleared the entire selection) and typed search text no longer starts under the arrow. Rendering under LTR is byte-identical. Notification position,DrawerPlacementleft/right, and Table alignment deliberately keep physical semantics. - Localization: new label parameters with unchanged English defaults —
PaginationPreviousPageLabel/NextPageLabel/PageLabelFormat;Select/EditSelectSearch/EditMultiSelectRemoveItemLabelFormat/ClearSelectionLabel/ClearSelectionsLabel/ListboxLabel— so localized apps can localize what screen readers hear.EditFile's five upload-error messages are likewise localizable via*MessageFormatparameters (UnsupportedFormat,FileTooLarge,FileReadFailed,MaxFiles,TotalSize); the pluralizing formats receive a pre-pluralized English unit argument that localized formats can ignore. - Culture correctness: the
[Range]one-sided message rewrite ("Cannot exceed 100") now works after a runtime culture switch and in mixed-culture Blazor Server processes — the type-min/max sentinels are resolved per current culture instead of being frozen at first touch. - Performance:
Tableno longer rebuilds its row keys and rescans selection state on every parent re-render (the cost was O(rows) with boxing, per keystroke in any sibling input for unpaged tables);FormLabel/FieldValidationDisplayskip label/attribute re-derivation — and stop re-invokingFormOptions.RequiredResolver— unless their inputs actually changed, honoring the resolver's documented "not on every keystroke" contract;EditMultiSelect's read-only label join is O(selected) via a value→label lookup. Measured reality check: for very large unpaged tables the remaining cost is Blazor re-rendering the row fragment itself — preferPageSizeor the server-side paging composition at that scale. - Verified this round: the full Playwright suite passes against a
TrimMode=fullpublish; Select's dropdown virtualization confirmed (20 DOM rows at 1,000 options).
Round-6 fixes (pre-release regression hunt on the round-4/5 fixes)
- The required star and
aria-requirednow share one computation site: each control resolves its required-ness once (IsRequiredparameter →[Required]→FormOptions.RequiredResolver) and passes the resolved value to its label, so a conditional resolver that reads model state moves both signals together on re-render (the round-5 label caching had letaria-requiredupdate while the star stayed frozen). - The
[Range]sentinel check compares against the current culture's actual formatting on every call (a per-culture-name cache could serve stale sentinels to same-name cultures with customized number formats). LabelTooltipresolves its tooltip text once per input change instead of scanning the attribute list twice per render.
10.3.0
New: EditFile — multi-file upload control
EditFileis a new form control that binds aList<IBrowserFile>via the standardValue/ValueChanged/Fieldpattern, integrating withEditContextvalidation like every otherEdit*control.- Supports drag-and-drop and click-to-browse. An invisible
<InputFile>overlay covers the entire drop zone so both interactions work natively without extra JS. - Multiple files are supported. The drop zone stays visible until an optional
MaxFilescap is reached; files already chosen appear as a dismissible list below it (hover to reveal the remove button per file). AllowedExtensions(e.g.".pdf",".xlsx") filters by extension;MaxFileSizeBytescaps individual file size (default 10 MB). Validation errors from either check are shown inline below the drop zone.- The drop zone border turns red when there's a validation error from the format/size check or when the field fails
EditContextvalidation; the upload icon switches to its error (red) variant to match. - Read-only mode shows the selected filenames with a paperclip icon; empty renders a blank
ReadOnlyValueconsistent with the other controls. - Styled to match the Hatch / Spot drop-zone look: dashed
#b7b7b7border,#f3f3f3background, primary-color hover border. Tokens bridge to--color-primaryand--color-dangerso the control follows the consumer's theme. - Adds four inline-SVG icon classes to
edit-controls.css:.edit-icon-upload,.edit-icon-upload-error,.edit-icon-paperclip,.edit-icon-delete.
Table — robust dynamic columns + graceful sort
- Columns may now be conditionally rendered (
@if). The Table re-collects its columns in document order on each render, so a hidden column drops out and a re-shown one returns to its declared position — previously a removed column left a stale header and cells behind, and re-showing it produced a duplicate. Hiding the column that drives the active sort now clears the sort so the indicator and the row order can't disagree. - A
SortablePropertyColumnwhose property type isn't comparable no longer throws on the first header click (which on Blazor Server tore down the circuit) — the header simply isn't made sortable. Supply aSortBycomparison to sort any type. - A sortable column declared without a
Titlenow gives its sort<button>anaria-label="Sort", so it isn't an unnamed button for screen-reader users.
Accessibility
Skeletonannounces its loading state to screen readers:role="status"+aria-busy="true"and a visually-hiddenLoadingText(default"Loading"); the placeholder bars arearia-hidden. New.wss-sr-onlyutility class.- Toast (
Message) andNotificationcontainers route each toast by severity into two always-present live regions — a politerole="status"region and an assertiverole="alert"region — instead of flipping a single shared region's politeness when an error arrives (a change screen readers don't reliably re-announce, which could swallow the error). The regions aredisplay:contents, so the on-screen layout is unchanged (errors group below the polite toasts).
10.2.0
Headline release: debuts the dependency-free AntDesign-style UI-kit controls (Select, Alert, Modal, Drawer, Table, Pagination, Popover / Popconfirm, Skeleton, toasts) and the searchable form selects (EditSelectSearch / EditMultiSelect), alongside a library-wide accessibility & architecture overhaul (the EditControlBase refactor). Adds Table column sorting and configurable pager placement. Includes one breaking dependency change — see below.
New: Table column sorting
- Columns can now sort. Set
Sortable="true"on aPropertyColumn(the comparison is derived from itsPropertyviaComparer<T>.Default), or supply aSortBycomparison on anyColumnfor custom / template columns. Clicking a sortable header cycles ascending → descending → unsorted (restoring the originalDataSourceorder); the sort is stable (ties keep their original order). Headers exposearia-sort(ascending/descending/none) and a keyboard-focusable<button>so the feature is screen-reader- and keyboard-accessible. Sorting resets to page 1 and survives aDataSourceswap.
Table / Pagination polish
- The table pager is now configurable:
PagerPosition="Top | Bottom | Both"(defaultBottom) places it above, below, or both above and below the table, andPagerAlign="Left | Center | Right"(defaultRight, matching AntD) aligns it horizontally. WhenBoth, the two pagers stay synced to the same page. - The pager buttons now hold a consistent 32px square via a
min-heightfloor, so an aggressive consumer reset such asbutton { max-height: fit-content }can no longer collapse them to content height (which made the icon-only prev/next buttons render shorter than the numbered ones). - The
Tablenow renders its grid and pager inside a single root element, so a parent's flex/gridgapdoesn't stack on top of the pager's margin and inflate the space between the table and its pager.
Accessibility, theming & performance (audit follow-up)
- Grouped controls now surface validation state. The radio controls (
EditRadio,EditRadioEnum,EditRadioString,EditBoolNullRadio) exposearia-invalid/aria-required/aria-describedbyon arole="radiogroup"<fieldset>named by its legend (previously splatted onto<InputRadioGroup>, which renders no element — so they didn't reliably appear). The checkbox lists (EditCheckedStringList,EditCheckedEnumList) mark each checkboxaria-invalid(they had none). And because the list controls areComponentBase(notInputBase), they now subscribe to theEditContextso their invalid state updates live on validation — matching the scalar controls. This completes "aria-invalidon every editable control". aria-describedbyno longer dangles — it references only thedesc-/tooltip-ids that actually render, and is resolved once per control rather than re-interpolated on every render.aria-errormessageis emitted only while the field is invalid (per the ARIA spec).- Form controls are self-sufficient out of the box.
edit-controls.cssnow ships a:focus-visiblering for the editable elements (WCAG 2.4.7 — no longer dependent on the browser default the consumer may have reset) and an.invalidborder, so keyboard focus and the validation error state are visible without the consumer supplying their own styles. The validation X icon and the tooltip info icon usecurrentColordriven by--color-danger/--color-text, so they follow the consumer theme. wss-controls.css: theSelectsizing now uses the existing--wss-*tokens (overriding a token rescales the control as intended), and the classes the markup referenced but the stylesheet never defined (wss-popconfirm-title,wss-table-caption,wss-select-selection-item-rest, …) are now declared.- Fewer per-render allocations.
Selectcaches its visible tags andTablecaches the current page (it was materializing the page twice per render).Tablenow treatsDataSource/SelectedItemsas immutable parameters (reference-guarded) — reassign them to refresh rather than mutating in place. - Removed the unused
ReadOnlyValue.IsRequiredparameter (it wasrequiredbut never rendered). - Nullable enum selects can represent null.
EditSelectEnum<TEnum?>now renders a leading empty/placeholder option (label via the newNullOptionTextparameter) so a null value shows blank instead of silently displaying the first member, and the user can clear the field. Non-nullable enums are unchanged. - More ARIA correctness. All bool-bound ARIA booleans (
aria-expanded/aria-hidden/aria-disabled) now render lowercase"true"/"false";Alertannounces by severity (role/aria-live: error = assertive, otherwise polite) instead of alwaysrole="alert"; the radio<fieldset>itself is therole="radiogroup"(no nested double-group) with its id gated to edit mode so it doesn't collide with the read-only value; read-onlyaria-labelledbyis suppressed when the label is hidden; theSelectgets a focus ring before it opens; andEscapeclosesPopover/Popconfirmfrom inside the panel. - Correctness fixes.
Selectnow shows the selected label and clear button even when a single value equalsdefault(TValue)(e.g. a non-nullable enum's0member — previously mis-rendered as the empty placeholder);ValidationViewsummary links now target each control's actual id, honoringIdPrefix/ an explicitId(the resolved id is captured at field registration) instead of a recomputed guess; the checkbox lists no longer throw in read-only mode when the bound list isnull, and sanitize their read-only per-option ids viaToId(); a disabledPopconfirmtrigger is nowaria-disabledand removed from the tab order. - More correctness & a11y fixes.
EditRadioStringnow follows an externally-changed value (form reset, async-loaded model, programmatic set) instead of caching the selection once — and a custom initial value correctly resolves to the "Other" radio with its text box pre-filled;EditRadioEnum's "Other" free-text input gained an accessible name (aria-label), matching itsEditRadioStringsibling; theSelectclear button is now revealed on keyboard focus (:focus-within), not only on hover, so a keyboard user can see the control they've tabbed to; theTable's "select all" checkbox enters the nativeindeterminate(mixed) state when only some rows on the page are selected, so screen readers announce the partial selection; and the length-attribute helper takes the tighter (smaller) upper bound when both[StringLength]and[MaxLength]apply. - Checkbox-list validation links resolve.
EditCheckedStringList/EditCheckedEnumListnow render their resolved id on the<fieldset>in edit mode (gated like the radio groups), so aValidationViewsummary link for one of these fields actually jumps to the control — their checkboxes/label/error elements all carry decorated ids, so the bare id previously had nowhere to land, leaving the link dangling. - Visual & robustness fixes.
Paginationclamps an out-of-rangeCurrentto the valid range, so Previous/Next enable correctly instead of looking clickable but doing nothing; a longPopconfirmtitle now wraps inside the panel instead of overflowing it; and the loadingSkeletonshows a flat fill underprefers-reduced-motionrather than a frozen, off-centre shimmer band. - Overlay focus-trap & scroll-lock hardening. The Modal / Drawer focus trap no longer lets Shift+Tab escape when focus is on the panel itself (e.g. after clicking an empty area of the body) — focus is pulled back into the dialog. The body-scroll lock is now ref-counted, so stacked overlays don't unlock the page when the first-opened one closes, and the focus handle's disposal is idempotent.
New: AntDesign-style controls (ported from Standalone.Controls)
- Form selects:
EditSelectSearch<T>(searchable single-select) andEditMultiSelect<T>(multiple / tags, bindsList<T>) — fullEdit*controls (validation, label, read-only,FormOptions) backed by a new dependency-free, virtualized dropdown engine (Select<T>). They sit alongside the existingEditSelect/EditSelectEnum/EditSelectString, which are unchanged. - UI kit (non-form):
Select<T>,Alert,Skeleton,Popover,Pagination,Modal,Drawer,Popconfirm,Table<TItem>(+Column/PropertyColumn/ActionColumn), and toasts/notifications in two flavors — scoped/Server-safe (IMessageService/INotificationServiceviaAddWssControlsToasts()+MessageContainer/NotificationContainer) and registration-free static for WASM (WasmMessageService/WasmNotificationService+ their containers).Icon,Button,Checkbox, andTagwere intentionally excluded. - New stylesheet: these controls use the
wss-class prefix and--wss-*theme tokens shipped inwss-controls.css. Add a second link alongsideedit-controls.css:
Tokens default to the AntDesign 4.x look and bridge to your existing<link href="_content/WssBlazorControls/wss-controls.css" rel="stylesheet" />--color-*/--border-colorwhere present. The Select keyboard helper ships as an RCL JS module at_content/WssBlazorControls/wss-select.js(auto-imported, degrades gracefully). - No service registration required (consistent with the rest of the library).
Accessibility & correctness (library audit)
- Modal / Drawer: trap focus while open, restore focus to the trigger on close, close on
Escape, lock body scroll, and exposerole="dialog"+aria-modal="true"+aria-labelledby(the title). OK/confirm still never auto-closes — the caller decides. - Popover / Popconfirm: the trigger is a real focusable control (
role="button",tabindex="0",aria-haspopup,aria-expanded) operable from the keyboard —Enter/Spaceto open,Escapeto close. Both flip to the opposite side and shift along the cross axis to stay within the viewport, rendering hidden for one frame so the placement is never seen to jump. Select/EditSelectSearch/EditMultiSelect: full combobox ARIA (role="combobox"/listbox/option,aria-expanded,aria-controls,aria-activedescendant); the dropdown now opens upward when it would otherwise run off the bottom of the viewport.Pagination: rewritten as a semantic<nav aria-label="Pagination">of<button>s witharia-current="page"on the active page andaria-labels on the prev/next controls (was<ul>/<li>/<a>).- Toasts / notifications: the live region is announced via
role="status"+aria-live="polite". ReadOnlyValuenow HTML-encodes the value it displays instead of rendering it as raw markup — bound user data can no longer inject markup.EditDateread-only formats the bound value by its own type withDateFormat. The old code round-tripped through the editor string, which could shift the date across midnight in non-UTC zones and rendered aTimeOnlyas a date; an incompatible format now degrades to the value's ownToStringrather than throwing.EditCheckedEnumList/EditCheckedStringListbuild a new list when toggling instead of mutating the caller's bound collection in place.- The placement enum for
Popover/Popconfirmis namedPopupPlacement(it positions popups, not tooltips). The library builds with 0 warnings across net8 / net9 / net10.
Breaking dependency change
- Removed
Microsoft.AspNetCore.Components.DataAnnotations.Validation(3.2.0-rc1) from theWssBlazorControlspackage — the library itself never used it. Consumers who use<ObjectGraphDataAnnotationsValidator>or the[ValidateComplexType]attribute for nested-object validation must now add the package to their own project:
This eliminates the prerelease-dependency warning that previously bled through to consumer builds.dotnet add package Microsoft.AspNetCore.Components.DataAnnotations.Validation --version 3.2.0-rc1.20223.4
Behavior
- Validation messages now respect the
Labelparameter override on every control. Previously onlyEditCheckedStringListandEditCheckedEnumListpassedLabelthrough toFieldValidationDisplay; the other 12 controls would still derive the label from the model's attribute. Now if you set<EditString Label="Username" ... />, the validation message shows "Username is required" instead of falling back to the property name. EditSelectString<option>elements now render thetitletooltip (consistent withEditSelectEnum).- Cosmetic:
EditDate'sReadOnlyValuenow uses@_id/@_isRequiredlike every other control.
Build / packaging
<GeneratePackageOnBuild>is now scoped toConfiguration == Release. Dev / inner-loop builds no longer regenerate.nupkgfiles on every save —dotnet pack -c Release -o ./nupkgcontinues to produce them on demand.- Package now ships with a 128×128 icon (
icon.png, white "W" on Blazor purple). Visible in NuGet listings and Visual Studio's Manage NuGet Packages dialog.
New shared CSS class
.edit-inputis now applied to every editable element (<input>,<textarea>,<InputSelect>,<InputDate>) acrossEditString,EditNumber,EditDate,EditTextArea,EditSelect,EditSelectString,EditSelectEnum, plus the "Other" text inputs inEditRadioString/EditRadioEnum. The bundlededit-controls.cssships an empty rule — consumers can now style every editable element with one selector instead of writing per-element CSS forinput/textarea/selectseparately. Per-control classes (.edit-string-input,.edit-textarea-input,.edit-select-select, etc.) remain available for fine-tuning.
Internal
HidingMode: dropped the meaningless explicit= 1, 2, 3, 4, 5numeric values. Default is now0(None) which matches the?? HidingMode.Nonefallback already in every control. Consumers don't notice unless they were persisting the enum as an int — in which case existing values shift down by 1.ValidationHelper: replaced the brittlemessage.Split(' ')+ hardcoded array-index parsing of Range messages with a compiled regex. Now tolerates multi-word field names ("Order Total") and small format variations. Type-min/max sentinel detection moved intoHashSet<string>lookups instead of a long||chain.
Architecture: EditControlBase<TValue>
- 11 of 14 controls now inherit a single
EditControlBase<TValue> : InputBase<TValue>, IEditControlinstead of inheriting one of Microsoft's specializedInput*classes (InputText / InputNumber / InputDate / InputCheckbox / InputSelect / etc.). The base hoists every IEditControl parameter, both cascading parameters, the protected derived state (_id,_isRequired,_attributes,_fieldIdentifier), and theShowEditor/ShouldHideLabelchecks — so each derived control's.razor.csshrinks to just its component-specific parameters + parser + helpers. Net ~430 lines removed across the 11 controls. - The string-input/textarea/number/date/select parsing logic that Microsoft's
Input*classes used to provide is now ported into each control (typically a 5-15 lineTryParseValueFromStringoverride that delegates toBindConverter). Behavior is preserved — the new parsers route through the sameBindConverterMicrosoft uses internally. EditCheckedStringListandEditCheckedEnumListmigrated to a siblingEditControlListBase<TItem>(different shape — bindsList<TItem>instead of a scalar). TheSetAsync(item)rename toToggleAsync(item)is the only consumer-facing surface change.EditRadiois the one remaining control still on Microsoft'sInputRadioGroup<T>— it depends on the cascading-context plumbing that<InputRadio>children consume, and replacing the group requires also replacing the public<InputRadio>API. Intentional._Imports.razornow exposesMicrosoft.AspNetCore.Components.FormsandControls.Helpersso individual razor files no longer need per-file@usingdirectives for<InputRadioGroup>/<InputRadio>/.ToId()/ etc.
Tests
FormTesting/FormTesting.Client.Tests/(xUnit + bUnit, multi-targeted net8/9/10) — 270 tests (run once per TFM) covering the helpers (EnumHelperscache + attribute precedence,AttributesHelper.GetId/GetLabelText/GetMinAndMaxLengths,EditControlInit,ValidationHelperregex parsing), bUnit smoke tests for the form controls (rendered DOM, ARIA, edit/read-only switching), the AntDesign-style selects, and the UI-kit widgets (Table, dialogs, toasts) — plus regression tests for the audit fixes (ReadOnlyValueHTML-encoding,EditDateread-only formatting, checked-list immutability). Run withdotnet test FormTesting/FormTesting.Client.Tests/FormTesting.Client.Tests.csproj.FormTesting/FormTesting.Client.E2ETests/(xUnit + Playwright .NET, net10) — a 67-test end-to-end suite (one class perEdit*control plus the searchable selects and a driver for the/uikitgallery) with committed visual-regression baselines. Run withdotnet test FormTesting/FormTesting.Client.E2ETests/FormTesting.Client.E2ETests.csproj.
10.1.0
Behavioral changes (read before upgrading)
EditBool: read-only mode now rendersReadOnlyValuewith the newTrueText/FalseTextparameters (default"Yes"/"No"), matching every other control. SetRenderAsCheckboxWhenReadOnly="true"to keep the legacy disabled-checkbox display.EditString:aria-requirednow reflects the actual required state instead of being hard-coded to"true".
New CSS class — required for the invalid-icon overlay
.edit-input-with-iconwraps<input>/<textarea>/<InputDate>together with the optional red-X invalid icon inEditString,EditNumber,EditDate,EditTextArea. The bundlededit-controls.cssships an empty hook (the icon overlays the input via.edit-icon-invalid's negative margin and needs no positioning here). If you have your own stylesheet, no changes are required unless you want to adjust the input row's layout.
New parameters
EditBool.TrueText(default"Yes")EditBool.FalseText(default"No")EditBool.RenderAsCheckboxWhenReadOnly(defaultfalse)
New components / helpers
<InvalidIcon CssClass="..." />— reusable red-X SVG, conditional on the host'sCssClasscontaining"invalid".EditControlInit(inControls.Helpers) — static helper that consolidates theOnInitializedsetup and theShowEditor/ShouldHideLabelchecks every control was duplicating.
Markup consistency
EditSelectEnumswitched from@bind:get/@bind:setto<InputSelect @bind-Value=...>so it matchesEditSelect/EditSelectString.EditBoolNullRadioradio inputs now carryaria-required,aria-invalid,aria-describedby, andaria-errormessage. (Moved to a group-levelrole="radiogroup"container in the next release — see Unreleased.)aria-invalidis now rendered on every scalar editable control. (The grouped radio / checkbox-list controls are brought to parity in the next release — see Unreleased.).ToId()is now applied to enum optionids inEditSelectEnumandEditRadioEnum— fixes invalid HTML ids when an enum's display name contains spaces or punctuation.- The red-X invalid icon (previously only on
EditString) now appears onEditNumber,EditDate, andEditTextAreaas well.
Performance
EnumHelpers._nameCacheis now a thread-safeConcurrentDictionary<(Type, string), string>keyed by enum type — fixes potential cross-type collisions and removes a thread-safety hazard on pre-rendering.EnumHelpers.GetNamenow honors both[EnumDisplayName]and[Display(Name=...)]. Previously[Display]only affected sort order and[EnumDisplayName]only affected display, so the two could disagree.- The reflection-heavy enum sort blocks in
EditSelectEnum/EditRadioEnum/EditCheckedEnumListcollapsed toOrderBy(x => x.GetName())and benefit from the cache.
Bug fixes
- Fixed package description typo (
HierarchyAndEmployeeRecordprovidingartifact). - Removed stray
IsRequiredChangedparameter that existed only onEditRadioEnum. EditCheckedStringListwas silently dropping theIdPrefixparameter (nullwas being passed instead). Now consistent with every other control.EditBoolNullRadiofalse-radio'sclassattribute incorrectly used@ContainerClassinstead of@CssClass.focusFirstInvalidField(JS) now correctly handles invalid wrapper elements that aren't form fields, includes<select>, and guards.select()for input types that don't support it.
Refactoring (internal)
- All 14 controls now call
EditControlInit.Init(...)inOnInitializedinstead of duplicating the same 4 lines. - All controls use
EditControlInit.ShowEditor(...)andEditControlInit.ShouldHideLabel(...)for the visibility checks. - JavaScript helpers namespaced under
window.WssEditControls.*. Legacywindow.focusFirstInvalidField/window.log/ etc. are still exposed for back-compat — safe to migrate at your own pace. JsInteropEc.FocusFirstInvalidFieldusesTask.Yield()instead ofTask.Delay(1).FormLabel._isRequiredchanged fromstring("true"/"false") tobool.IEditControl.IsDisableddoc comment fixed (was"Not used"despite being used by every control).- Deleted dead
ExampleJsInterop.cstemplate code. - Removed unused
EditCheckedStringList.hasErrorandReadOnlyValue._emptyValuefields. - Build warnings reduced from 87 → 57.
10.0.7
- EditString: Add
Autocompleteparameter (defaults to"one-time-code") to prevent browser extensions and autofill from intercepting Blazor input events on fields with IDs containing keywords like "email"
10.0.2
- Support .net 8,9,10
10.0.1
- Upgrade to .net 10
- Add the ability to hide the required star within FormOptions
- Changed editControls.js to edit-controls.js
1.13.8
- Exposed xmldoc comments
1.13.7
- refactoring
1.13.6
- Move the star for non-legends to the left.
1.13.5
- Enable tooltips through markup
- Move the required star to the left of the label
1.13.4
- EditDate and other controls. Add a null value string to display when the value is null, such as a dash instead of blank space.
- IsRequired parameter on all controls. When set forces the “edit-label-required-star” to show up without being required in the DataAnnotations.
- Accessibility updates for EditCheckedStringList
1.13.3
- Current stable release
- Full feature set with comprehensive validation support
1.0.13.2
- EditCheckedListEnum
1.0.13.1
- Rename icons to have edit- in front of the current names
- .icon-eye ⇒ .edit-icon-eye
- Icon-invalid, icon-eye-invisible
- EditSelectEnum no longer requires specifying the type.
- Tooltips exist on the controls
- Only from attributes right now [Tooltip(“My cool tooltip”)
1.0.12.11
- Import js into application in App.razor or index.html
-
<script src="_content/WssBlazorControls/editControls.js"></script> - This is to add the functionality of “When submit is clicked, but invalid, scroll to the first input that is invalid.
- Use JsInteropEc to access js methods. Use JsInteropEc.FocusFirstInvalidField() when there are validation errors while submitting.
-
- EditCheckedStringList
- Error message shows up on each checkbox
1.0.12.10
- IsRequired parameter on all controls. When set forces the “edit-label-required-star” to show up without being required in the DataAnnotations.
- Accessibility updates for EditCheckedStringList
1.0.12.x
- moved away from utilizing bootstrap css classes such as form-group to using classes that start with edit- to avoid conflicts with other libraries
- New Features
- IsHidden to hide controls withougt wrapping them in an if statement
- Hiding allows hiding controls based on their own property for [Never, WhenReadonlyAndNull, WhenReadonly, etc.]
- This also exists within FormOptions, so the hiding can be controlled over a large group of controls.
- Control Changes
- EditRadio and EditCheckedList
- Change parameter from HasHorizontalButtons → IsHorizontal
- Removed the need for "Type" parameter, now uses the type of the value passed in.
- EditSelectEnum
- Removed the need for "Type" parameter, now uses the type of the value passed in.
- New Controls
- EditBoolNullRadio
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.Components.DataAnnotations.Validation (>= 3.2.0-rc1.20223.4)
- Microsoft.AspNetCore.Components.Web (>= 10.0.0)
- WssBlazorControls (>= 10.6.6)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.