WssBlazorControls.Demo
10.6.4
See the version list below for details.
dotnet add package WssBlazorControls.Demo --version 10.6.4
NuGet\Install-Package WssBlazorControls.Demo -Version 10.6.4
<PackageReference Include="WssBlazorControls.Demo" Version="10.6.4" />
<PackageVersion Include="WssBlazorControls.Demo" Version="10.6.4" />
<PackageReference Include="WssBlazorControls.Demo" />
paket add WssBlazorControls.Demo --version 10.6.4
#r "nuget: WssBlazorControls.Demo, 10.6.4"
#:package WssBlazorControls.Demo@10.6.4
#addin nuget:?package=WssBlazorControls.Demo&version=10.6.4
#tool nuget:?package=WssBlazorControls.Demo&version=10.6.4
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.
- 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 componentEditBool- 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">
<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.
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/Home/End move + select, roving tabindex)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 false, so every existing EditBool renders exactly as before.
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.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.
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 (see EVALUATION.md) 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 — see EVALUATION.md)
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 — see EVALUATION.md)
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 — see EVALUATION.md)
- 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 — see EVALUATION.md)
- 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.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.