Compare commits

...

47 Commits

Author SHA1 Message Date
yordanov
0c537fa5a8 Update expand/collapse button styles in RadzenDataGrid 2026-02-04 14:46:39 +02:00
Vladimir Enchev
49bfac1d86 tests updated 2026-02-04 14:46:39 +02:00
Vladimir Enchev
dac60a1c6b RadzenFabMenu fixed 2026-02-04 14:46:39 +02:00
Vladimir Enchev
9044596b6d RadzenSwitch fixed 2026-02-04 14:46:39 +02:00
yordanov
8994bd400d Update RadzenRating styles 2026-02-04 14:46:39 +02:00
yordanov
fb9ff6227a Add rz-hidden-accessible styles 2026-02-04 14:46:38 +02:00
Vladimir Enchev
6532cba586 accessibility issues fixes
Various localizable accessibility related properties added

tests updated

side dialog close button id fixed

more accessibility improvements

tests fixed

more accessibility fixes

missing ARIA attributes added
2026-02-04 14:46:38 +02:00
Vladimir Enchev
5bbf7c1fde build fixed 2026-02-04 14:46:25 +02:00
Vladimir Enchev
e416cede62 DropDownBase possible null reference exception with browser autofil 2026-02-04 12:08:41 +02:00
Vladimir Enchev
270a7e0f80 DropDown, ListBox and DropDownDatGrid paste using context menu into search box not filtering
Fix #2437
2026-02-04 11:28:12 +02:00
Vladimir Enchev
1a12a75bde Version updated 2026-02-03 09:56:04 +02:00
Vladimir Enchev
d1917eac0c Fixed QRCode eyes with transparent background 2026-02-03 08:44:24 +02:00
Vladimir Enchev
09830f0ea2 DataGrid possible memory leak fixed 2026-02-03 07:46:32 +02:00
Vladimir Enchev
d34e0684fb DataGrid GroupRowRenderEventArgs Expandable property added 2026-02-02 12:09:40 +02:00
Vladimir Enchev
482eca3278 tests fixed 2026-01-30 12:23:10 +02:00
Vladimir Enchev
5c8ac16c83 Version updated 2026-01-30 12:09:54 +02:00
Vladimir Enchev
29382cf0f4 DataGrid QueryOnlyVisibleColumns property added 2026-01-30 12:09:34 +02:00
Vladimir Enchev
53204cc8d6 RadzenPager GoToPage() will not update page index 2026-01-30 09:53:52 +02:00
Vladimir Enchev
6cf550c517 Version updated 2026-01-28 17:54:45 +02:00
vadimstrekha
7bf107af4c Fix sytax error in Radzen.Blazor.js (#2436) 2026-01-28 17:53:21 +02:00
Vladimir Enchev
adf2785a5a Version updated 2026-01-28 15:31:27 +02:00
Vladimir Enchev
cae44df00a Bar Charts have a zero for the min and max on the y-axis
Fix #2434
2026-01-28 15:31:05 +02:00
Vladimir Enchev
8ba1c69573 RadzenQRCode and RadzenBarcode ToSvg() methods added 2026-01-28 10:31:13 +02:00
Vladimir Enchev
56031c2fd4 QRCode and Barcode save to SVG examples added 2026-01-28 10:00:24 +02:00
Vladimir Enchev
ad44802d30 RadzenBarcodeEncoder and RadzenQREncoder made public
Fix #2433
2026-01-27 18:27:40 +02:00
Vladimir Enchev
64ca088e61 ListBox duplicates the first typed letter in WASM when inside Popup
Fix #2429
2026-01-27 14:08:46 +02:00
Theronguard
f4777565a2 Changed the method signature to virtual, to allow overrding Enum translation with component activators (#2432) 2026-01-27 13:14:28 +02:00
Vladimir Enchev
eb1423e757 Version update 2026-01-27 08:34:44 +02:00
Vladimir Enchev
596b251511 DataGrid column custom filter indicator is active even when no filter 2026-01-27 08:34:29 +02:00
Vladimir Enchev
e186315935 DataGrid grouping arrows do not show expanded state
Fix #2431
2026-01-27 08:27:26 +02:00
yordanov
cba9a5120d Reorder examples 2026-01-26 16:14:07 +02:00
Vladimir Enchev
ee62a21ab6 version updated 2026-01-26 14:36:48 +02:00
yordanov
0a5e318f80 Update premium themes 2026-01-26 14:34:01 +02:00
Vladimir Enchev
8dd7d7f521 DataGrid simple string filter clear button not shown 2026-01-26 10:34:05 +02:00
Vladimir Enchev
69573b2d7d demo updated 2026-01-26 10:06:23 +02:00
Vladimir Enchev
5b933c6643 DataGrid reorder column stick to mouse 2026-01-23 12:11:44 +02:00
Atanas Korchev
126b2d1efa RadzenSpiderChart (#2417)
* spider chart added

* code improved

* EventConsole added

* Pastel is the default color scheme

* Fix color schemes in Spider chart

* Update SpiderChart styles

---------

Co-authored-by: Ehab Hussein <me@ehabhussein.com>
Co-authored-by: Vladimir Enchev <vladimir.enchev@gmail.com>
Co-authored-by: yordanov <vasil@yordanov.info>
2026-01-22 14:27:01 +02:00
Vladimir Enchev
a65c1a9482 version updated 2026-01-21 17:31:55 +02:00
Vladimir Enchev
4939e8498a OData filtering by no string columns fixed 2026-01-21 17:31:33 +02:00
Vladimir Enchev
e380467853 Version updated 2026-01-21 10:17:06 +02:00
Vladimir Enchev
d3adc9733b PivotDataGrid user interaction breaks multi-column/row sorting
Fix #2427
2026-01-20 13:37:13 +02:00
Vladimir Enchev
a03fc50ee8 DataFilter should set filter Type on property change 2026-01-20 09:31:07 +02:00
Mason Voxland
c9201bf947 Fix TextProperty documentation in RadzenScheduler (#2426)
Should be string, not DateTime
2026-01-20 09:17:23 +02:00
yordanov
8c8d288afd Fix #2188 rz-layout height should be 100dvh by default 2026-01-19 17:15:40 +02:00
Vladimir Enchev
1a679af008 Chart demo source code fixed 2026-01-19 16:00:26 +02:00
yordanov
bc1654a405 Fix #2218 - RadzenSidebar border should take into account the sidebar's position 2026-01-19 13:12:54 +02:00
Atanas Korchev
94ef62e00b Customize split button dropdown icon demo is showing wrong source code. 2026-01-19 11:33:24 +02:00
155 changed files with 4804 additions and 1379 deletions

View File

@@ -123,7 +123,7 @@ namespace Radzen.Blazor.Tests
});
// Find and click the accordion header link to expand
var header = component.Find(".rz-accordion-header a");
var header = component.Find(".rz-accordion-header button");
header.Click();
Assert.True(expandRaised);
@@ -156,7 +156,7 @@ namespace Radzen.Blazor.Tests
});
// Find and click the accordion header link to collapse
var header = component.Find(".rz-accordion-header a");
var header = component.Find(".rz-accordion-header button");
header.Click();
Assert.True(collapseRaised);
@@ -184,7 +184,7 @@ namespace Radzen.Blazor.Tests
});
// Try to click the disabled item
var header = component.Find(".rz-accordion-header a");
var header = component.Find(".rz-accordion-header button");
header.Click();
// Event should not be raised for disabled item

View File

@@ -155,7 +155,7 @@ namespace Radzen.Blazor.Tests
parameters.Add<bool>(p => p.AllowClear, true);
});
Assert.Contains(@$"<i class=""notranslate rz-dropdown-clear-icon rzi rzi-times""", component.Markup);
Assert.Contains(@$"<button type=""button"" class=""notranslate rz-dropdown-clear-icon rzi rzi-times""", component.Markup);
}
[Fact]

View File

@@ -152,7 +152,7 @@ namespace Radzen.Blazor.Tests
parameters.Add(p => p.Collapse, args => { raised = true; });
});
component.Find("a").Click();
component.Find("legend button").Click();
Assert.True(raised);
@@ -160,7 +160,7 @@ namespace Radzen.Blazor.Tests
component.SetParametersAndRender(parameters => parameters.Add(p => p.Expand, args => { raised = true; }));
component.Find("a").Click();
component.Find("legend button").Click();
}
[Fact]

View File

@@ -119,7 +119,7 @@ namespace Radzen.Blazor.Tests
parameters.Add(p => p.AllowResetPassword, true);
});
Assert.Contains(@$"Forgot password?</a>", component.Markup);
Assert.Contains(@$"Forgot password?</button>", component.Markup);
}
[Fact]
@@ -134,7 +134,7 @@ namespace Radzen.Blazor.Tests
parameters.Add(p => p.ResetPasswordText, "Test");
});
Assert.Contains(@$"Test</a>", component.Markup);
Assert.Contains(@$"Test</button>", component.Markup);
}
[Fact]
@@ -215,7 +215,7 @@ namespace Radzen.Blazor.Tests
parameters.Add(p => p.ResetPassword, args => { clicked = true; });
});
component.Find("a").Click();
component.Find("button.rz-link").Click();
Assert.True(clicked);
}
@@ -234,7 +234,7 @@ namespace Radzen.Blazor.Tests
parameters.Add(p => p.ResetPassword, args => { clicked = true; });
});
component.Find("a").Click();
component.Find("button.rz-link").Click();
Assert.True(clicked);
}

View File

@@ -2,6 +2,7 @@ using Bunit;
using Bunit.JSInterop;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;
namespace Radzen.Blazor.Tests
@@ -54,7 +55,7 @@ namespace Radzen.Blazor.Tests
}
[Fact]
public async void RadzenPager_Renders_Summary() {
public async Task RadzenPager_Renders_Summary() {
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
@@ -64,7 +65,7 @@ namespace Radzen.Blazor.Tests
parameters.Add<int>(p => p.Count, 100);
parameters.Add<bool>(p => p.ShowPagingSummary, true);
});
await component.Instance.GoToPage(2);
await component.InvokeAsync(() => component.Instance.GoToPage(2));
component.Render();
Assert.Contains(@$"rz-pager-summary", component.Markup);
@@ -111,7 +112,7 @@ namespace Radzen.Blazor.Tests
}
[Fact]
public async void RadzenPager_First_And_Prev_Buttons_Are_Disabled_When_On_The_First_Page()
public async Task RadzenPager_First_And_Prev_Buttons_Are_Disabled_When_On_The_First_Page()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
@@ -123,18 +124,18 @@ namespace Radzen.Blazor.Tests
parameters.Add<bool>(p => p.ShowPagingSummary, true);
});
await component.Instance.GoToPage(0);
await component.InvokeAsync(() => component.Instance.GoToPage(0));
component.Render();
var firstPageButton = component.Find("a.rz-pager-first");
var firstPageButton = component.Find("button.rz-pager-first");
Assert.True(firstPageButton.HasAttribute("disabled"));
var prevPageButton = component.Find("a.rz-pager-prev");
var prevPageButton = component.Find("button.rz-pager-prev");
Assert.True(prevPageButton.HasAttribute("disabled"));
}
[Fact]
public async void RadzenPager_Last_And_Next_Buttons_Are_Disabled_When_On_The_Last_Page()
public async Task RadzenPager_Last_And_Next_Buttons_Are_Disabled_When_On_The_Last_Page()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
@@ -146,13 +147,13 @@ namespace Radzen.Blazor.Tests
parameters.Add<bool>(p => p.ShowPagingSummary, true);
});
await component.Instance.GoToPage(9);
await component.InvokeAsync(() => component.Instance.GoToPage(9));
component.Render();
var lastPageButton = component.Find("a.rz-pager-last");
var lastPageButton = component.Find("button.rz-pager-last");
Assert.True(lastPageButton.HasAttribute("disabled"));
var nextPageButton = component.Find("a.rz-pager-next");
var nextPageButton = component.Find("button.rz-pager-next");
Assert.True(nextPageButton.HasAttribute("disabled"));
}
}

View File

@@ -167,7 +167,7 @@ namespace Radzen.Blazor.Tests
parameters.Add(p => p.Collapse, args => { raised = true; });
});
component.Find("a").Click();
component.Find("button.rz-panel-titlebar-toggler").Click();
Assert.True(raised);
@@ -175,7 +175,7 @@ namespace Radzen.Blazor.Tests
component.SetParametersAndRender(parameters => parameters.Add(p => p.Expand, args => { raised = true; }));
component.Find("a").Click();
component.Find("button.rz-panel-titlebar-toggler").Click();
}
[Fact]

View File

@@ -110,7 +110,7 @@ namespace Radzen.Blazor.Tests
component.SetParametersAndRender(parameters => parameters.Add(p => p.Change, args => { raised = true; newValue = args; }));
component.Find("div").Click();
component.Find("input[type=\"checkbox\"]").Click();
Assert.True(raised);
Assert.True(object.Equals(value, !(bool)newValue));
@@ -129,7 +129,7 @@ namespace Radzen.Blazor.Tests
component.SetParametersAndRender(parameters => parameters.Add(p => p.ValueChanged, args => { raised = true; newValue = args; }));
component.Find("div").Click();
component.Find("input[type=\"checkbox\"]").Click();
Assert.True(raised);
Assert.True(object.Equals(value, !(bool)newValue));

View File

@@ -716,6 +716,42 @@ namespace Radzen
}
}
private string? ariaLabel = "Dialog";
/// <summary>
/// Gets or sets the dialog aria-label text when no title is rendered.
/// </summary>
/// <value>The dialog aria-label text.</value>
public string? AriaLabel
{
get => ariaLabel;
set
{
if (ariaLabel != value)
{
ariaLabel = value;
OnPropertyChanged(nameof(AriaLabel));
}
}
}
private string closeAriaLabel = "Close dialog";
/// <summary>
/// Gets or sets the close button aria-label text.
/// </summary>
/// <value>The close button aria-label text.</value>
public string CloseAriaLabel
{
get => closeAriaLabel;
set
{
if (closeAriaLabel != value)
{
closeAriaLabel = value;
OnPropertyChanged(nameof(CloseAriaLabel));
}
}
}
private string? width;
/// <summary>
/// Gets or sets the width of the dialog.

View File

@@ -254,6 +254,13 @@ namespace Radzen
[Parameter]
public string RemoveChipTitle { get; set; } = "Remove";
/// <summary>
/// Gets or sets the clear button aria label text.
/// </summary>
/// <value>The clear button aria label text.</value>
[Parameter]
public string ClearAriaLabel { get; set; } = "Clear";
/// <summary>
/// Gets or sets the search aria label text.
/// </summary>
@@ -508,7 +515,7 @@ namespace Radzen
/// <param name="item">The item.</param>
/// <param name="property">The property.</param>
/// <returns>System.Object.</returns>
public object? GetItemOrValueFromProperty(object? item, string property)
public virtual object? GetItemOrValueFromProperty(object? item, string property)
{
if (item != null)
{
@@ -558,6 +565,18 @@ namespace Radzen
}
}
/// <summary>
/// Gets the listbox identifier.
/// </summary>
/// <value>The listbox identifier.</value>
protected string ListId
{
get
{
return $"{GetId()}-list";
}
}
/// <summary>
/// Gets the search identifier.
/// </summary>
@@ -635,6 +654,8 @@ namespace Radzen
await JSRuntime.InvokeVoidAsync("Radzen.focusElement", isFilter ? UniqueID : SearchID);
}
isPopupOpen = true;
if (list != null && JSRuntime != null)
{
await JSRuntime.InvokeVoidAsync("Radzen.selectListItem", search, list, selectedIndex);
@@ -651,7 +672,11 @@ namespace Radzen
/// <param name="shouldSelectOnChange">Should select item on item change with keyboard.</param>
protected virtual async Task HandleKeyPress(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs args, bool isFilter = false, bool? shouldSelectOnChange = null)
{
if (Disabled || Data == null || args == null)
ArgumentNullException.ThrowIfNull(args);
var key = args.Code != null ? args.Code : args.Key;
if (Disabled || Data == null || args == null || key == null)
return;
List<object> items = Enumerable.Empty<object>().ToList();
@@ -675,8 +700,6 @@ namespace Radzen
}
}
var key = args.Code != null ? args.Code : args.Key;
if (!args.AltKey && (key == "ArrowDown" || key == "ArrowLeft" || key == "ArrowUp" || key == "ArrowRight"))
{
preventKeydown = true;
@@ -841,8 +864,15 @@ namespace Radzen
{
await JSRuntime.InvokeVoidAsync("Radzen.closePopup", PopupID);
}
isPopupOpen = false;
}
/// <summary>
/// Gets a value indicating whether the popup is open.
/// </summary>
protected bool isPopupOpen;
int itemIndex;
string? previousKey;
@@ -936,6 +966,25 @@ namespace Radzen
}
}
/// <summary>
/// Handles filter input changes (e.g. paste).
/// </summary>
/// <param name="args">The <see cref="ChangeEventArgs"/> instance containing the event data.</param>
protected virtual async Task OnFilterInput(ChangeEventArgs args)
{
ArgumentNullException.ThrowIfNull(args);
searchText = $"{args.Value}";
await SearchTextChanged.InvokeAsync(searchText);
if (ResetSelectedIndexOnFilter)
{
selectedIndex = -1;
}
Debounce(DebounceFilter, FilterDelay);
}
/// <summary>
/// Gets the load data arguments.
/// </summary>
@@ -1269,9 +1318,25 @@ namespace Radzen
await Change.InvokeAsync(internalValue);
}
StateHasChanged();
}
/// <summary>
/// Handles keyboard activation for the select-all action.
/// </summary>
/// <param name="args">The <see cref="Microsoft.AspNetCore.Components.Web.KeyboardEventArgs"/> instance containing the event data.</param>
protected async Task OnSelectAllKeyDown(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs args)
{
ArgumentNullException.ThrowIfNull(args);
var key = args.Code != null ? args.Code : args.Key;
if (key == "Enter" || key == "Space")
{
await SelectAll();
}
}
/// <inheritdoc />
public override object? GetValue()
{

View File

@@ -17,6 +17,12 @@ public class GroupRowRenderEventArgs
/// </summary>
public Group? Group { get; internal set; }
/// <summary>
/// Gets or sets a value indicating whether this group row is expandable.
/// </summary>
/// <value><c>true</c> if expandable; otherwise, <c>false</c>.</value>
public bool Expandable { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this group row is expanded.
/// </summary>

View File

@@ -0,0 +1,22 @@
using System.Threading.Tasks;
namespace Radzen.Blazor
{
/// <summary>
/// Non-generic contract for <see cref="RadzenSpiderChart"/> used by configuration components
/// like <see cref="RadzenSpiderLegend"/> without relying on reflection (important for trimming/AOT).
/// </summary>
public interface IRadzenSpiderChart
{
/// <summary>
/// Gets or sets the legend configuration for the chart.
/// </summary>
RadzenSpiderLegend Legend { get; set; }
/// <summary>
/// Requests the chart to refresh its rendering.
/// </summary>
Task Refresh();
}
}

View File

@@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Components;
using System.Collections.Generic;
namespace Radzen.Blazor
{
/// <summary>
/// Non-generic contract for spider series so <see cref="RadzenSpiderChart"/> can be non-generic.
/// </summary>
internal interface IRadzenSpiderSeries
{
int Index { get; set; }
string Title { get; }
bool IsVisible { get; set; }
bool MarkersVisible { get; }
double MarkerSize { get; }
double StrokeWidth { get; }
IEnumerable<string> GetCategories();
IEnumerable<double> GetValues();
double GetValue(string category);
object? GetData(string category);
string FormatValue(double value);
double MeasureLegend();
RenderFragment RenderLegendItem();
void ForceUpdate();
}
}

View File

@@ -152,6 +152,12 @@ namespace Radzen
/// <value>The summary content.</value>
public RenderFragment<NotificationService>? SummaryContent { get; set; }
/// <summary>
/// Gets or sets the close button aria-label.
/// </summary>
/// <value>The close button aria-label.</value>
public string CloseAriaLabel { get; set; } = "Close";
#region Implementation of IEquatable<NotificationMessage> and operators overloading

View File

@@ -41,6 +41,65 @@ namespace Radzen
return source.Provider.CreateQuery(selectExpression);
}
/// <summary>
/// Projects each element of a sequence into a collection of property values.
/// </summary>
public static IQueryable Select(this IQueryable source, IEnumerable<string> propertyNames)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(propertyNames);
var parameter = Expression.Parameter(source.ElementType, "x");
var bindings = new List<MemberBinding>();
var allProperties = source.ElementType.GetProperties();
foreach (var property in allProperties.Where(p => propertyNames.Contains(p.Name)))
{
bindings.Add(Expression.Bind(property, Expression.Property(parameter, property)));
}
var body = Expression.MemberInit(Expression.New(source.ElementType), bindings);
var delegateType = typeof(Func<,>).MakeGenericType(source.ElementType, source.ElementType);
var lambda = Expression.Lambda(delegateType, body, parameter);
var selectExpression = Expression.Call(typeof(Queryable),
nameof(Queryable.Select), [source.ElementType, source.ElementType], source.Expression,
Expression.Quote(lambda));
return source.Provider.CreateQuery(selectExpression);
}
/// <summary>
/// Projects each element of a sequence into a collection of property values.
/// </summary>
public static IQueryable<T> Select<T>(this IQueryable<T> source, IEnumerable<string> propertyNames)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(propertyNames);
var parameter = Expression.Parameter(typeof(T), "x");
var bindings = new List<MemberBinding>();
var allProperties = typeof(T).GetProperties();
foreach (var property in allProperties.Where(p => propertyNames.Contains(p.Name)))
{
bindings.Add(Expression.Bind(property, Expression.Property(parameter, property)));
}
var body = Expression.MemberInit(Expression.New(typeof(T)), bindings);
var lambda = Expression.Lambda<Func<T, T>>(body, parameter);
var selectExpression = Expression.Call(typeof(Queryable),
nameof(Queryable.Select), [typeof(T), typeof(T)], source.Expression,
Expression.Quote(lambda));
return source.Provider.CreateQuery<T>(selectExpression);
}
/// <summary>
/// Projects each element of a sequence to an IEnumerable and flattens the resulting sequences into one sequence.
@@ -1695,7 +1754,7 @@ namespace Radzen
return columns
.Where(c => c.Filterable
&& c.FilterPropertyType != null
&& (!string.IsNullOrEmpty(c.GetFilterValue() as string)
&& (!(c.GetFilterValue() == null || (c.GetFilterValue() as string)?.Length == 0)
|| !c.CanSetFilterValue()
|| c.HasCustomFilter())
&& c.GetFilterProperty() != null)

View File

@@ -11,7 +11,7 @@
<IsPackable>true</IsPackable>
<PackageId>Radzen.Blazor</PackageId>
<Product>Radzen.Blazor</Product>
<Version>8.6.3</Version>
<Version>8.7.5</Version>
<Copyright>Radzen Ltd.</Copyright>
<Authors>Radzen Ltd.</Authors>
<Description>Radzen Blazor is the most sophisticated free UI component library for Blazor, featuring 100+ native components including DataGrid, Scheduler, Charts, and advanced theming with full support for Material Design and Fluent UI.</Description>

View File

@@ -19,9 +19,11 @@
if (!item.Visible)
continue;
var accordionId = $"{GetId()}-accordiontab-{items.IndexOf(item)}";
<div @ref="@item.Element" id="@item.GetItemId()" @attributes="item.Attributes" class="@item.GetItemCssClass()" style="@item.Style" @onkeydown:stopPropagation>
<a @onclick="@((args) => SelectItem(item))" aria-label="@ItemAriaLabel(i, item)" title="@ItemTitle(i, item)" @onclick:preventDefault="true" role="tab"
id="@($"rz-accordiontab-{items.IndexOf(item)}")" aria-controls="@($"rz-accordiontab-{items.IndexOf(item)}-content")" aria-expanded="true">
<button type="button" @onclick="@((args) => SelectItem(item))" aria-label="@ItemAriaLabel(i, item)" title="@ItemTitle(i, item)" role="tab"
id="@accordionId" aria-controls="@($"{accordionId}-content")" aria-expanded="@(item.GetSelected() ? "true" : "false")"
aria-selected="@(item.GetSelected() ? "true" : "false")" tabindex="@(item.GetSelected() ? "0" : "-1")">
<span class="@ToggleIconClass(item)">keyboard_arrow_down</span>
@if (!string.IsNullOrEmpty(item.Icon))
{
@@ -35,10 +37,10 @@
{
<span>@item.Text</span>
}
</a>
</button>
</div>
<Expander Expanded=@item.GetSelected() role="tabpanel"
id="@($"rz-accordiontab-{items.IndexOf(item)}-content")" aria-labelledby="@($"rz-accordiontab-{items.IndexOf(item)}")">
id="@($"{accordionId}-content")" aria-labelledby="@accordionId">
<div class="rz-accordion-content" @onkeydown:stopPropagation>
@item.ChildContent
</div>

View File

@@ -13,25 +13,27 @@
{
<textarea @ref="@search" @attributes="InputAttributes" @onkeydown="@OnFilterKeyPress" value="@Value" disabled="@Disabled"
oninput="@OpenScript()" tabindex="@(Disabled ? "-1" : $"{TabIndex}")" @onchange="@OnChange" onfocus="@(OpenOnFocus ? OpenScript() : null)"
aria-autocomplete="list" aria-haspopup="true" autocomplete="off" role="combobox"
aria-autocomplete="list" aria-haspopup="listbox" aria-controls="@ListId" aria-expanded="@(IsPopupOpen ? "true" : "false")" autocomplete="off" role="combobox"
class=@InputClass onblur="Radzen.activeElement = null"
id="@Name" aria-expanded="true" placeholder="@CurrentPlaceholder" maxlength="@MaxLength" />
id="@Name" placeholder="@CurrentPlaceholder" maxlength="@MaxLength" />
}
else
{
<input @ref="@search" @attributes="InputAttributes" @onkeydown="@OnFilterKeyPress" value="@Value" disabled="@Disabled"
oninput="@OpenScript()" tabindex="@(Disabled ? "-1" : $"{TabIndex}")" @onchange="@OnChange" onfocus="@(OpenOnFocus ? OpenScript() : null)"
aria-autocomplete="list" aria-haspopup="true" autocomplete="off" role="combobox"
aria-autocomplete="list" aria-haspopup="listbox" aria-controls="@ListId" aria-expanded="@(IsPopupOpen ? "true" : "false")" autocomplete="off" role="combobox"
class=@InputClass onblur="Radzen.activeElement = null"
type="@InputType" id="@Name" aria-expanded="true" placeholder="@CurrentPlaceholder" maxlength="@MaxLength" />
type="@InputType" id="@Name" placeholder="@CurrentPlaceholder" maxlength="@MaxLength" />
}
<div id="@PopupID" class="rz-autocomplete-panel" style="@PopupStyle">
<ul @ref="@list" class="rz-autocomplete-items rz-autocomplete-list" role="listbox">
<ul id="@ListId" @ref="@list" class="rz-autocomplete-items rz-autocomplete-list" role="listbox">
@if (OpenOnFocus || (!string.IsNullOrEmpty(searchText) || !string.IsNullOrEmpty(customSearchText)))
{
@foreach (var item in LoadData.HasDelegate ? Data != null ? Data : Enumerable.Empty<object>() : (View != null ? View : Enumerable.Empty<object>()))
{
<li role="option" class="rz-autocomplete-list-item" @onclick="@(() => OnSelectItem(item))" onmousedown="Radzen.activeElement = null">
<li role="option" class="rz-autocomplete-list-item" tabindex="-1"
aria-selected="@(object.Equals(item, SelectedItem) ? "true" : "false")"
@onclick="@(() => OnSelectItem(item))" onmousedown="Radzen.activeElement = null">
<span>
@if (Template != null)
{

View File

@@ -222,6 +222,10 @@ namespace Radzen.Blazor
}
}
private string ListId => $"{PopupID}-list";
private bool IsPopupOpen => OpenOnFocus || (!string.IsNullOrEmpty(searchText) || !string.IsNullOrEmpty(customSearchText));
private async Task OnSelectItem(object item)
{
if (JSRuntime != null)

View File

@@ -1,9 +1,11 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Radzen.Blazor
{
@@ -93,22 +95,6 @@ namespace Radzen.Blazor
_ => false
};
internal readonly struct BarcodeRect
{
public readonly double X;
public readonly double Y;
public readonly double Width;
public readonly double Height;
public BarcodeRect(double x, double y, double width, double height)
{
X = x;
Y = y;
Width = width;
Height = height;
}
}
/// <summary>
/// Gets or sets the barcode value to encode.
/// </summary>
@@ -275,894 +261,20 @@ namespace Radzen.Blazor
if (vbWidth <= 0) vbWidth = 1;
return (geometry.bars, vbWidth, checksumText, null);
}
}
internal static class RadzenBarcodeEncoder
{
// Code 128 patterns (0..106). Each entry is 6 digits (bar/space/bar/space/bar/space) module widths.
// Stop code (106) is 7 digits in the spec (includes a final bar). We keep it as 7 digits and handle it.
static readonly string[] Code128Patterns = new[]
/// <summary>
/// Returns the SVG markup of the rendered QR code as a string.
/// </summary>
/// <returns>
/// A <see cref="Task{String}"/> representing the asynchronous operation. The task result contains the SVG markup of the QR code.
/// </returns>
public async Task<string> ToSvg()
{
"212222","222122","222221","121223","121322","131222","122213","122312","132212","221213",
"221312","231212","112232","122132","122231","113222","123122","123221","223211","221132",
"221231","213212","223112","312131","311222","321122","321221","312212","322112","322211",
"212123","212321","232121","111323","131123","131321","112313","132113","132311","211313",
"231113","231311","112133","112331","132131","113123","113321","133121","313121","211331",
"231131","213113","213311","213131","311123","311321","331121","312113","312311","332111",
"314111","221411","431111","111224","111422","121124","121421","141122","141221","112214",
"112412","122114","122411","142112","142211","241211","221114","413111","241112","134111",
"111242","121142","121241","114212","124112","124211","411212","421112","421211","212141",
"214121","412121","111143","111341","131141","114113","114311","411113","411311","113141",
"114131","311141","411131","211412","211214","211232","2331112"
};
public static IReadOnlyList<int> EncodeCode128B(string value) => EncodeCode128B(value, out _);
public static IReadOnlyList<int> EncodeCode128B(string value, out int checksum)
{
// Code 128 subset B supports ASCII 32..127 (inclusive). We treat 127 as DEL.
var codes = new List<int>(value.Length + 3);
const int startB = 104;
const int stop = 106;
codes.Add(startB);
for (int i = 0; i < value.Length; i++)
if (JSRuntime != null)
{
var ch = value[i];
int ascii = ch;
if (ascii < 32 || ascii > 127)
{
throw new ArgumentException($"Code128B supports ASCII 32..127. Invalid character: U+{ascii:X4}.");
}
// In Code128B, code value is ascii - 32
codes.Add(ascii - 32);
return await JSRuntime.InvokeAsync<string>("Radzen.outerHTML", Element);
}
int checksumValue = codes[0];
for (int i = 1; i < codes.Count; i++)
{
checksumValue += codes[i] * i;
}
checksumValue %= 103;
// expose checksum (0..102)
checksum = checksumValue;
codes.Add(checksumValue);
codes.Add(stop);
// Convert codes to module pattern widths, alternating bar/space.
// Most codes are 6 digits; stop is 7 digits.
var modules = new List<int>(codes.Count * 6);
foreach (var code in codes)
{
var p = Code128Patterns[code];
for (int i = 0; i < p.Length; i++)
{
modules.Add(p[i] - '0');
}
}
// Code 128 requires a 2-module termination bar after the stop pattern.
// Many pattern tables omit it because it can be represented by extending the
// final bar of the stop pattern by 2 modules.
if (modules.Count > 0)
{
modules[^1] += 2;
}
return modules;
}
static readonly Dictionary<char, string> Code39Map = new Dictionary<char, string>()
{
// Each pattern is 9 elements (bar/space alternating, starting with bar).
// 'n' = narrow (1), 'w' = wide (2). We expand to digits.
['0'] = "nnnwwnwnn",
['1'] = "wnnwnnnnw",
['2'] = "nnwwnnnnw",
['3'] = "wnwwnnnnn",
['4'] = "nnnwwnnnw",
['5'] = "wnnwwnnnn",
['6'] = "nnwwwnnnn",
['7'] = "nnnwnnwnw",
['8'] = "wnnwnnwnn",
['9'] = "nnwwnnwnn",
['A'] = "wnnnnwnnw",
['B'] = "nnwnnwnnw",
['C'] = "wnwnnwnnn",
['D'] = "nnnnwwnnw",
['E'] = "wnnnwwnnn",
['F'] = "nnwnwwnnn",
['G'] = "nnnnnwwnw",
['H'] = "wnnnnwwnn",
['I'] = "nnwnnwwnn",
['J'] = "nnnnwwwnn",
['K'] = "wnnnnnnww",
['L'] = "nnwnnnnww",
['M'] = "wnwnnnnwn",
['N'] = "nnnnwnnww",
['O'] = "wnnnwnnwn",
['P'] = "nnwnwnnwn",
['Q'] = "nnnnnnwww",
['R'] = "wnnnnnwwn",
['S'] = "nnwnnnwwn",
['T'] = "nnnnwnwwn",
['U'] = "wwnnnnnnw",
['V'] = "nwwnnnnnw",
['W'] = "wwwnnnnnn",
['X'] = "nwnnwnnnw",
['Y'] = "wwnnwnnnn",
['Z'] = "nwwnwnnnn",
['-'] = "nwnnnnwnw",
['.'] = "wwnnnnwnn",
[' '] = "nwwnnnwnn",
['$'] = "nwnwnwnnn",
['/'] = "nwnwnnnwn",
['+'] = "nwnnnwnwn",
['%'] = "nnnwnwnwn",
['*'] = "nwnnwnwnn", // start/stop
};
public static IReadOnlyList<int> EncodeCode39(string value)
{
// Code39 traditionally uses uppercase
var text = value.ToUpperInvariant();
foreach (var ch in text)
{
if (!Code39Map.ContainsKey(ch))
{
throw new ArgumentException($"Code39 does not support character '{ch}'.");
}
}
// Start + data + stop, inter-character gap (narrow space) between characters.
var full = "*" + text + "*";
var modules = new List<int>(full.Length * 10);
for (int idx = 0; idx < full.Length; idx++)
{
var pat = Code39Map[full[idx]];
for (int i = 0; i < pat.Length; i++)
{
modules.Add(pat[i] == 'w' ? 2 : 1);
}
// Inter-character gap (narrow space) except after last char.
if (idx != full.Length - 1)
{
modules.Add(1);
}
}
return modules;
}
public static IReadOnlyList<int> EncodeItf(string value)
{
var digits = new string(value.Where(char.IsDigit).ToArray());
if (digits.Length == 0)
{
throw new ArgumentException("ITF requires numeric input.");
}
if (digits.Length % 2 != 0)
{
// pad with leading zero (common behavior)
digits = "0" + digits;
}
const int narrow = 1;
const int wide = 3;
static string Pat(char d) => d switch
{
'0' => "nnwwn",
'1' => "wnnnw",
'2' => "nwnnw",
'3' => "wwnnn",
'4' => "nnwnw",
'5' => "wnwnn",
'6' => "nwwnn",
'7' => "nnnww",
'8' => "wnnwn",
'9' => "nwnwn",
_ => throw new ArgumentException("ITF requires numeric input.")
};
var widths = new List<int>(digits.Length * 10 + 16);
// Start: narrow bar, narrow space, narrow bar, narrow space (1010)
widths.Add(narrow);
widths.Add(narrow);
widths.Add(narrow);
widths.Add(narrow);
for (int i = 0; i < digits.Length; i += 2)
{
var a = Pat(digits[i]);
var b = Pat(digits[i + 1]);
for (int j = 0; j < 5; j++)
{
widths.Add(a[j] == 'w' ? wide : narrow); // bar
widths.Add(b[j] == 'w' ? wide : narrow); // space
}
}
// Stop: wide bar, narrow space, narrow bar (1101)
widths.Add(wide);
widths.Add(narrow);
widths.Add(narrow);
return widths;
}
public static IReadOnlyList<int> EncodeCodabar(string value)
{
// Wikipedia table mapping (bars: 1=wide, spaces: 0=wide) for the standard symbol set.
// Ensure start/stop are present; default to A ... B if missing.
var raw = (value ?? string.Empty).Trim().ToUpperInvariant();
if (raw.Length == 0)
{
throw new ArgumentException("Codabar requires a non-empty value.");
}
bool HasStartStop(string s)
{
if (s.Length < 2) return false;
bool isStart = s[0] == 'A' || s[0] == 'B' || s[0] == 'C' || s[0] == 'D';
bool isStop = s[s.Length - 1] == 'A' || s[s.Length - 1] == 'B' || s[s.Length - 1] == 'C' || s[s.Length - 1] == 'D';
return isStart && isStop;
}
var text = HasStartStop(raw) ? raw : $"A{raw}B";
static (string spaceBits, string barBits) Map(char ch) => ch switch
{
'0' => ("001", "0001"),
'1' => ("001", "0010"),
'2' => ("010", "0001"),
'3' => ("100", "1000"),
'4' => ("001", "0100"),
'5' => ("001", "1000"),
'6' => ("100", "0001"),
'7' => ("100", "0010"),
'8' => ("100", "0100"),
'9' => ("010", "1000"),
'-' => ("010", "0010"),
'$' => ("010", "0100"),
'.' => ("000", "0001"),
'/' => ("000", "0010"),
':' => ("000", "0100"),
'+' => ("000", "1000"),
'A' => ("011", "0100"),
'B' => ("110", "0001"),
'C' => ("011", "0001"),
'D' => ("011", "0010"),
_ => throw new ArgumentException($"Codabar does not support character '{ch}'.")
};
const int narrow = 1;
const int wide = 3;
var widths = new List<int>(text.Length * 8);
for (int idx = 0; idx < text.Length; idx++)
{
var ch = text[idx];
var (spaceBits, barBits) = Map(ch);
// Bars: 4 bits, 1=wide
int BarWidth(int pos) => barBits[pos] == '1' ? wide : narrow;
// Spaces: 3 bits, 0=wide (per wikipedia mapping table)
int SpaceWidth(int pos) => spaceBits[pos] == '0' ? wide : narrow;
widths.Add(BarWidth(0));
widths.Add(SpaceWidth(0));
widths.Add(BarWidth(1));
widths.Add(SpaceWidth(1));
widths.Add(BarWidth(2));
widths.Add(SpaceWidth(2));
widths.Add(BarWidth(3));
// Inter-character narrow space (except after last char).
if (idx != text.Length - 1)
{
widths.Add(narrow);
}
}
return widths;
}
static readonly string[] EanL = new[]
{
"0001101","0011001","0010011","0111101","0100011","0110001","0101111","0111011","0110111","0001011"
};
static readonly string[] EanG = new[]
{
"0100111","0110011","0011011","0100001","0011101","0111001","0000101","0010001","0001001","0010111"
};
static readonly string[] EanR = new[]
{
"1110010","1100110","1101100","1000010","1011100","1001110","1010000","1000100","1001000","1110100"
};
static readonly string[] Ean13Parity = new[]
{
"LLLLLL","LLGLGG","LLGGLG","LLGGGL","LGLLGG","LGGLLG","LGGGLL","LGLGLG","LGLGGL","LGGLGL"
};
static int ComputeEanCheckDigit(string digitsWithoutCheck)
{
// digitsWithoutCheck is 7/11/12 digits depending on symbology.
int sum = 0;
bool weight3 = true;
for (int i = digitsWithoutCheck.Length - 1; i >= 0; i--)
{
int d = digitsWithoutCheck[i] - '0';
sum += weight3 ? d * 3 : d;
weight3 = !weight3;
}
int mod = sum % 10;
return (10 - mod) % 10;
}
public static string EncodeEan13(string value, out string checksumText)
{
var digits = new string(value.Where(char.IsDigit).ToArray());
if (digits.Length != 12 && digits.Length != 13)
{
throw new ArgumentException("EAN-13 requires 12 or 13 digits.");
}
if (digits.Length == 12)
{
var check = ComputeEanCheckDigit(digits);
digits += check.ToString(CultureInfo.InvariantCulture);
}
else
{
var expected = ComputeEanCheckDigit(digits[..12]);
if (digits[12] - '0' != expected)
{
throw new ArgumentException("Invalid EAN-13 check digit.");
}
}
checksumText = digits[^1].ToString();
int first = digits[0] - '0';
var parity = Ean13Parity[first];
var sb = new StringBuilder(95);
sb.Append("101");
// digits 2..7 (index 1..6)
for (int i = 1; i <= 6; i++)
{
int d = digits[i] - '0';
sb.Append(parity[i - 1] == 'G' ? EanG[d] : EanL[d]);
}
sb.Append("01010");
for (int i = 7; i <= 12; i++)
{
int d = digits[i] - '0';
sb.Append(EanR[d]);
}
sb.Append("101");
return sb.ToString();
}
public static string EncodeUpcA(string value, out string checksumText)
{
var digits = new string(value.Where(char.IsDigit).ToArray());
if (digits.Length != 11 && digits.Length != 12)
{
throw new ArgumentException("UPC-A requires 11 or 12 digits.");
}
if (digits.Length == 11)
{
var check = ComputeEanCheckDigit(digits);
digits += check.ToString(CultureInfo.InvariantCulture);
}
else
{
var expected = ComputeEanCheckDigit(digits[..11]);
if (digits[11] - '0' != expected)
{
throw new ArgumentException("Invalid UPC-A check digit.");
}
}
checksumText = digits[^1].ToString();
var sb = new StringBuilder(95);
sb.Append("101");
for (int i = 0; i < 6; i++)
{
int d = digits[i] - '0';
sb.Append(EanL[d]);
}
sb.Append("01010");
for (int i = 6; i < 12; i++)
{
int d = digits[i] - '0';
sb.Append(EanR[d]);
}
sb.Append("101");
return sb.ToString();
}
public static string EncodeEan8(string value, out string checksumText)
{
var digits = new string(value.Where(char.IsDigit).ToArray());
if (digits.Length != 7 && digits.Length != 8)
{
throw new ArgumentException("EAN-8 requires 7 or 8 digits.");
}
if (digits.Length == 7)
{
var check = ComputeEanCheckDigit(digits);
digits += check.ToString(CultureInfo.InvariantCulture);
}
else
{
var expected = ComputeEanCheckDigit(digits[..7]);
if (digits[7] - '0' != expected)
{
throw new ArgumentException("Invalid EAN-8 check digit.");
}
}
checksumText = digits[^1].ToString();
var sb = new StringBuilder(67);
sb.Append("101");
for (int i = 0; i < 4; i++)
{
int d = digits[i] - '0';
sb.Append(EanL[d]);
}
sb.Append("01010");
for (int i = 4; i < 8; i++)
{
int d = digits[i] - '0';
sb.Append(EanR[d]);
}
sb.Append("101");
return sb.ToString();
}
public static string EncodeIsbnAsEan13(string value, out string checksumText)
{
var raw = new string(value.Where(char.IsLetterOrDigit).ToArray()).ToUpperInvariant();
if (raw.Length == 10)
{
// ISBN-10 -> EAN-13: 978 + first 9 digits + EAN check
var core = raw[..9];
if (!core.All(char.IsDigit)) throw new ArgumentException("Invalid ISBN-10.");
return EncodeEan13("978" + core, out checksumText);
}
if (raw.Length == 13)
{
if (!raw.All(char.IsDigit)) throw new ArgumentException("Invalid ISBN-13.");
return EncodeEan13(raw, out checksumText);
}
throw new ArgumentException("ISBN requires 10 or 13 characters.");
}
public static string EncodeIssnAsEan13(string value, out string checksumText)
{
// ISSN EAN-13: 977 + first 7 digits + 00 + EAN check
var raw = new string(value.Where(char.IsLetterOrDigit).ToArray()).ToUpperInvariant();
if (raw.Length != 8) throw new ArgumentException("ISSN requires 8 characters.");
var core = raw[..7];
if (!core.All(char.IsDigit)) throw new ArgumentException("Invalid ISSN.");
return EncodeEan13("977" + core + "00", out checksumText);
}
public static (IReadOnlyList<RadzenBarcode.BarcodeRect> bars, double vbWidth) EncodePharmacode(string value, double barHeight, int quietZone)
{
// Pharmacode one-track: numbers 3..131070
var digits = new string(value.Where(char.IsDigit).ToArray());
if (!int.TryParse(digits, NumberStyles.None, CultureInfo.InvariantCulture, out var n))
{
throw new ArgumentException("Pharmacode requires a numeric value.");
}
if (n < 3 || n > 131070)
{
throw new ArgumentException("Pharmacode value must be in range 3..131070.");
}
var bars = new List<(int width, bool isWide)>();
while (n > 0)
{
if (n % 2 == 0)
{
bars.Add((2, true));
n = (n - 2) / 2;
}
else
{
bars.Add((1, false));
n = (n - 1) / 2;
}
}
bars.Reverse();
double x = Math.Max(0, quietZone);
var rects = new List<RadzenBarcode.BarcodeRect>(bars.Count);
foreach (var b in bars)
{
rects.Add(new RadzenBarcode.BarcodeRect(x, 0, b.width, barHeight));
x += b.width + 1; // 1 module gap
}
var vbWidth = x + Math.Max(0, quietZone);
if (vbWidth <= 0) vbWidth = 1;
return (rects, vbWidth);
}
// POSTNET digit encoding from Wikipedia (weights 7,4,2,1,0). 1=full bar, 0=half bar.
static readonly Dictionary<char, string> PostnetDigitBits = new Dictionary<char, string>()
{
['0'] = "11000",
['1'] = "00011",
['2'] = "00101",
['3'] = "00110",
['4'] = "01001",
['5'] = "01010",
['6'] = "01100",
['7'] = "10001",
['8'] = "10010",
['9'] = "10100",
};
public static (IReadOnlyList<RadzenBarcode.BarcodeRect> bars, double vbWidth) EncodePostnet(string value, double barHeight, int quietZone, out string checksumText)
{
var digits = new string(value.Where(char.IsDigit).ToArray());
if ((digits.Length != 5 && digits.Length != 9 && digits.Length != 11))
{
throw new ArgumentException("POSTNET requires 5, 9, or 11 digits (ZIP / ZIP+4 / Delivery Point).");
}
int sum = digits.Sum(ch => ch - '0');
int check = (10 - (sum % 10)) % 10;
checksumText = check.ToString(CultureInfo.InvariantCulture);
var payload = digits + checksumText;
double fullH = barHeight;
double halfH = barHeight / 2.0;
double halfY = fullH - halfH;
double x = Math.Max(0, quietZone);
var rects = new List<RadzenBarcode.BarcodeRect>();
// Start frame bar (full)
rects.Add(new RadzenBarcode.BarcodeRect(x, 0, 1, fullH));
x += 2; // bar(1) + space(1)
foreach (var ch in payload)
{
var bits = PostnetDigitBits[ch];
for (int i = 0; i < 5; i++)
{
bool full = bits[i] == '1';
rects.Add(full
? new RadzenBarcode.BarcodeRect(x, 0, 1, fullH)
: new RadzenBarcode.BarcodeRect(x, halfY, 1, halfH));
x += 2;
}
}
// Stop frame bar (full)
rects.Add(new RadzenBarcode.BarcodeRect(x, 0, 1, fullH));
x += 1;
var vbWidth = x + Math.Max(0, quietZone);
if (vbWidth <= 0) vbWidth = 1;
return (rects, vbWidth);
}
// RM4SCC patterns and symbol matrix from Wikipedia:
// Top patterns (values 1..6) and Bottom patterns (values 1..6) are:
// 1=0011, 2=0101, 3=0110, 4=1001, 5=1010, 6=1100
static readonly string[] Rm4Patterns = new[] { "0011", "0101", "0110", "1001", "1010", "1100" };
// Matrix indexed by [topValue-1, bottomValue-1]
static readonly char[,] Rm4Matrix = new char[6, 6]
{
{ '0', '1', '2', '3', '4', '5' },
{ '6', '7', '8', '9', 'A', 'B' },
{ 'C', 'D', 'E', 'F', 'G', 'H' },
{ 'I', 'J', 'K', 'L', 'M', 'N' },
{ 'O', 'P', 'Q', 'R', 'S', 'T' },
{ 'U', 'V', 'W', 'X', 'Y', 'Z' }
};
static readonly Dictionary<char, (string top, string bottom)> Rm4CharToBits = BuildRm4CharToBits();
static Dictionary<char, (string top, string bottom)> BuildRm4CharToBits()
{
var dict = new Dictionary<char, (string top, string bottom)>();
for (int r = 0; r < 6; r++)
{
for (int c = 0; c < 6; c++)
{
dict[Rm4Matrix[r, c]] = (Rm4Patterns[r], Rm4Patterns[c]);
}
}
return dict;
}
public static (IReadOnlyList<RadzenBarcode.BarcodeRect> bars, double vbWidth) EncodeRm4scc(string value, double barHeight, int quietZone, out string checksumText)
{
var text = new string(value.Where(char.IsLetterOrDigit).ToArray()).ToUpperInvariant();
if (string.IsNullOrEmpty(text))
{
throw new ArgumentException("RM4SCC requires alphanumeric input.");
}
// Only symbols present in the Wikipedia table (0-9, A-Z).
foreach (var ch in text)
{
if (!(ch is >= '0' and <= '9') && !(ch is >= 'A' and <= 'Z'))
{
throw new ArgumentException($"RM4SCC does not support character '{ch}'.");
}
}
// Compute checksum per Wikipedia: sum top values and bottom values separately, mod 6, 0 => 6.
int sumTop = 0;
int sumBottom = 0;
foreach (var ch in text)
{
if (!Rm4CharToBits.TryGetValue(ch, out var bits))
{
throw new ArgumentException($"RM4SCC does not support character '{ch}'.");
}
int t = Array.IndexOf(Rm4Patterns, bits.top) + 1;
int b = Array.IndexOf(Rm4Patterns, bits.bottom) + 1;
sumTop += t;
sumBottom += b;
}
int topVal = sumTop % 6;
topVal = topVal == 0 ? 6 : topVal;
int bottomVal = sumBottom % 6;
bottomVal = bottomVal == 0 ? 6 : bottomVal;
var checkChar = Rm4Matrix[topVal - 1, bottomVal - 1];
checksumText = checkChar.ToString();
// Encode start + data + checksum + stop.
// Start/stop are single bars; we use ascender for start and descender for stop.
double h = barHeight;
double third = h / 3.0;
double trackerY = third;
double trackerH = third;
RadzenBarcode.BarcodeRect BarRect(double x, bool top, bool bottom)
{
return (top, bottom) switch
{
(false, false) => new RadzenBarcode.BarcodeRect(x, trackerY, 1, trackerH), // tracker
(true, false) => new RadzenBarcode.BarcodeRect(x, 0, 1, trackerY + trackerH), // ascender (top + tracker)
(false, true) => new RadzenBarcode.BarcodeRect(x, trackerY, 1, h - trackerY), // descender (tracker + bottom)
(true, true) => new RadzenBarcode.BarcodeRect(x, 0, 1, h), // full
};
}
double xPos = Math.Max(0, quietZone);
var rects = new List<RadzenBarcode.BarcodeRect>();
// Start bar (ascender)
rects.Add(BarRect(xPos, top: true, bottom: false));
xPos += 2;
void AddSymbol(char ch)
{
var (topBits, bottomBits) = Rm4CharToBits[ch];
for (int i = 0; i < 4; i++)
{
bool top = topBits[i] == '1';
bool bottom = bottomBits[i] == '1';
rects.Add(BarRect(xPos, top, bottom));
xPos += 2;
}
}
foreach (var ch in text) AddSymbol(ch);
AddSymbol(checkChar);
// Stop bar (descender)
rects.Add(BarRect(xPos, top: false, bottom: true));
xPos += 1;
var vbWidth = xPos + Math.Max(0, quietZone);
if (vbWidth <= 0) vbWidth = 1;
return (rects, vbWidth);
}
public static string EncodeMsiPlessey(string value, out string checksumText)
{
var digits = new string(value.Where(char.IsDigit).ToArray());
if (digits.Length == 0) throw new ArgumentException("Plessey (MSI) requires numeric input.");
// Mod 10 (Luhn) check digit (common)
int check = ComputeLuhnCheckDigit(digits);
checksumText = check.ToString(CultureInfo.InvariantCulture);
digits += checksumText;
// MSI map from Wikipedia.
static string DigitMap(char d) => d switch
{
'0' => "100100100100",
'1' => "100100100110",
'2' => "100100110100",
'3' => "100100110110",
'4' => "100110100100",
'5' => "100110100110",
'6' => "100110110100",
'7' => "100110110110",
'8' => "110100100100",
'9' => "110100100110",
_ => throw new ArgumentException("MSI requires numeric input.")
};
var sb = new StringBuilder();
sb.Append("110"); // start
foreach (var ch in digits) sb.Append(DigitMap(ch));
sb.Append("1001"); // stop
return sb.ToString();
}
static int ComputeLuhnCheckDigit(string digits)
{
int sum = 0;
bool dbl = true;
for (int i = digits.Length - 1; i >= 0; i--)
{
int d = digits[i] - '0';
if (dbl)
{
d *= 2;
if (d > 9) d -= 9;
}
sum += d;
dbl = !dbl;
}
return (10 - (sum % 10)) % 10;
}
public static IReadOnlyList<int> EncodeTelepen(string value, out string checksumText)
{
// Telepen algorithm per Wikipedia: even parity bytes, little-endian bit order, modulo-127 checksum.
var bytes = Encoding.ASCII.GetBytes(value ?? string.Empty);
int sum = 0;
for (int i = 0; i < bytes.Length; i++)
{
if (bytes[i] > 0x7F) throw new ArgumentException("Telepen supports ASCII only.");
sum = (sum + bytes[i]) % 127;
}
int check = (127 - sum) % 127;
checksumText = check.ToString(CultureInfo.InvariantCulture);
// Build payload: start '_' + data + checksum byte + stop 'z'
var payload = new List<byte>(bytes.Length + 3) { (byte)'_' };
payload.AddRange(bytes);
payload.Add((byte)check);
payload.Add((byte)'z');
// Build bit stream LSB-first with even parity bit as MSB.
var bitStream = new List<int>(payload.Count * 8);
foreach (var b0 in payload)
{
int b = b0 & 0x7F;
int ones = CountBits(b);
int parityBit = (ones % 2 == 0) ? 0 : 1; // make total even
int byteWithParity = b | (parityBit << 7);
for (int i = 0; i < 8; i++)
{
bitStream.Add((byteWithParity >> i) & 1);
}
}
// Encode bit stream into bar/space widths (narrow=1, wide=3).
// We produce alternating bar/space widths list, starting with bar.
const int narrow = 1;
const int wide = 3;
var widths = new List<int>();
int idx = 0;
while (idx < bitStream.Count)
{
if (bitStream[idx] == 1)
{
// "1" => narrow bar, narrow space
widths.Add(narrow);
widths.Add(narrow);
idx += 1;
continue;
}
// starts with 0
if (idx + 1 < bitStream.Count && bitStream[idx + 1] == 0)
{
// "00" => wide bar, narrow space
widths.Add(wide);
widths.Add(narrow);
idx += 2;
continue;
}
if (idx + 2 < bitStream.Count && bitStream[idx] == 0 && bitStream[idx + 1] == 1 && bitStream[idx + 2] == 0)
{
// "010" => wide bar, wide space
widths.Add(wide);
widths.Add(wide);
idx += 3;
continue;
}
// General block 0 1^k 0 with k>=2
if (idx + 3 >= bitStream.Count || bitStream[idx] != 0 || bitStream[idx + 1] != 1)
{
throw new ArgumentException("Invalid Telepen bit stream.");
}
int j = idx + 1;
while (j < bitStream.Count && bitStream[j] == 1) j++;
if (j >= bitStream.Count || bitStream[j] != 0)
{
throw new ArgumentException("Invalid Telepen bit stream.");
}
int k = j - (idx + 1); // number of 1s
if (k < 2) throw new ArgumentException("Invalid Telepen bit stream.");
// leading "01" => narrow bar, wide space
widths.Add(narrow);
widths.Add(wide);
// middle extra 1s (k-2) => narrow bar, narrow space
for (int m = 0; m < k - 2; m++)
{
widths.Add(narrow);
widths.Add(narrow);
}
// trailing "10" => narrow bar, wide space
widths.Add(narrow);
widths.Add(wide);
idx = j + 1;
}
return widths;
}
static int CountBits(int v)
{
int c = 0;
while (v != 0)
{
c += v & 1;
v >>= 1;
}
return c;
return string.Empty;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,11 +7,11 @@
<section @ref=@Element style=@Style @attributes=@Attributes class=@GetCssClass() id=@GetId() tabindex="0">
@if(AllowPaging && (PagerPosition == PagerPosition.Top || PagerPosition == PagerPosition.TopAndBottom))
{
<RadzenStack class="rz-carousel-pager rz-carousel-pager-top" Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" JustifyContent="JustifyContent.Center" Wrap="FlexWrap.Wrap">
<RadzenStack class="rz-carousel-pager rz-carousel-pager-top" Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" JustifyContent="JustifyContent.Center" Wrap="FlexWrap.Wrap" role="list">
@foreach (var item in items)
{
var index = items.IndexOf(item);
<a @onclick="@(args => Navigate(index))" class="rz-carousel-pager-button @(index == selectedIndex ? "rz-state-active" : "")"></a>
<button type="button" role="listitem" @onclick="@(args => Navigate(index))" class="rz-carousel-pager-button @(index == selectedIndex ? "rz-state-active" : "")" aria-label="@string.Format(PagerButtonAriaLabelFormat, index + 1)"></button>
}
</RadzenStack>
}
@@ -30,11 +30,11 @@
</ul>
@if(AllowPaging && (PagerPosition == PagerPosition.Bottom || PagerPosition == PagerPosition.TopAndBottom))
{
<RadzenStack class="rz-carousel-pager rz-carousel-pager-bottom" Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" JustifyContent="JustifyContent.Center" Wrap="FlexWrap.Wrap">
<RadzenStack class="rz-carousel-pager rz-carousel-pager-bottom" Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" JustifyContent="JustifyContent.Center" Wrap="FlexWrap.Wrap" role="list">
@foreach (var item in items)
{
var index = items.IndexOf(item);
<a @onclick="@(args => Navigate(index))" class="rz-carousel-pager-button @(index == selectedIndex ? "rz-state-active" : "")"></a>
<button type="button" role="listitem" @onclick="@(args => Navigate(index))" class="rz-carousel-pager-button @(index == selectedIndex ? "rz-state-active" : "")" aria-label="@string.Format(PagerButtonAriaLabelFormat, index + 1)"></button>
}
</RadzenStack>
}

View File

@@ -179,6 +179,12 @@ namespace Radzen.Blazor
[Parameter]
public EventCallback<int> Change { get; set; }
/// <summary>
/// Gets or sets the pager button aria-label format. Use {0} for the 1-based slide index.
/// </summary>
[Parameter]
public string PagerButtonAriaLabelFormat { get; set; } = "Go to slide {0}";
/// <inheritdoc />
public override async Task SetParametersAsync(ParameterView parameters)
{

View File

@@ -55,9 +55,9 @@ namespace Radzen.Blazor
/// <summary>
/// Gets or sets the color scheme used to assign colors to chart series.
/// Determines the palette of colors applied sequentially to each series when series-specific colors are not set.
/// Available schemes include Pastel, Palette (default), Monochrome, and custom color schemes.
/// Available schemes include Pastel (default), Palette, Monochrome, and custom color schemes.
/// </summary>
/// <value>The color scheme. Default uses the Palette scheme.</value>
/// <value>The color scheme. Default uses the Pastel scheme.</value>
[Parameter]
public ColorScheme ColorScheme { get; set; }

View File

@@ -5,12 +5,14 @@
@inherits FormComponent<TValue>
@if (Visible)
{
<div @ref="@Element" @attributes="Attributes" class="@GetCssClass()" @onkeypress=@OnKeyPress @onkeypress:preventDefault style="@Style" tabindex="@(Disabled || ReadOnly ? "-1" : $"{TabIndex}")" id="@GetId()">
<div @ref="@Element" @attributes="Attributes" class="@GetCssClass()" @onkeypress=@OnKeyPress @onkeypress:preventDefault @onclick=@Toggle @onclick:preventDefault style="@Style"
role="checkbox" aria-checked="@CheckBoxAriaChecked" aria-disabled="@(Disabled || ReadOnly ? "true" : "false")"
tabindex="@(Disabled || ReadOnly ? "-1" : $"{TabIndex}")" id="@GetId()">
<div class="rz-helper-hidden-accessible">
<input type="checkbox" @onchange=@Toggle value=@CheckBoxValue name=@Name id=@Name checked=@CheckBoxChecked aria-checked="@((Value as bool? == true).ToString().ToLowerInvariant())" @attributes="InputAttributes"
tabindex="-1" readonly="@ReadOnly">
</div>
<div class=@BoxClass @onclick=@Toggle @onclick:preventDefault>
<div class=@BoxClass>
<span class=@IconClass></span>
</div>
</div>

View File

@@ -59,6 +59,8 @@ namespace Radzen.Blazor
bool CheckBoxChecked => object.Equals(Value, true);
string CheckBoxAriaChecked => object.Equals(Value, true) ? "true" : Value == null ? "mixed" : "false";
/// <inheritdoc />
protected override string GetComponentCssClass() => GetClassList("rz-chkbox").ToString();

View File

@@ -15,23 +15,27 @@
@if (Visible)
{
<div @ref="@Element" style="@Style" @attributes="Attributes" class="@GetCssClass()" id="@GetId()"
role="checkboxgroup" @onfocus=@OnFocus @onblur=@OnBlur
role="checkboxgroup" aria-disabled="@(Disabled ? "true" : "false")" @onfocus=@OnFocus @onblur=@OnBlur
tabindex="@(Disabled ? "-1" : $"{TabIndex}")" @onkeydown="@(args => OnKeyPress(args))" @onkeydown:preventDefault=preventKeyPress @onkeydown:stopPropagation>
@if (AllowSelectAll)
{
<div class="rz-multiselect-header rz-helper-clearfix" @onclick:preventDefault>
<RadzenCheckBox ReadOnly="@ReadOnly" Disabled="@Disabled" Name="SelectAll" TValue="bool?" Value="@IsAllSelected()" Change="@SelectAll"
InputAttributes="@(new Dictionary<string,object>(){ { "aria-label", SelectAllText ?? "" }})" />
<span @onclick="@(() => SelectAll(!IsAllSelected() ?? true))" class="rz-chkbox-label" style="cursor:pointer">@SelectAllText</span>
<button type="button" @onclick="@(() => SelectAll(!IsAllSelected() ?? true))" class="rz-chkbox-label" disabled="@(Disabled || ReadOnly)">@SelectAllText</button>
</div>
}
<RadzenStack Orientation="@Orientation" JustifyContent="@JustifyContent" AlignItems="@AlignItems" Gap="@Gap" Wrap="@Wrap">
@foreach (var item in allItems.Where(i => i.Visible))
{
<div @ref="@item.Element" id="@item.GetItemId()" @onclick="@(args => SelectItem(item))" @attributes="item.Attributes" class="@item.GetItemCssClass()" style="@item.Style" role="checkbox" aria-checked=@(IsSelected(item)? "true" : "false") aria-label="@item.Text">
<div @ref="@item.Element" id="@item.GetItemId()" @onclick="@(args => SelectItem(item))" @onkeydown="@(args => OnItemKeyDown(args, item))"
@attributes="item.Attributes" class="@item.GetItemCssClass()" style="@item.Style"
role="checkbox" tabindex="@(Disabled || item.Disabled ? "-1" : "0")"
aria-checked=@(IsSelected(item)? "true" : "false") aria-label="@item.Text"
aria-disabled="@(Disabled || item.Disabled ? "true" : "false")">
<div class="rz-chkbox">
<div class="rz-helper-hidden-accessible">
<input type="checkbox" name="@Name" value="@item.Value" disabled="@Disabled" readonly="@ReadOnly" tabindex="-1" aria-label="@(item.Text + " " + item.Value)" aria-checked="@(IsSelected(item).ToString().ToLowerInvariant())">
<input type="checkbox" name="@Name" value="@item.Value" disabled="@(Disabled || item.Disabled)" readonly="@ReadOnly" tabindex="-1" aria-label="@(item.Text + " " + item.Value)" aria-checked="@(IsSelected(item).ToString().ToLowerInvariant())">
</div>
<div class=@ItemClass(item)>
<span class=@IconClass(item)></span>

View File

@@ -332,6 +332,15 @@ namespace Radzen.Blazor
StateHasChanged();
}
async Task OnItemKeyDown(KeyboardEventArgs args, RadzenCheckBoxListItem<TValue> item)
{
var key = args.Code != null ? args.Code : args.Key;
if (key == "Enter" || key == "Space")
{
await SelectItem(item);
}
}
bool focused;
int focusedIndex = -1;
bool preventKeyPress = true;

View File

@@ -5,14 +5,18 @@
@if (Visible)
{
<div @ref=@Element style=@Style @onclick=@Toggle @attributes=@Attributes class=@GetCssClass() id=@GetId() tabindex="@(Disabled ? -1 : TabIndex)" @onkeypress="@(args => OnKeyPress(args, Toggle()))" @onkeypress:preventDefault=preventKeyPress @onkeypress:stopPropagation>
<div @ref=@Element style=@Style @onclick=@Toggle @attributes=@Attributes class=@GetCssClass() id=@GetId()
role="button" aria-haspopup="dialog" aria-expanded="@isPopupOpen.ToString().ToLowerInvariant()" aria-controls="@($"{GetId()}-popup")" aria-disabled="@(Disabled.ToString().ToLowerInvariant())"
tabindex="@(Disabled ? -1 : TabIndex)" @onkeypress="@(args => OnKeyPress(args, Toggle()))" @onkeypress:preventDefault=preventKeyPress @onkeypress:stopPropagation>
@if (Icon != null)
{
<i class="notranslate rzi" style="@(!string.IsNullOrEmpty(IconColor) ? $"color:{IconColor}" : null)">@Icon</i>
}
<div class="rz-colorpicker-value" style="background-color: @Value" ></div>
<button aria-label="@ToggleAriaLabel" type="button" tabindex="-1" class="rz-colorpicker-trigger" disabled=@Disabled @onclick:preventDefault><i class="notranslate rzi" /></button>
<Popup Lazy=@(PopupRenderMode == PopupRenderMode.OnDemand) @ref=@Popup class="rz-colorpicker-popup" Close=@OnClosePopup Open=@Open>
<Popup id="@($"{GetId()}-popup")" Lazy=@(PopupRenderMode == PopupRenderMode.OnDemand) @ref=@Popup
role="dialog" aria-label="@PopupAriaLabel"
class="rz-colorpicker-popup" Close=@OnClosePopup Open=@OnPopupOpen>
@if (ShowHSV)
{
<Draggable class="rz-saturation-picker rz-colorpicker-section" style=@($"background-color: hsl({(HueHandleLeft * 360).ToInvariantString()}, 100%, 50%)") Drag=@OnSaturationMove

View File

@@ -42,6 +42,13 @@ namespace Radzen.Blazor
[Parameter]
public string ToggleAriaLabel { get; set; } = "Toggle";
/// <summary>
/// Gets or sets the popup aria label text.
/// </summary>
/// <value>The popup aria label text.</value>
[Parameter]
public string PopupAriaLabel { get; set; } = "Color picker";
/// <summary>
/// Gets or sets the open callback.
/// </summary>
@@ -113,6 +120,7 @@ namespace Radzen.Blazor
public string ButtonText { get; set; } = "OK";
Popup Popup { get; set; } = default!;
bool isPopupOpen;
internal event EventHandler<string>? SelectedColorChanged;
@@ -381,6 +389,7 @@ namespace Radzen.Blazor
async Task OnClosePopup()
{
isPopupOpen = false;
if (ShowButton)
{
SetInitialValue();
@@ -389,6 +398,12 @@ namespace Radzen.Blazor
await Close.InvokeAsync(null);
}
async Task OnPopupOpen()
{
isPopupOpen = true;
await Open.InvokeAsync(null);
}
/// <summary>
/// Gets or sets a value indicating whether button is shown.
/// </summary>

View File

@@ -1,4 +1,5 @@
@using Radzen.Blazor.Rendering
<div class="rz-colorpicker-item @(isSelected ? "selected" : "")" style="background-color: @Background" @onmousedown:preventDefault @onclick=@OnClick
role="button" aria-label="@Value" aria-disabled="@(ColorPicker?.Disabled == true ? "true" : "false")"
tabindex="@(ColorPicker?.Disabled == true ? -1 : 0)" @onkeypress="@(args => OnKeyPress(args, OnClick()))" @onkeypress:preventDefault=preventKeyPress @onkeypress:stopPropagation></div>

View File

@@ -189,6 +189,8 @@ else if (Filter != null)
}
Filter.FilterValue = null;
Filter.Type = property.FilterPropertyType;
var defaultOperator = typeof(System.Collections.IEnumerable).IsAssignableFrom(property.FilterPropertyType) ? FilterOperator.Contains : default(FilterOperator);
if (property.GetFilterOperators().Any(o => o == property.FilterOperator))

View File

@@ -35,9 +35,9 @@
{
<div class="rz-group-header-item">
<span class="rz-group-header-item-title">@gd.GetTitle()</span>
<a id="@(GetId() + "rg")" aria-label="@RemoveGroupAriaLabel" @onclick:preventDefault="true" @onclick=@(args => RemoveGroupAsync(gd)) role="button" class="rz-dialog-titlebar-icon rz-dialog-titlebar-close">
<button type="button" id="@(GetId() + "rg")" aria-label="@RemoveGroupAriaLabel" @onclick=@(args => RemoveGroupAsync(gd)) class="rz-dialog-titlebar-icon rz-dialog-titlebar-close">
<span class="notranslate rzi rzi-times"></span>
</a>
</button>
</div>
}
</div>
@@ -123,9 +123,12 @@
<span class="rz-column-title">
@if(ShowExpandAll && ExpandMode == DataGridExpandMode.Multiple)
{
<a title=@TitleAttribute() aria-label="@TitleAttribute()" @onclick:preventDefault="true" @onclick="_ => this.ToggleAllRowsExpand()" @onclick:stopPropagation="true">
<span class="@(allRowsExpanded ? "rzi-chevron-circle-down" : "rzi-chevron-circle-right")"></span>
</a>
<button type="button" title=@TitleAttribute() aria-label="@TitleAttribute()"
aria-expanded="@(allRowsExpanded.ToString().ToLowerInvariant())"
class="rz-button rz-button-sm rz-button-icon-only rz-variant-text rz-base rz-shade-default"
@onclick="_ => this.ToggleAllRowsExpand()" @onclick:stopPropagation="true">
<span class="@(allRowsExpanded ? "rzi-chevron-circle-down" : "rzi-chevron-circle-right")"></span>
</button>
}
else
{
@@ -329,7 +332,10 @@
{
if (filterMode == FilterMode.Simple)
{
<button aria-label="@FilterToggleAriaLabel" class="rz-button rz-button-md rz-button-icon-only rz-variant-flat rz-base rz-shade-default @(column.HasActiveFilter() ? "rz-grid-filter-active" : "")" onclick="@($"Radzen.togglePopup(this.parentNode, '{PopupID}{column.GetFilterProperty()}')")">
<button type="button" aria-label="@FilterToggleAriaLabel" aria-controls="@($"{PopupID}{column.GetFilterProperty()}")"
aria-haspopup="dialog" aria-expanded="false"
class="rz-button rz-button-md rz-button-icon-only rz-variant-flat rz-base rz-shade-default @(column.HasActiveFilter() ? "rz-grid-filter-active" : "")"
onclick="@($"Radzen.togglePopup(this.parentNode, '{PopupID}{column.GetFilterProperty()}')")">
<i class="notranslate rzi">date_range</i>
</button>
<span>
@@ -338,16 +344,16 @@
@if (filterValue != null && filters.Any(d => d.Property == column.GetFilterProperty()))
{
<span class="rz-current-filter">@string.Format("{0:" + getFilterDateFormat(column) + "}", filterValue)</span>
<i @onclick="@((args) => ClearFilter(column))" class="notranslate rzi rz-cell-filter-clear">close</i>
<button type="button" class="notranslate rzi rz-cell-filter-clear" aria-label="@ClearFilterText" @onclick="@((args) => ClearFilter(column))">close</button>
}
else if ((filterOperator == FilterOperator.IsNull || filterOperator == FilterOperator.IsNotNull) && filters.Any(d => d.Property == column.GetFilterProperty()))
{
<span class="rz-current-filter">@column.GetFilterOperatorText(filterOperator)</span>
<i @onclick="@((args) => ClearFilter(column))" class="notranslate rzi rz-cell-filter-clear">close</i>
<button type="button" class="notranslate rzi rz-cell-filter-clear" aria-label="@ClearFilterText" @onclick="@((args) => ClearFilter(column))">close</button>
}
</span>
<div id="@($"{PopupID}{column.GetFilterProperty()}")" class="rz-overlaypanel rz-grid-date-filter"
style="display:none;" tabindex="0">
style="display:none;" tabindex="0" role="dialog" aria-label="@FilterText">
<div class="rz-overlaypanel-content">
<div class="rz-date-filter">
@@ -357,62 +363,82 @@
<ul class="rz-listbox-list">
@if (column.GetFilterOperators().Contains(FilterOperator.Equals))
{
<li class="@(DateFilterOperatorStyle(column, FilterOperator.Equals))" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.Equals))" style="display: block;">
<span>@EqualsText</span>
<li role="presentation">
<button type="button" class="@(DateFilterOperatorStyle(column, FilterOperator.Equals)) rz-filter-menu-item" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.Equals))">
<span>@EqualsText</span>
</button>
</li>
}
@if (column.GetFilterOperators().Contains(FilterOperator.NotEquals))
{
<li class="@(DateFilterOperatorStyle(column, FilterOperator.NotEquals))" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.NotEquals))" style="display: block;">
<span>@NotEqualsText</span>
<li role="presentation">
<button type="button" class="@(DateFilterOperatorStyle(column, FilterOperator.NotEquals)) rz-filter-menu-item" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.NotEquals))">
<span>@NotEqualsText</span>
</button>
</li>
}
@if (column.GetFilterOperators().Contains(FilterOperator.LessThan))
{
<li class="@(DateFilterOperatorStyle(column, FilterOperator.LessThan))" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.LessThan))" style="display: block;">
<span>@LessThanText</span>
<li role="presentation">
<button type="button" class="@(DateFilterOperatorStyle(column, FilterOperator.LessThan)) rz-filter-menu-item" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.LessThan))">
<span>@LessThanText</span>
</button>
</li>
}
@if (column.GetFilterOperators().Contains(FilterOperator.LessThanOrEquals))
{
<li class="@(DateFilterOperatorStyle(column, FilterOperator.LessThanOrEquals))" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.LessThanOrEquals))" style="display: block;">
<span>@LessThanOrEqualsText</span>
<li role="presentation">
<button type="button" class="@(DateFilterOperatorStyle(column, FilterOperator.LessThanOrEquals)) rz-filter-menu-item" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.LessThanOrEquals))">
<span>@LessThanOrEqualsText</span>
</button>
</li>
}
@if (column.GetFilterOperators().Contains(FilterOperator.GreaterThan))
{
<li class="@(DateFilterOperatorStyle(column, FilterOperator.GreaterThan))" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.GreaterThan))" style="display: block;">
<span>@GreaterThanText</span>
<li role="presentation">
<button type="button" class="@(DateFilterOperatorStyle(column, FilterOperator.GreaterThan)) rz-filter-menu-item" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.GreaterThan))">
<span>@GreaterThanText</span>
</button>
</li>
}
@if (column.GetFilterOperators().Contains(FilterOperator.GreaterThanOrEquals))
{
<li class="@(DateFilterOperatorStyle(column, FilterOperator.GreaterThanOrEquals))" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.GreaterThanOrEquals))" style="display: block;">
<span>@GreaterThanOrEqualsText</span>
<li role="presentation">
<button type="button" class="@(DateFilterOperatorStyle(column, FilterOperator.GreaterThanOrEquals)) rz-filter-menu-item" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.GreaterThanOrEquals))">
<span>@GreaterThanOrEqualsText</span>
</button>
</li>
}
@if (column.GetFilterOperators().Contains(FilterOperator.IsEmpty))
{
<li class="@(DateFilterOperatorStyle(column, FilterOperator.IsEmpty))" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.IsEmpty))" style="display: block;">
<span>@IsEmptyText</span>
<li role="presentation">
<button type="button" class="@(DateFilterOperatorStyle(column, FilterOperator.IsEmpty)) rz-filter-menu-item" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.IsEmpty))">
<span>@IsEmptyText</span>
</button>
</li>
}
@if (column.GetFilterOperators().Contains(FilterOperator.IsNotEmpty))
{
<li class="@(DateFilterOperatorStyle(column, FilterOperator.IsNotEmpty))" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.IsNotEmpty))" style="display: block;">
<span>@IsNotEmptyText</span>
<li role="presentation">
<button type="button" class="@(DateFilterOperatorStyle(column, FilterOperator.IsNotEmpty)) rz-filter-menu-item" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.IsNotEmpty))">
<span>@IsNotEmptyText</span>
</button>
</li>
}
@if (column.GetFilterOperators().Contains(FilterOperator.IsNull))
{
<li class="@(DateFilterOperatorStyle(column, FilterOperator.IsNull))" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.IsNull))" style="display: block;">
<span>@IsNullText</span>
<li role="presentation">
<button type="button" class="@(DateFilterOperatorStyle(column, FilterOperator.IsNull)) rz-filter-menu-item" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.IsNull))">
<span>@IsNullText</span>
</button>
</li>
}
@if (column.GetFilterOperators().Contains(FilterOperator.IsNotNull))
{
<li class="@(DateFilterOperatorStyle(column, FilterOperator.IsNotNull))" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.IsNotNull))" style="display: block;">
<span>@IsNotNullText</span>
<li role="presentation">
<button type="button" class="@(DateFilterOperatorStyle(column, FilterOperator.IsNotNull)) rz-filter-menu-item" @onclick="@((args) => ApplyDateFilterByFilterOperator(column, FilterOperator.IsNotNull))">
<span>@IsNotNullText</span>
</button>
</li>
}
</ul>
@@ -502,9 +528,9 @@
else
{
<input autocomplete="off" aria-label=@(column.Title + FilterValueAriaLabel + column.GetFilterValue()) disabled=@(!column.CanSetFilterValue()) id="@(getFilterInputId(column))" @onchange="@((args) => OnFilter(args, column))" @onkeydown="@((args) => OnFilterKeyPress(args, column))" value="@column.GetFilterValue()" type="text" placeholder="@column.GetFilterPlaceholder()" class="rz-textbox" style="width: 100%;" />
@if (column.GetFilterValue() != null && filters.Any(d => d.Property == column.Property && d.FilterProperty == column.FilterProperty))
@if (column.GetFilterValue() != null && filters.Any(d => d.Property == column.Property && $"{d.FilterProperty}" == $"{column.FilterProperty}"))
{
<i @onclick="@((args) => ClearFilter(column))" class="notranslate rzi rz-cell-filter-clear" style="position:absolute;inset-inline-end:10px;">close</i>
<button type="button" class="notranslate rzi rz-cell-filter-clear" style="position:absolute;inset-inline-end:10px;" aria-label="@ClearFilterText" @onclick="@((args) => ClearFilter(column))">close</button>
}
}
}
@@ -607,9 +633,12 @@
@if (this.LoadChildData.HasDelegate && this.ShowExpandColumn && this.allColumns.IndexOf(column) == 0)
{
<span class="rz-cell-toggle">
<a id="@(GetId() + "exp")" aria-label="@ExpandChildItemAriaLabel" class="@(getExpandIconCssClass(this, Item))" style="@(getExpandIconStyle(this, Item, rowArgs.Item1.Expandable))" @onclick:preventDefault="true" @onclick="_ => this.ExpandItem(Item)" @onclick:stopPropagation="true">
<span class="@(this.ExpandedItemStyle(Item))"></span>
</a>
<button type="button" id="@(GetId() + "exp")" aria-label="@ExpandChildItemAriaLabel"
class="rz-button rz-button-sm rz-button-icon-only rz-variant-text rz-base rz-shade-default"
style="@(getExpandIconStyle(this, Item, rowArgs.Item1.Expandable))"
@onclick="_ => this.ExpandItem(Item)" @onclick:stopPropagation="true">
<span class="@(getExpandIconCssClass(this, Item)) @(this.ExpandedItemStyle(Item))"></span>
</button>
<span class="@column.GetCellClass()" @attributes="@spanAttributes">
@if (Item != null)
{

View File

@@ -10,6 +10,7 @@ using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Text.Json;
using System.Threading.Tasks;
@@ -892,7 +893,7 @@ namespace Radzen.Blazor
}
}
void ToggleColumns()
async Task ToggleColumns()
{
if (selectedColumns == null)
{
@@ -906,8 +907,13 @@ namespace Radzen.Blazor
c.SetVisible(selected.Contains(c));
}
PickedColumnsChanged.InvokeAsync(new DataGridPickedColumnsChangedEventArgs<TItem>() { Columns = selected });
await PickedColumnsChanged.InvokeAsync(new DataGridPickedColumnsChangedEventArgs<TItem>() { Columns = selected });
SaveSettings();
if (QueryOnlyVisibleColumns)
{
await Reload();
}
}
/// <summary>
@@ -1627,6 +1633,13 @@ namespace Radzen.Blazor
[Parameter]
public int ColumnsPickerMaxSelectedLabels { get; set; } = 2;
/// <summary>
/// Gets or sets a value indicating whether only visible columns are included in the query.
/// </summary>
/// <value><c>true</c> if only visible columns are included; otherwise, <c>false</c>.</value>
[Parameter]
public bool QueryOnlyVisibleColumns { get; set; }
/// <summary>
/// Gets or sets the column picker all columns text.
/// </summary>
@@ -2016,7 +2029,8 @@ namespace Radzen.Blazor
}
}
return view;
return QueryOnlyVisibleColumns ? view
.Select(allColumns.Where(c => c.GetVisible() && !string.IsNullOrEmpty(c.Property)).Select(c => c.Property)) : view;
}
}
@@ -2496,7 +2510,7 @@ namespace Radzen.Blazor
internal Tuple<GroupRowRenderEventArgs, IReadOnlyDictionary<string, object>> GroupRowAttributes(RadzenDataGridGroupRow<TItem> item)
{
var args = new Radzen.GroupRowRenderEventArgs() { Group = item.Group, FirstRender = firstRender };
var args = new Radzen.GroupRowRenderEventArgs() { Group = item.Group, FirstRender = firstRender, Expandable = item.GroupResult.Count > 0 };
if (GroupRowRender != null)
{
@@ -3576,6 +3590,59 @@ namespace Radzen.Blazor
}
}
if (expandedItems != null)
{
expandedItems.Clear();
}
if (editedItems != null)
{
editedItems.Clear();
}
if (editContexts != null)
{
editContexts.Clear();
}
if (childData != null)
{
childData.Clear();
}
if (selectedItems != null)
{
selectedItems.Clear();
}
if (rowSpans != null)
{
rowSpans.Clear();
}
if (columns != null)
{
columns.Clear();
}
if (allPickableColumns != null)
{
allPickableColumns.Clear();
}
if (allColumns != null)
{
allColumns.Clear();
}
if (childColumns != null)
{
childColumns.Clear();
}
_value = null;
Data = null;
GC.SuppressFinalize(this);
}

View File

@@ -1310,7 +1310,7 @@ namespace Radzen.Blazor
internal bool HasCustomFilter()
{
return GetFilterOperator() == FilterOperator.Custom && GetCustomFilterExpression() != null;
return GetFilterOperator() == FilterOperator.Custom && !string.IsNullOrEmpty(GetCustomFilterExpression());
}
internal bool HasActiveFilter()

View File

@@ -2,7 +2,8 @@
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.JSInterop
@inject IJSRuntime JSRuntime
<button title=@Column.GetFilterOperatorText(Column.GetFilterOperator()) class="@FilterIconStyle()" onclick="@($"Radzen.togglePopup(this.parentNode, '{filterId}')")">
<button title=@Column.GetFilterOperatorText(Column.GetFilterOperator()) class="@FilterIconStyle()" onclick="@($"Radzen.togglePopup(this.parentNode, '{filterId}')")"
aria-haspopup="menu" aria-controls="@filterId" aria-expanded="false">
<i class="notranslate rzi">@Grid.FilterIcon</i>
@if (Column.GetFilterOperator() == FilterOperator.DoesNotContain)
{
@@ -14,110 +15,144 @@
}
</button>
<div id="@($"{filterId}")" class="rz-overlaypanel"
style="display:none;" tabindex="0">
style="display:none;" tabindex="0" role="menu" aria-label="@Grid.FilterText">
<div class="rz-overlaypanel-content">
<ul class="rz-listbox-list">
@if (Column.FilterPropertyType != null && (QueryableExtension.IsEnumerable(Column.FilterPropertyType) || Column.FilterPropertyType == typeof(string)))
{
@if (Column.GetFilterOperators().Contains(FilterOperator.Contains))
{
<li class="@(FilterOperatorStyle(Column, FilterOperator.Contains))" @onclick="@(args => ApplyFilter(FilterOperator.Contains))" style="display: block;">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.Contains)</span><span>@Grid.ContainsText</span>
<li role="presentation">
<button type="button" class="@(FilterOperatorStyle(Column, FilterOperator.Contains)) rz-filter-menu-item" @onclick="@(args => ApplyFilter(FilterOperator.Contains))">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.Contains)</span><span>@Grid.ContainsText</span>
</button>
</li>
}
@if (Column.GetFilterOperators().Contains(FilterOperator.DoesNotContain))
{
<li class="@(FilterOperatorStyle(Column, FilterOperator.DoesNotContain))" @onclick="@(args => ApplyFilter(FilterOperator.DoesNotContain))" style="display: block;">
<span class="rz-filter-menu-symbol"><s>@Column.GetFilterOperatorSymbol(FilterOperator.DoesNotContain)</s></span><span>@Grid.DoesNotContainText</span>
<li role="presentation">
<button type="button" class="@(FilterOperatorStyle(Column, FilterOperator.DoesNotContain)) rz-filter-menu-item" @onclick="@(args => ApplyFilter(FilterOperator.DoesNotContain))">
<span class="rz-filter-menu-symbol"><s>@Column.GetFilterOperatorSymbol(FilterOperator.DoesNotContain)</s></span><span>@Grid.DoesNotContainText</span>
</button>
</li>
}
@if (Column.GetFilterOperators().Contains(FilterOperator.In))
{
<li class="@(FilterOperatorStyle(Column, FilterOperator.In))" @onclick="@(args => ApplyFilter(FilterOperator.In))" style="display: block;">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.In)</span><span>@Grid.InText</span>
<li role="presentation">
<button type="button" class="@(FilterOperatorStyle(Column, FilterOperator.In)) rz-filter-menu-item" @onclick="@(args => ApplyFilter(FilterOperator.In))">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.In)</span><span>@Grid.InText</span>
</button>
</li>
}
@if (Column.GetFilterOperators().Contains(FilterOperator.NotIn))
{
<li class="@(FilterOperatorStyle(Column, FilterOperator.NotIn))" @onclick="@(args => ApplyFilter(FilterOperator.NotIn))" style="display: block;">
<span class="rz-filter-menu-symbol"><s>@Column.GetFilterOperatorSymbol(FilterOperator.NotIn)</s></span><span>@Grid.NotInText</span>
<li role="presentation">
<button type="button" class="@(FilterOperatorStyle(Column, FilterOperator.NotIn)) rz-filter-menu-item" @onclick="@(args => ApplyFilter(FilterOperator.NotIn))">
<span class="rz-filter-menu-symbol"><s>@Column.GetFilterOperatorSymbol(FilterOperator.NotIn)</s></span><span>@Grid.NotInText</span>
</button>
</li>
}
@if (Column.GetFilterOperators().Contains(FilterOperator.StartsWith) && Column.FilterPropertyType == typeof(string))
{
<li class="@(FilterOperatorStyle(Column, FilterOperator.StartsWith))" @onclick="@(args => ApplyFilter(FilterOperator.StartsWith))" style="display: block;">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.StartsWith)</span><span>@Grid.StartsWithText</span>
<li role="presentation">
<button type="button" class="@(FilterOperatorStyle(Column, FilterOperator.StartsWith)) rz-filter-menu-item" @onclick="@(args => ApplyFilter(FilterOperator.StartsWith))">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.StartsWith)</span><span>@Grid.StartsWithText</span>
</button>
</li>
}
@if (Column.GetFilterOperators().Contains(FilterOperator.EndsWith) && Column.FilterPropertyType == typeof(string))
{
<li class="@(FilterOperatorStyle(Column, FilterOperator.EndsWith))" @onclick="@(args => ApplyFilter(FilterOperator.EndsWith))" style="display: block;">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.EndsWith)</span><span>@Grid.EndsWithText</span>
<li role="presentation">
<button type="button" class="@(FilterOperatorStyle(Column, FilterOperator.EndsWith)) rz-filter-menu-item" @onclick="@(args => ApplyFilter(FilterOperator.EndsWith))">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.EndsWith)</span><span>@Grid.EndsWithText</span>
</button>
</li>
}
}
@if (Column.GetFilterOperators().Contains(FilterOperator.Equals))
{
<li class="@(FilterOperatorStyle(Column, FilterOperator.Equals))" @onclick="@(args => ApplyFilter(FilterOperator.Equals))" style="display: block;">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.Equals)</span><span>@Grid.EqualsText</span>
<li role="presentation">
<button type="button" class="@(FilterOperatorStyle(Column, FilterOperator.Equals)) rz-filter-menu-item" @onclick="@(args => ApplyFilter(FilterOperator.Equals))">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.Equals)</span><span>@Grid.EqualsText</span>
</button>
</li>
}
@if (Column.GetFilterOperators().Contains(FilterOperator.NotEquals))
{
<li class="@(FilterOperatorStyle(Column, FilterOperator.NotEquals))" @onclick="@(args => ApplyFilter(FilterOperator.NotEquals))" style="display: block;">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.NotEquals)</span><span>@Grid.NotEqualsText</span>
<li role="presentation">
<button type="button" class="@(FilterOperatorStyle(Column, FilterOperator.NotEquals)) rz-filter-menu-item" @onclick="@(args => ApplyFilter(FilterOperator.NotEquals))">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.NotEquals)</span><span>@Grid.NotEqualsText</span>
</button>
</li>
}
@if (Column.GetFilterOperators().Contains(FilterOperator.LessThan))
{
<li class="@(FilterOperatorStyle(Column, FilterOperator.LessThan))" @onclick="@(args => ApplyFilter(FilterOperator.LessThan))" style="display: block;">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.LessThan)</span><span>@Grid.LessThanText</span>
<li role="presentation">
<button type="button" class="@(FilterOperatorStyle(Column, FilterOperator.LessThan)) rz-filter-menu-item" @onclick="@(args => ApplyFilter(FilterOperator.LessThan))">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.LessThan)</span><span>@Grid.LessThanText</span>
</button>
</li>
}
@if (Column.GetFilterOperators().Contains(FilterOperator.LessThanOrEquals))
{
<li class="@(FilterOperatorStyle(Column, FilterOperator.LessThanOrEquals))" @onclick="@(args => ApplyFilter(FilterOperator.LessThanOrEquals))" style="display: block;">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.LessThanOrEquals)</span><span>@Grid.LessThanOrEqualsText</span>
<li role="presentation">
<button type="button" class="@(FilterOperatorStyle(Column, FilterOperator.LessThanOrEquals)) rz-filter-menu-item" @onclick="@(args => ApplyFilter(FilterOperator.LessThanOrEquals))">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.LessThanOrEquals)</span><span>@Grid.LessThanOrEqualsText</span>
</button>
</li>
}
@if (Column.GetFilterOperators().Contains(FilterOperator.GreaterThan))
{
<li class="@(FilterOperatorStyle(Column, FilterOperator.GreaterThan))" @onclick="@(args => ApplyFilter(FilterOperator.GreaterThan))" style="display: block;">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.GreaterThan)</span><span>@Grid.GreaterThanText</span>
<li role="presentation">
<button type="button" class="@(FilterOperatorStyle(Column, FilterOperator.GreaterThan)) rz-filter-menu-item" @onclick="@(args => ApplyFilter(FilterOperator.GreaterThan))">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.GreaterThan)</span><span>@Grid.GreaterThanText</span>
</button>
</li>
}
@if (Column.GetFilterOperators().Contains(FilterOperator.GreaterThanOrEquals))
{
<li class="@(FilterOperatorStyle(Column, FilterOperator.GreaterThanOrEquals))" @onclick="@(args => ApplyFilter(FilterOperator.GreaterThanOrEquals))" style="display: block;">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.GreaterThanOrEquals)</span><span>@Grid.GreaterThanOrEqualsText</span>
<li role="presentation">
<button type="button" class="@(FilterOperatorStyle(Column, FilterOperator.GreaterThanOrEquals)) rz-filter-menu-item" @onclick="@(args => ApplyFilter(FilterOperator.GreaterThanOrEquals))">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.GreaterThanOrEquals)</span><span>@Grid.GreaterThanOrEqualsText</span>
</button>
</li>
}
@if (Column.GetFilterOperators().Contains(FilterOperator.IsNull))
{
<li class="@(FilterOperatorStyle(Column, FilterOperator.IsNull))" @onclick="@(args => ApplyFilter(FilterOperator.IsNull))" style="display: block;">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.IsNull)</span><span>@Grid.IsNullText</span>
<li role="presentation">
<button type="button" class="@(FilterOperatorStyle(Column, FilterOperator.IsNull)) rz-filter-menu-item" @onclick="@(args => ApplyFilter(FilterOperator.IsNull))">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.IsNull)</span><span>@Grid.IsNullText</span>
</button>
</li>
}
@if (Column.GetFilterOperators().Contains(FilterOperator.IsNotNull))
{
<li class="@(FilterOperatorStyle(Column, FilterOperator.IsNotNull))" @onclick="@(args => ApplyFilter(FilterOperator.IsNotNull))" style="display: block;">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.IsNotNull)</span><span>@Grid.IsNotNullText</span>
<li role="presentation">
<button type="button" class="@(FilterOperatorStyle(Column, FilterOperator.IsNotNull)) rz-filter-menu-item" @onclick="@(args => ApplyFilter(FilterOperator.IsNotNull))">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.IsNotNull)</span><span>@Grid.IsNotNullText</span>
</button>
</li>
}
@if (Column.GetFilterOperators().Contains(FilterOperator.IsEmpty) && Column.FilterPropertyType == typeof(string))
{
<li class="@(FilterOperatorStyle(Column, FilterOperator.IsEmpty))" @onclick="@(args => ApplyFilter(FilterOperator.IsEmpty))" style="display: block;">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.IsEmpty)</span><span>@Grid.IsEmptyText</span>
<li role="presentation">
<button type="button" class="@(FilterOperatorStyle(Column, FilterOperator.IsEmpty)) rz-filter-menu-item" @onclick="@(args => ApplyFilter(FilterOperator.IsEmpty))">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.IsEmpty)</span><span>@Grid.IsEmptyText</span>
</button>
</li>
}
@if (Column.GetFilterOperators().Contains(FilterOperator.IsNotEmpty) && Column.FilterPropertyType == typeof(string))
{
<li class="@(FilterOperatorStyle(Column, FilterOperator.IsNotEmpty))" @onclick="@(args => ApplyFilter(FilterOperator.IsNotEmpty))" style="display: block;">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.IsNotEmpty)</span><span>@Grid.IsNotEmptyText</span>
<li role="presentation">
<button type="button" class="@(FilterOperatorStyle(Column, FilterOperator.IsNotEmpty)) rz-filter-menu-item" @onclick="@(args => ApplyFilter(FilterOperator.IsNotEmpty))">
<span class="rz-filter-menu-symbol">@Column.GetFilterOperatorSymbol(FilterOperator.IsNotEmpty)</span><span>@Grid.IsNotEmptyText</span>
</button>
</li>
}
<li class="rz-multiselect-item" @onclick="@(args => ClearFilter())" style="display:block;">
<span class="rz-filter-menu-symbol">x</span><span>@Grid.ClearFilterText</span>
<li role="presentation">
<button type="button" class="rz-multiselect-item rz-filter-menu-item" @onclick="@(args => ClearFilter())">
<span class="rz-filter-menu-symbol">x</span><span>@Grid.ClearFilterText</span>
</button>
</li>
</ul>
</div>

View File

@@ -26,9 +26,15 @@
}
<td class="rz-col-icon">
<span class="rz-column-title"></span>
<a id="@(Grid.GridId() + Group.GetHashCode())" aria-label=@Grid.ExpandGroupAriaLabel @onclick:preventDefault="true" @onclick="@(_ => Grid.ExpandGroupItem(this, rowArgs?.Item1.Expanded ?? false))">
<span class="@(Grid.ExpandedGroupItemStyle(this, Grid.allGroupsExpanded != null ? Grid.allGroupsExpanded : rowArgs?.Item1.Expanded ?? false))"></span>
</a>
@if (rowArgs?.Item1.Expandable == true)
{
<button type="button" id="@(Grid.GridId() + Group.GetHashCode())" aria-label=@Grid.ExpandGroupAriaLabel
aria-expanded="@(Grid.IsGroupItemExpanded(this).ToString().ToLowerInvariant())"
class="rz-button rz-button-sm rz-button-icon-only rz-variant-text rz-base rz-shade-default"
@onclick="@(_ => Grid.ExpandGroupItem(this, rowArgs?.Item1.Expanded))">
<span class="@(Grid.ExpandedGroupItemStyle(this, Grid.allGroupsExpanded != null ? Grid.allGroupsExpanded : rowArgs?.Item1.Expanded))"></span>
</button>
}
</td>
}
<td colspan="@(TotalColumnCount + (Grid?.Groups.Count ?? 0) - 1 - Group.Level + ((Grid?.Template != null && Grid?.ShowExpandColumn == true) ? 1 : 0))">

View File

@@ -7,8 +7,11 @@
@if (RowIndex == Column.GetLevel())
{
<th rowspan="@(Column.GetRowSpan())" colspan="@(Column.GetColSpan())" @attributes="@Attributes" class="@CssClass" scope="col" style="@GetStyle()" @onmouseup=@(args => Grid.EndColumnReorder(args, ColumnIndex))>
<div @onclick='@((args) => Grid.OnSort(args, Column))' @onkeydown="OnSortKeyPressed">
<th rowspan="@(Column.GetRowSpan())" colspan="@(Column.GetColSpan())" @attributes="@Attributes" class="@CssClass"
scope="col" style="@GetStyle()" @onmouseup=@(args => Grid.EndColumnReorder(args, ColumnIndex))
aria-sort="@(Grid.AllowSorting && Column.Sortable ? (Column.GetSortOrder() == SortOrder.Ascending ? "ascending" : Column.GetSortOrder() == SortOrder.Descending ? "descending" : "none") : null)">
<div @onclick='@((args) => Grid.OnSort(args, Column))' @onkeydown="OnSortKeyPressed"
role="@(Grid.AllowSorting && Column.Sortable ? "button" : null)">
@if ((Grid.AllowColumnReorder && Column.Reorderable || Grid.AllowGrouping && Column.Groupable))
{
<span id="@(Grid.getColumnUniqueId(ColumnIndex) + "-drag")" class="rz-column-drag"
@@ -60,12 +63,15 @@
@{var filterMode = Column.FilterMode ?? Grid.FilterMode;}
@if (Grid.AllowFiltering && Column.Filterable && (filterMode == FilterMode.Advanced || filterMode == FilterMode.CheckBoxList))
{
<i @ref=@filterButton @onclick:stopPropagation="true" @onmousedown=@ToggleFilter
class="@getFilterIconCss(Column)" onclick=@getFilterOpen() @onclick:preventDefault="true">
<button type="button" @ref=@filterButton @onclick:stopPropagation="true" @onmousedown=@ToggleFilter
class="@getFilterIconCss(Column)" onclick=@getFilterOpen() @onclick:preventDefault="true"
aria-haspopup="dialog" aria-controls="@Column.GetColumnPopupID()" aria-expanded="false"
aria-label="@Grid.FilterToggleAriaLabel">
@Grid.FilterIcon
</i>
</button>
<Popup Lazy=@(Grid.FilterPopupRenderMode == PopupRenderMode.OnDemand) @ref=popup id="@($"{Column.GetColumnPopupID()}")" class="rz-overlaypanel"
role="dialog" aria-label="@Grid.FilterText"
style="display:none;min-width:250px;" @onkeydown="OnFilterPopupKeyPressed">
<div class="rz-overlaypanel-content">
@if (Column.FilterTemplate != null)

View File

@@ -27,9 +27,12 @@
<span class="rz-column-title"></span>
@if (rowArgs?.Item1.Expandable == true && !(Grid?.LoadChildData.HasDelegate ?? false))
{
<a id="@((Grid?.GridId() ?? "") + Item.GetHashCode())" aria-label="@(Grid?.ExpandChildItemAriaLabel ?? "")" @onclick:preventDefault="true" @onclick="@(_ => Grid?.ExpandItem(Item)!)" @onclick:stopPropagation>
<button type="button" id="@((Grid?.GridId() ?? "") + Item.GetHashCode())" aria-label="@(Grid?.ExpandChildItemAriaLabel ?? "")"
aria-expanded="@(Grid != null && Grid.IsRowExpanded(Item) ? "true" : "false")"
class="rz-button rz-button-sm rz-button-icon-only rz-variant-text rz-base rz-shade-default"
@onclick="@(_ => Grid?.ExpandItem(Item)!)" @onclick:stopPropagation>
<span class="@(Grid?.ExpandedItemStyle(Item) ?? "")"></span>
</a>
</button>
}
</td>
}

View File

@@ -17,27 +17,30 @@
}
@if (ShowButton)
{
<button aria-label="@ToggleAriaLabel" @onmousedown=@OnToggle class="@($"rz-datepicker-trigger{(ShowInput ? " rz-datepicker-field-button" : "")} rz-button rz-button-icon-only{(Disabled ? " rz-state-disabled" : "")} {ButtonClass}")" tabindex="-1" type="button">
<button aria-label="@ToggleAriaLabel" aria-haspopup="dialog" aria-controls="@PopupID" aria-expanded="false"
@onmousedown=@OnToggle class="@($"rz-datepicker-trigger{(ShowInput ? " rz-datepicker-field-button" : "")} rz-button rz-button-icon-only{(Disabled ? " rz-state-disabled" : "")} {ButtonClass}")" tabindex="-1" type="button">
<span aria-hidden="true" class="@ButtonClasses"></span><span class="rz-button-text"></span>
</button>
}
@if (AllowClear && HasValue && (ShowInput || !ShowButton))
{
<i class="notranslate rz-dropdown-clear-icon rzi rzi-times" @onclick="@Clear" @onclick:stopPropagation="true"></i>
<button type="button" class="notranslate rz-dropdown-clear-icon rzi rzi-times" aria-label="@ClearAriaLabel" @onclick="@Clear" @onclick:stopPropagation="true"></button>
}
}
<Popup id="@PopupID" Lazy=@(PopupRenderMode == PopupRenderMode.OnDemand) @ref=@popup style=@PopupStyle class="@($"{(Inline ? "rz-datepicker-inline-container " : "rz-datepicker-popup-container ")}")">
<Popup id="@PopupID" Lazy=@(PopupRenderMode == PopupRenderMode.OnDemand) @ref=@popup style=@PopupStyle
role="dialog" aria-label="@PopupAriaLabel"
class="@($"{(Inline ? "rz-datepicker-inline-container " : "rz-datepicker-popup-container ")}")">
<div class="rz-calendar" @onkeydown="@OnPopupKeyDown">
@if (!TimeOnly)
{
<div class="rz-calendar-header">
<a id="@(GetId() + "pm")" tabindex="-1" aria-label="@PrevMonthAriaLabel" @onclick:preventDefault="true" class="rz-button rz-button-md rz-variant-text rz-button-icon-only rz-secondary rz-shade-default rz-calendar-prev" @onclick="@(() => { if (!Disabled) { try { if(CurrentDate.AddMonths(-1).Year >= YearFrom) {CurrentDate = CurrentDate.AddMonths(-1);}} catch (ArgumentOutOfRangeException) {}} })">
<button id="@(GetId() + "pm")" type="button" tabindex="-1" aria-label="@PrevMonthAriaLabel" class="rz-button rz-button-md rz-variant-text rz-button-icon-only rz-secondary rz-shade-default rz-calendar-prev" @onclick="@(() => { if (!Disabled) { try { if(CurrentDate.AddMonths(-1).Year >= YearFrom) {CurrentDate = CurrentDate.AddMonths(-1);}} catch (ArgumentOutOfRangeException) {}} })" disabled="@Disabled" aria-disabled="@(Disabled ? "true" : "false")">
<span class="notranslate rzi rz-calendar-prev-icon"></span>
</a>
<a id="@(GetId() + "nm")" tabindex="-1" aria-label="@NextMonthAriaLabel" @onclick:preventDefault="true" class="rz-button rz-button-md rz-variant-text rz-button-icon-only rz-secondary rz-shade-default rz-calendar-next" @onclick="@(() => { if (!Disabled) { try { if(CurrentDate.AddMonths(1).Year <= YearTo) {CurrentDate = CurrentDate.AddMonths(1);}} catch (ArgumentOutOfRangeException) {} } })">
</button>
<button id="@(GetId() + "nm")" type="button" tabindex="-1" aria-label="@NextMonthAriaLabel" class="rz-button rz-button-md rz-variant-text rz-button-icon-only rz-secondary rz-shade-default rz-calendar-next" @onclick="@(() => { if (!Disabled) { try { if(CurrentDate.AddMonths(1).Year <= YearTo) {CurrentDate = CurrentDate.AddMonths(1);}} catch (ArgumentOutOfRangeException) {} } })" disabled="@Disabled" aria-disabled="@(Disabled ? "true" : "false")">
<span class="notranslate rzi rz-calendar-next-icon"></span>
</a>
</button>
<div class="rz-calendar-title">
<RadzenDropDown @ref="monthDropDown" class="rz-calendar-month-dropdown" TabIndex="@TabIndex"
TValue="int" Value="@CurrentDate.Month" Disabled="@Disabled" Data="@months" TextProperty="Name" ValueProperty="Value"
@@ -88,8 +91,10 @@
DateTime date = StartDate.AddDays(dayNumber++);
var dateArgs = DateAttributes(date);
<td @attributes="@(dateArgs.Attributes)" class="@GetDayCssClass(date, dateArgs)"
@onclick="@(async () => { if (!Disabled && !dateArgs.Disabled) { await SetDay(date); } })">
<td @attributes="@(dateArgs.Attributes)" class="@GetDayCssClass(date, dateArgs)"
role="button" tabindex="@(Disabled || dateArgs.Disabled ? "-1" : "0")"
@onclick="@(async () => { if (!Disabled && !dateArgs.Disabled) { await SetDay(date); } })"
@onkeydown="@(args => OnDayKeyDown(args, date, dateArgs))">
<span class=@GetDayCssClass(date, dateArgs, false)>@date.Day</span>
</td>
}
@@ -112,7 +117,7 @@
<div class="rz-timepicker" @onmousedown:stopPropagation>
@if (ShowHour)
{
<RadzenNumeric InputAttributes="@(new Dictionary<string,object>(){ { "aria-label", "hour" }})" TValue="int" Disabled="@Disabled" Value="@(HourFormat == "12" ? ((CurrentDate.Hour + 11) % 12) + 1 : CurrentDate.Hour)"
<RadzenNumeric InputAttributes="@(new Dictionary<string,object>(){ { "aria-label", HourAriaLabel }})" TValue="int" Disabled="@Disabled" Value="@(HourFormat == "12" ? ((CurrentDate.Hour + 11) % 12) + 1 : CurrentDate.Hour)"
Min="@(HourFormat == "12" ? 1 : -1)" Max="@(HourFormat == "12" ? 12 : 24)" Step="@HoursStep"
Change="@UpdateHour" class="rz-hour-picker" @oninput=@OnUpdateHourInput Format="@(PadHours ? "00" : "")" Name="@($"{UniqueID}-h")" />
}
@@ -121,7 +126,7 @@
<div class="rz-separator">
<span>:</span>
</div>
<RadzenNumeric InputAttributes="@(new Dictionary<string,object>(){ { "aria-label", "minutes" }})" TValue="int" Disabled="@Disabled" Value="CurrentDate.Minute" Step="@MinutesStep" Min="0" Max="59"
<RadzenNumeric InputAttributes="@(new Dictionary<string,object>(){ { "aria-label", MinutesAriaLabel }})" TValue="int" Disabled="@Disabled" Value="CurrentDate.Minute" Step="@MinutesStep" Min="0" Max="59"
Change="@UpdateMinutes" class="rz-minute-picker" @oninput=@OnUpdateHourMinutes Format="@(PadMinutes ? "00" : "")" Name="@($"{UniqueID}-m")"/>
}
@if (ShowSeconds)
@@ -129,19 +134,19 @@
<div class="rz-separator">
<span>:</span>
</div>
<RadzenNumeric InputAttributes="@(new Dictionary<string,object>(){ { "aria-label", "seconds" }})" TValue="int" Disabled="@Disabled" Value="CurrentDate.Second" Step="@SecondsStep" Min="0" Max="59"
<RadzenNumeric InputAttributes="@(new Dictionary<string,object>(){ { "aria-label", SecondsAriaLabel }})" TValue="int" Disabled="@Disabled" Value="CurrentDate.Second" Step="@SecondsStep" Min="0" Max="59"
Change="@UpdateSeconds" class="rz-second-picker" @oninput=@OnUpdateHourSeconds Format="@(PadSeconds ? "00" : "")" Name="@($"{UniqueID}-s")"/>
}
@if (HourFormat == "12")
{
<div class="rz-ampm-picker">
<a id="@(GetId() + "ampmup")" tabindex="@(Disabled ? "-1" : $"{TabIndex}")" aria-label="@ToggleAmPmAriaLabel" @onclick:preventDefault="true" @onclick="@ToggleAmPm">
<button id="@(GetId() + "ampmup")" type="button" tabindex="@(Disabled ? "-1" : $"{TabIndex}")" aria-label="@ToggleAmPmAriaLabel" @onclick="@ToggleAmPm" disabled="@Disabled" aria-disabled="@(Disabled ? "true" : "false")">
<span class="notranslate rzi rzi-chevron-up"></span>
</a>
</button>
<span>@CurrentDate.ToString("tt")</span>
<a id="@(GetId() + "ampmdown")" tabindex="@(Disabled ? "-1" : $"{TabIndex}")" aria-label="@ToggleAmPmAriaLabel" @onclick:preventDefault="true" @onclick="@ToggleAmPm">
<button id="@(GetId() + "ampmdown")" type="button" tabindex="@(Disabled ? "-1" : $"{TabIndex}")" aria-label="@ToggleAmPmAriaLabel" @onclick="@ToggleAmPm" disabled="@Disabled" aria-disabled="@(Disabled ? "true" : "false")">
<span class="notranslate rzi rzi-chevron-down"></span>
</a>
</button>
</div>
}
@if (ShowTimeOkButton)

View File

@@ -70,6 +70,41 @@ namespace Radzen.Blazor
[Parameter]
public string ToggleAriaLabel { get; set; } = "Toggle";
/// <summary>
/// Gets or sets the popup aria label text.
/// </summary>
/// <value>The popup aria label text.</value>
[Parameter]
public string PopupAriaLabel { get; set; } = "Date picker";
/// <summary>
/// Gets or sets the clear button aria label text.
/// </summary>
/// <value>The clear button aria label text.</value>
[Parameter]
public string ClearAriaLabel { get; set; } = "Clear";
/// <summary>
/// Gets or sets the hour input aria label text.
/// </summary>
/// <value>The hour input aria label text.</value>
[Parameter]
public string HourAriaLabel { get; set; } = "Hour";
/// <summary>
/// Gets or sets the minutes input aria label text.
/// </summary>
/// <value>The minutes input aria label text.</value>
[Parameter]
public string MinutesAriaLabel { get; set; } = "Minutes";
/// <summary>
/// Gets or sets the seconds input aria label text.
/// </summary>
/// <value>The seconds input aria label text.</value>
[Parameter]
public string SecondsAriaLabel { get; set; } = "Seconds";
/// <summary>
/// Gets or sets the OK button aria label text.
/// </summary>
@@ -453,6 +488,15 @@ namespace Radzen.Blazor
return args;
}
async Task OnDayKeyDown(KeyboardEventArgs args, DateTime date, DateRenderEventArgs dateArgs)
{
var key = args.Code != null ? args.Code : args.Key;
if ((key == "Enter" || key == "Space") && !Disabled && !dateArgs.Disabled)
{
await SetDay(date);
}
}
/// <summary>
/// Gets or sets the kind of DateTime bind to control
/// </summary>

View File

@@ -3,6 +3,26 @@
@using Radzen.Blazor.Rendering
@inject IJSRuntime JSRuntime
@{
var sideOptions = sideDialogOptions;
var sideDialogId = $"rz-dialog-side-{sideOptions?.GetHashCode()}";
var sideDialogLabelId = $"{sideDialogId}-label";
var sideShowTitle = sideOptions?.ShowTitle == true;
var sideAriaAttributes = new Dictionary<string, object?>();
if (sideShowTitle)
{
sideAriaAttributes["aria-labelledby"] = sideDialogLabelId;
}
else
{
sideAriaAttributes["aria-label"] = sideOptions?.AriaLabel ?? sideDialog?.Title ?? sideOptions?.Title;
}
if (sideOptions?.ShowMask == true)
{
sideAriaAttributes["aria-modal"] = "true";
}
}
@foreach (var dialog in dialogs)
{
<DialogContainer @key=@dialog Dialog=@dialog ShowMask=@(dialog==dialogs.LastOrDefault()) />
@@ -12,57 +32,58 @@
{
<aside
class="@GetSideDialogCssClass()"
role="dialog"
@ref="sideDialogElement"
tabindex="0"
style="@GetSideDialogStyle()"
aria-labelledby="rz-dialog-side-label">
@if (sideDialogOptions?.Resizable == true)
@attributes="sideAriaAttributes">
@if (sideOptions?.Resizable == true)
{
<div @ref="resizeBarElement"
class="@GetResizeBarCssClass()"
title="@sideDialogOptions?.ResizeBarTitle" aria-label="@sideDialogOptions?.ResizeBarAriaLabel">
title="@sideOptions?.ResizeBarTitle" aria-label="@sideOptions?.ResizeBarAriaLabel">
<span class="rz-resize" aria-hidden="true"></span>
</div>
}
@if (sideDialogOptions?.ShowTitle == true)
@if (sideShowTitle)
{
<div class="rz-dialog-side-titlebar">
<div class="rz-dialog-side-title" style="display: inline" id="rz-dialog-side-label">
@if (sideDialogOptions.TitleContent != null)
<div class="rz-dialog-side-title" style="display: inline" id="@sideDialogLabelId">
@if (sideOptions?.TitleContent != null)
{
@sideDialogOptions.TitleContent(Service!)
@sideOptions.TitleContent(Service!)
}
else if (!string.IsNullOrEmpty(sideDialog?.Title))
{
@sideDialog.Title
}
else
else if (!string.IsNullOrEmpty(sideOptions?.Title))
{
@sideDialogOptions.Title
@sideOptions.Title
}
</div>
@if (sideDialogOptions.ShowClose)
@if (sideOptions?.ShowClose == true)
{
<a id="@(sideDialogOptions.GetHashCode() + "cl")" aria-label="@CloseSideDialogAriaLabel" @onclick:preventDefault="true" class="rz-dialog-side-titlebar-close" @onclick="@(_ => Service!.CloseSide())" role="button" tabindex="@sideDialogOptions.CloseTabIndex">
<button id="@($"rz-dialog-side-{sideOptions?.GetHashCode() ?? 0}-cl")" type="button" aria-label="@CloseSideDialogAriaLabel" class="rz-dialog-side-titlebar-close" @onclick="@(_ => Service!.CloseSide())" tabindex="@(sideOptions?.CloseTabIndex ?? 0)">
<span class="notranslate rzi rzi-times"></span>
</a>
</button>
}
</div>
}
<div class="@SideDialogContentCssClass" style="@sideDialogOptions?.Style">
<div class="@SideDialogContentCssClass" style="@sideOptions?.Style">
@sideDialogContent
</div>
</aside>
@if (dialogs.Count == 0 && sideDialogOptions?.ShowMask == true)
@if (dialogs.Count == 0 && sideOptions?.ShowMask == true)
{
@if (sideDialogOptions.CloseDialogOnOverlayClick)
@if (sideOptions.CloseDialogOnOverlayClick)
{
<div @onclick="@Service!.CloseSide" class="rz-dialog-mask"></div>
<div @onclick="@Service!.CloseSide" class="rz-dialog-mask" aria-hidden="true" tabindex="-1"></div>
}
else
{
<div class="rz-dialog-mask"></div>
<div class="rz-dialog-mask" aria-hidden="true" tabindex="-1"></div>
}
}
}
@@ -175,7 +196,9 @@
WrapperCssClass = options.WrapperCssClass,
ContentCssClass = options.ContentCssClass,
CloseTabIndex = options.CloseTabIndex,
TitleContent = options.TitleContent
TitleContent = options.TitleContent,
AriaLabel = options.AriaLabel,
CloseAriaLabel = options.CloseAriaLabel
};
// Create a Dialog object for the side dialog to support cascading parameter

View File

@@ -8,7 +8,9 @@
@if (Visible)
{
<div @ref="@Element" @attributes="Attributes" class="@GetCssClass()" onmousedown="Radzen.activeElement = null" @onclick="@(args => OpenPopup("ArrowDown", false, true))" @onclick:preventDefault @onclick:stopPropagation style="@Style" tabindex="@(Disabled ? "-1" : $"{TabIndex}")"
<div @ref="@Element" @attributes="Attributes" class="@GetCssClass()" onmousedown="Radzen.activeElement = null"
role="combobox" aria-haspopup="listbox" aria-expanded="@isPopupOpen.ToString().ToLowerInvariant()" aria-controls="@ListId" aria-disabled="@(Disabled.ToString().ToLowerInvariant())"
@onclick="@(args => OpenPopup("ArrowDown", false, true))" @onclick:preventDefault @onclick:stopPropagation style="@Style" tabindex="@(Disabled ? "-1" : $"{TabIndex}")"
@onkeydown="@((args) => OnKeyPress(args))" @onkeydown:preventDefault="@preventKeydown" id="@GetId()" @onfocus=@this.AsNonRenderingEventHandler(OnFocus)>
<div class="rz-helper-hidden-accessible">
<input disabled="@Disabled" aria-haspopup="listbox" readonly="" type="text" tabindex="-1"
@@ -54,7 +56,13 @@
@GetItemOrValueFromProperty(item, TextProperty ?? string.Empty)
}
</span>
<button tabindex="0" title="@(RemoveChipTitle + " " + GetItemOrValueFromProperty(item, TextProperty ?? ""))" type=button class="rz-button rz-button-sm rz-button-icon-only rz-base rz-shade-default @(Disabled ?"rz-state-disabled":"")" @onclick:preventDefault @onclick:stopPropagation @onclick="() => OnChipRemove(item)"><RadzenIcon Icon="close" /></button>
<button tabindex="0"
title="@(RemoveChipTitle + " " + GetItemOrValueFromProperty(item, TextProperty ?? ""))"
aria-label="@(RemoveChipTitle + " " + GetItemOrValueFromProperty(item, TextProperty ?? ""))"
type=button class="rz-button rz-button-sm rz-button-icon-only rz-base rz-shade-default @(Disabled ?"rz-state-disabled":"")"
@onclick:preventDefault @onclick:stopPropagation @onclick="() => OnChipRemove(item)">
<RadzenIcon Icon="close" />
</button>
</div>
}
</div>
@@ -133,9 +141,13 @@
<div class="rz-multiselect-header rz-helper-clearfix" @onclick:preventDefault>
@if(AllowSelectAll && Data != null && Data.Cast<object>().Any())
{
<div class="rz-chkbox" title="@(!AllowFiltering ? "" : SelectAllText)" @onclick="@SelectAll">
<div class="rz-chkbox" title="@(!AllowFiltering ? "" : SelectAllText)" @onclick="@SelectAll"
role="checkbox" tabindex="@(Disabled || ReadOnly ? "-1" : "0")"
aria-checked="@(IsAllSelected().ToString().ToLowerInvariant())"
aria-disabled="@(Disabled || ReadOnly ? "true" : "false")"
@onkeydown="OnSelectAllKeyDown">
<div class="rz-helper-hidden-accessible">
<input readonly="readonly" type="checkbox" id="@($"{(Name ?? UniqueID + "sa")}")" aria-label="@SearchAriaLabel" aria-checked="@(IsAllSelected().ToString().ToLowerInvariant())">
<input readonly="readonly" type="checkbox" id="@($"{(Name ?? UniqueID + "sa")}")" aria-label="@SelectAllText" aria-checked="@(IsAllSelected().ToString().ToLowerInvariant())">
</div>
<div class="@(IsAllSelected() ? "notranslate rz-chkbox-box rz-state-active" : "notranslate rz-chkbox-box")">
<span class="@(IsAllSelected() ? "notranslate rz-chkbox-icon rzi rzi-check" : "notranslate rz-chkbox-icon")"></span>
@@ -144,28 +156,30 @@
}
@if (AllowSelectAll && !AllowFiltering && !string.IsNullOrEmpty(SelectAllText) && Data != null && Data.Cast<object>().Any())
{
<span style="cursor:pointer" @onclick="@SelectAll">@SelectAllText</span>
<button type="button" class="rz-multiselect-selectall" disabled="@(Disabled || ReadOnly)" @onclick="@SelectAll">@SelectAllText</button>
}
@if (AllowFiltering)
{
<div class="rz-multiselect-filter-container">
<input id="@SearchID" tabindex="@(Disabled ? "-1" : $"{TabIndex}")" class="rz-inputtext" role="textbox" type="text"
onclick="Radzen.preventDefaultAndStopPropagation(event)" aria-label="@SearchAriaLabel"
@ref="@search" @oninput=@(args => { searchText = $"{args.Value}"; SearchTextChanged.InvokeAsync(searchText);})
@onchange="@((args) => OnFilter(args))" @onkeydown="@((args) => OnFilterKeyPress(args))" value="@searchText" autocomplete="@FilterAutoCompleteType" />
@ref="@search" @oninput="@OnFilterInput"
@onchange="@OnFilter" @onkeydown="@OnFilterKeyPress" value="@searchText" autocomplete="@FilterAutoCompleteType" />
<span class="notranslate rz-multiselect-filter-icon rzi rzi-search"></span>
</div>
}
<a id="@(GetId() + "clear")" class="rz-multiselect-close " @onclick="@ClearAll" @onclick:stopPropagation="true">
<button type="button" id="@(GetId() + "clear")" class="rz-multiselect-close" aria-label="@ClearAriaLabel"
@onclick="@ClearAll" @onclick:stopPropagation="true">
<span class="notranslate rzi rzi-times"></span>
</a>
</button>
</div>
}
}
<div class="@(Multiple ? "rz-multiselect-items-wrapper" : "rz-dropdown-items-wrapper")" style="@PopupStyle">
@if (Data != null && Data.Cast<object>().Any())
{
<ul @ref="list" class="@(Multiple ? "rz-multiselect-items rz-multiselect-list" : "rz-dropdown-items rz-dropdown-list")" role="listbox">
<ul id="@ListId" @ref="list" class="@(Multiple ? "rz-multiselect-items rz-multiselect-list" : "rz-dropdown-items rz-dropdown-list")"
role="listbox" aria-multiselectable="@(Multiple ? "true" : "false")">
@if (View != null)
{
@RenderItems()
@@ -184,7 +198,8 @@
</div>
@if (AllowClear && (!Multiple && HasValue || Multiple && selectedItems.Count > 0))
{
<i class="notranslate rz-dropdown-clear-icon rzi rzi-times" @onclick="@ClearAll" @onclick:stopPropagation="true"></i>
<button type="button" class="notranslate rz-dropdown-clear-icon rzi rzi-times" aria-label="@ClearAriaLabel"
@onclick="@ClearAll" @onclick:stopPropagation="true"></button>
}
</div>
}
@@ -197,7 +212,7 @@
<div class="rz-dropdown-filter-container">
<input aria-label="@SearchAriaLabel" id="@SearchID" @ref="@search" tabindex="@(Disabled ? "-1" : $"{TabIndex}")" placeholder="@FilterPlaceholder" class="rz-dropdown-filter rz-inputtext" autocomplete="@FilterAutoCompleteType" aria-autocomplete="none" type="text"
@onchange="@OnFilter" @onkeydown="@OnFilterKeyPress"
@bind:event="oninput" @bind:get="searchText" @bind:set="@(args => { searchText = $"{args}"; SearchTextChanged.InvokeAsync(searchText);})" />
@bind:event="oninput" @bind:get="searchText" @bind:set="@(args => OnFilterInput(new ChangeEventArgs(){ Value = args }))" />
<span class="notranslate rz-dropdown-filter-icon rzi rzi-search"></span>
</div>
</text>
@@ -208,7 +223,7 @@
<div class="rz-dropdown-filter-container">
<input aria-label="@SearchAriaLabel" id="@SearchID" @ref="@search" tabindex="@(Disabled ? "-1" : $"{TabIndex}")" placeholder="@FilterPlaceholder" class="rz-dropdown-filter rz-inputtext" autocomplete="@FilterAutoCompleteType" aria-autocomplete="none" type="text"
@onchange="@OnFilter" @onkeydown="@OnFilterKeyPress" value="@searchText"
@oninput="@((ChangeEventArgs args) => { searchText = $"{args.Value}"; SearchTextChanged.InvokeAsync(searchText);})" />
@oninput="@OnFilterInput" />
<span class="notranslate rz-dropdown-filter-icon rzi rzi-search"></span>
</div>
</text>

View File

@@ -1,4 +1,4 @@
@using Radzen
@using Radzen
@using System.Collections
@using System.Collections.Generic
@using Microsoft.AspNetCore.Components.Forms
@@ -9,6 +9,7 @@
@if (Visible)
{
<div @ref="@Element" @attributes="Attributes" class="@GetCssClass()" @onfocus=@this.AsNonRenderingEventHandler(OnFocus)
role="combobox" aria-haspopup="listbox" aria-expanded="@isPopupOpen.ToString().ToLowerInvariant()" aria-controls="@PopupID" aria-disabled="@(Disabled.ToString().ToLowerInvariant())"
style="@Style" tabindex="@(Disabled ? "-1" : $"{TabIndex}")" id="@GetId()" @onclick="@(args => OpenPopup("ArrowDown", false, true))" @onclick:preventDefault @onclick:stopPropagation
@onkeydown="@((args) => OnKeyPress(args))" @onkeydown:preventDefault="@preventKeydown">
<div class="rz-helper-hidden-accessible">
@@ -58,7 +59,13 @@
@GetItemOrValueFromProperty(item!, TextProperty ?? string.Empty)
}
</span>
<button tabindex="0" title="@(RemoveChipTitle + " " + GetItemOrValueFromProperty(item!, TextProperty ?? string.Empty))" type="button" class="rz-button rz-button-sm rz-button-icon-only rz-base rz-shade-default @(Disabled ?"rz-state-disabled":"")" @onclick:preventDefault @onclick:stopPropagation @onclick="() => OnChipRemove(item)"><RadzenIcon Icon="close" /></button>
<button tabindex="0"
title="@(RemoveChipTitle + " " + GetItemOrValueFromProperty(item!, TextProperty ?? string.Empty))"
aria-label="@(RemoveChipTitle + " " + GetItemOrValueFromProperty(item!, TextProperty ?? string.Empty))"
type="button" class="rz-button rz-button-sm rz-button-icon-only rz-base rz-shade-default @(Disabled ?"rz-state-disabled":"")"
@onclick:preventDefault @onclick:stopPropagation @onclick="() => OnChipRemove(item)">
<RadzenIcon Icon="close" />
</button>
</div>
}
</div>
@@ -126,13 +133,15 @@
@RenderFilter()
@if (ShowSearch)
{
<button tabindex="0" class="rz-button rz-button-md rz-button-icon-only rz-primary" type="button" title="@SearchTextPlaceholder" @onclick="@((args) => OnFilter(new ChangeEventArgs()))">
<button tabindex="0" class="rz-button rz-button-md rz-button-icon-only rz-primary" type="button"
title="@SearchTextPlaceholder" aria-label="@SearchTextPlaceholder" @onclick="@((args) => OnFilter(new ChangeEventArgs()))">
<i class="notranslate rz-button-icon-left rzi">search</i>
</button>
}
@if (ShowAdd)
{
<button tabindex="0" class="rz-button rz-button-md rz-button-icon-only rz-primary" style="margin-left: 5px" type="button" title="@AddAriaLabel" @onclick="@OnAddClick">
<button tabindex="0" class="rz-button rz-button-md rz-button-icon-only rz-primary" style="margin-left: 5px" type="button"
title="@AddAriaLabel" aria-label="@AddAriaLabel" @onclick="@OnAddClick">
<i class="notranslate rz-button-icon-left rzi">add</i>
</button>
}
@@ -192,7 +201,8 @@
@if (AllowClear && (!Multiple && HasValue || Multiple && (selectedItems.Count > 0 || SelectedValue is IEnumerable)))
{
<i class="notranslate rz-dropdown-clear-icon rzi rzi-times" @onclick="@Clear" @onclick:stopPropagation="true"></i>
<button type="button" class="notranslate rz-dropdown-clear-icon rzi rzi-times" aria-label="@ClearAriaLabel"
@onclick="@Clear" @onclick:stopPropagation="true"></button>
}
</div>
}
@@ -204,7 +214,7 @@
<text>
<input aria-label="@SearchAriaLabel" class="rz-lookup-search-input" id="@SearchID" @ref="@search" tabindex="0" placeholder="@SearchTextPlaceholder"
@onchange="@OnFilter" @onkeydown="@OnFilterKeyPress" style="@(ShowSearch ? "" : "margin-right:0px;")"
@bind:event="oninput" @bind:get="searchText" @bind:set=@(args => { searchText = $"{args}"; SearchTextChanged.InvokeAsync(searchText);}) />
@bind:event="oninput" @bind:get="searchText" @bind:set=@(args => OnFilterInput(new ChangeEventArgs(){ Value = args }))" />
</text>
};
#else
@@ -212,7 +222,7 @@
<text>
<input aria-label="@SearchAriaLabel" class="rz-lookup-search-input" id="@SearchID" @ref="@search" tabindex="0" placeholder="@SearchTextPlaceholder"
@onchange="@OnFilter" @onkeydown="@OnFilterKeyPress" value="@searchText" style="@(ShowSearch ? "" : "margin-right:0px;")"
@oninput=@((ChangeEventArgs args) => { searchText = $"{args.Value}"; SearchTextChanged.InvokeAsync(searchText);}) />
@oninput="@OnFilterInput" />
</text>
};
#endif

View File

@@ -622,18 +622,12 @@ namespace Radzen.Blazor
foreach (string word in words)
{
if (TextProperty != null)
{
query = query.Where(TextProperty, word, FilterOperator, FilterCaseSensitivity);
}
query = query.Where(TextProperty!, word, FilterOperator, FilterCaseSensitivity);
}
}
else
{
if (TextProperty != null)
{
query = query.Where(TextProperty, searchText, FilterOperator, FilterCaseSensitivity);
}
query = query.Where(TextProperty!, searchText, FilterOperator, FilterCaseSensitivity);
}
}
}

View File

@@ -8,8 +8,10 @@
Disabled = itemArgs.Disabled;
@if (DropDown?.Multiple == true)
{
<li class="@GetComponentCssClass("rz-multiselect")"
<li class="@GetComponentCssClass("rz-multiselect")" role="option" tabindex="-1"
aria-label="@(DropDown?.GetItemOrValueFromProperty(Item!, DropDown?.TextProperty ?? string.Empty) ?? "")"
aria-selected="@(DropDown?.IsSelected(Item) == true)"
aria-disabled="@(Disabled || DropDown?.Disabled == true ? "true" : "false")"
@onmousedown:preventDefault @onmousedown="args=>SelectItem(args,false)"
@onclick:preventDefault @onclick="args=>SelectItem(args,true)"
@attributes="@(itemArgs?.Attributes.Any() == true ? itemArgs.Attributes : Attributes)">
@@ -32,10 +34,13 @@ Disabled = itemArgs.Disabled;
}
else
{
<li role="option" class="@GetComponentCssClass("rz-dropdown")" aria-label="@(DropDown?.GetItemOrValueFromProperty(Item, DropDown?.TextProperty ?? "") ?? "")"
@onmousedown:preventDefault @onmousedown="args=>SelectItem(args,false)"
@onclick:preventDefault @onclick="args=>SelectItem(args,true)"
@attributes="@(itemArgs?.Attributes.Any() == true ? itemArgs.Attributes : Attributes)">
<li role="option" class="@GetComponentCssClass("rz-dropdown")" tabindex="-1"
aria-label="@(DropDown?.GetItemOrValueFromProperty(Item, DropDown?.TextProperty ?? "") ?? "")"
aria-selected="@(DropDown?.IsSelected(Item) == true)"
aria-disabled="@(Disabled || DropDown?.Disabled == true ? "true" : "false")"
@onmousedown:preventDefault @onmousedown="args=>SelectItem(args,false)"
@onclick:preventDefault @onclick="args=>SelectItem(args,true)"
@attributes="@(itemArgs?.Attributes.Any() == true ? itemArgs.Attributes : Attributes)">
<span>
@if (DropDown?.Template != null)
{

View File

@@ -14,12 +14,15 @@
ToggleShade="@ToggleShade"
Disabled="@Disabled"
AriaLabel="@AriaLabel"
AriaExpanded="@isOpen.ToString().ToLowerInvariant()"
AriaControls="@($"{GetId()}-items")"
AriaHasPopup="menu"
class="@ButtonClass"
style="@ButtonStyleCss">
</RadzenToggleButton>
@if (isOpen)
{
<RadzenStack onclick=@CloseAsync Orientation=@StackOrientation AlignItems=@StackAlignment Gap=@Gap class=@GetStackClasses() style=@ComputedItemsStyle ChildContent="@ChildContent" />
<RadzenStack onclick=@CloseAsync Orientation=@StackOrientation AlignItems=@StackAlignment Gap=@Gap class=@GetStackClasses() style=@ComputedItemsStyle ChildContent="@ChildContent" id=@($"{GetId()}-items") role="menu" />
}
</div>

View File

@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Radzen.Blazor;

View File

@@ -9,9 +9,9 @@
<legend class="rz-fieldset-legend" style="white-space:nowrap">
@if (AllowCollapse)
{
<a id="@(GetId() + "expc")" title="@TitleAttribute()" aria-label="@AriaLabelAttribute()" @onclick:preventDefault="true"
aria-controls="rz-fieldset-0-content" aria-expanded="@(collapsed ? "false" : "true")" @onclick=@Toggle
tabindex="0" @onkeypress="@(args => OnKeyPress(args, Toggle(new EventArgs())))" @onkeypress:preventDefault=preventKeyPress @onkeypress:stopPropagation>
<button type="button" id="@(GetId() + "expc")" title="@TitleAttribute()" aria-label="@AriaLabelAttribute()"
aria-controls="@($"{GetId()}-content")" aria-expanded="@(collapsed ? "false" : "true")" @onclick=@Toggle
@onkeypress="@(args => OnKeyPress(args, Toggle(new EventArgs())))" @onkeypress:preventDefault=preventKeyPress @onkeypress:stopPropagation>
@if (collapsed)
{
<span class="notranslate rz-fieldset-toggler rzi rzi-w rzi-plus"></span>
@@ -30,7 +30,7 @@
<span class="rz-fieldset-legend-text">@Text</span>
}
@HeaderTemplate
</a>
</button>
}
else
{
@@ -46,7 +46,7 @@
}
</legend>
}
<Expander Expanded=@(!collapsed) CssClass="rz-fieldset-content-wrapper" id="rz-fieldset-0-content">
<Expander Expanded=@(!collapsed) CssClass="rz-fieldset-content-wrapper" id="@($"{GetId()}-content")">
<div class="rz-fieldset-content">
@ChildContent
</div>

View File

@@ -9,7 +9,10 @@
{
<div @ref="@Element" style="@Style" @attributes="Attributes" class="@GetCssClass()" id="@GetId()">
<div class="rz-fileupload-buttonbar">
<span class=@ChooseClass tabindex="@(Disabled ? "-1" : $"{TabIndex}")" onkeydown="if(event.keyCode == 32 || event.keyCode == 13){event.preventDefault();this.firstElementChild.click();}">
<span class=@ChooseClass tabindex="@(Disabled ? "-1" : $"{TabIndex}")" role="button"
aria-disabled="@(Disabled ? "true" : "false")" aria-label="@ChooseText"
aria-controls="@(Name ?? GetId())"
onkeydown="if(event.keyCode == 32 || event.keyCode == 13){event.preventDefault();this.firstElementChild.click();}">
<input id="@(Name ?? GetId())" @attributes="InputAttributes" tabindex="-1" disabled="@Disabled" @ref="@fileUpload" name="@Name" type="file" accept="@Accept" onkeydown="event.stopPropagation()"
onchange="Radzen.uploadInputChange(event, null, false, false, true)" />
<span class="rz-button-text">@ChooseText</span>
@@ -23,7 +26,9 @@
<div>
@if (IsImage)
{
<img style="@ImageStyle" src="@ImageValue" @onclick="@OnImageClick" alt="@ImageAlternateText" />
<img style="@ImageStyle" src="@ImageValue" @onclick="@OnImageClick" alt="@ImageAlternateText"
role="@(ImageClick.HasDelegate ? "button" : null)" tabindex="@(ImageClick.HasDelegate ? "0" : null)"
@onkeydown="OnImageKeyDown" />
}
</div>
<div>

View File

@@ -247,6 +247,20 @@ namespace Radzen.Blazor
}
}
async Task OnImageKeyDown(KeyboardEventArgs args)
{
if (!ImageClick.HasDelegate)
{
return;
}
var key = args.Code != null ? args.Code : args.Key;
if (key == "Enter" || key == "Space")
{
await OnImageClick(new MouseEventArgs());
}
}
async System.Threading.Tasks.Task Remove(EventArgs args)
{
Value = default(TValue)!;

View File

@@ -1,6 +1,21 @@
@inherits RadzenComponentWithChildren
@using System.Collections.Generic
@inherits RadzenComponentWithChildren
@if (Visible)
{
<img @ref="@Element" src="@Path" style="@Style" @onclick="@((args) => OnClick(args))" @attributes="Attributes" class="@GetCssClass()" id="@GetId()" alt="@GetAlternateText()" />
var imgAttributes = new Dictionary<string, object>();
if (Attributes != null)
{
foreach (var attribute in Attributes)
{
imgAttributes[attribute.Key] = attribute.Value;
}
}
if (Click.HasDelegate)
{
imgAttributes["role"] = "button";
imgAttributes["tabindex"] = "0";
}
<img @ref="@Element" src="@Path" style="@Style" @onclick="@((args) => OnClick(args))" @onkeydown="OnKeyDown" @attributes="imgAttributes" class="@GetCssClass()" id="@GetId()" alt="@GetAlternateText()" />
}

View File

@@ -64,6 +64,26 @@ namespace Radzen.Blazor
await Click.InvokeAsync(args);
}
/// <summary>
/// Handles keyboard activation for clickable images.
/// </summary>
/// <param name="args">The <see cref="KeyboardEventArgs"/> instance containing the event data.</param>
protected async System.Threading.Tasks.Task OnKeyDown(KeyboardEventArgs args)
{
ArgumentNullException.ThrowIfNull(args);
if (!Click.HasDelegate)
{
return;
}
var key = args.Code != null ? args.Code : args.Key;
if (key == "Enter" || key == "Space")
{
await OnClick(new MouseEventArgs());
}
}
string GetAlternateText()
{
if (Attributes != null && Attributes.TryGetValue("alt", out var @alt) && !string.IsNullOrEmpty(Convert.ToString(@alt, CultureInfo.InvariantCulture)))

View File

@@ -1,4 +1,4 @@
@using Radzen
@using Radzen
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.JSInterop
@using Microsoft.AspNetCore.Components.Rendering
@@ -23,7 +23,10 @@
{
@if (AllowSelectAll)
{
<div class="rz-chkbox " @onclick="@SelectAll">
<div class="rz-chkbox" @onclick="@SelectAll" role="checkbox" tabindex="@(Disabled || ReadOnly ? "-1" : "0")"
aria-checked="@(IsAllSelected().ToString().ToLowerInvariant())"
aria-disabled="@(Disabled || ReadOnly ? "true" : "false")"
@onkeydown="OnSelectAllKeyDown">
<div class="rz-helper-hidden-accessible">
<input id="@($"{UniqueID}sa")" readonly="readonly" type="checkbox" aria-label="@SelectAllText" aria-checked="@(IsAllSelected().ToString().ToLowerInvariant())">
</div>
@@ -50,7 +53,7 @@
@if (View != null)
{
<div class="rz-listbox-list-wrapper">
<ul @ref="list" class="rz-listbox-list">
<ul @ref="list" class="rz-listbox-list" role="listbox" aria-multiselectable="@(Multiple ? "true" : "false")">
@RenderItems()
</ul>
</div>
@@ -65,7 +68,7 @@
<text>
<input id="@SearchID" @ref="@search" disabled="@Disabled" class="rz-inputtext" role="textbox" type="text" placeholder="@Placeholder"
@onchange="@OnFilter" @onkeydown="@OnFilterKeyPress" @onkeydown:stopPropagation="true"
@bind:event="oninput" @bind:get="searchText" @bind:set=@(args => { searchText = $"{args}"; SearchTextChanged.InvokeAsync(searchText);}) aria-label="@SearchAriaLabel" />
@bind:event="oninput" @bind:get="searchText" @bind:set=@(args => OnFilterInput(new ChangeEventArgs(){ Value = args }))" aria-label="@SearchAriaLabel" />
</text>
};
#else
@@ -73,7 +76,7 @@
<text>
<input id="@SearchID" @ref="@search" disabled="@Disabled" class="rz-inputtext" role="textbox" type="text" placeholder="@Placeholder"
@onchange="@OnFilter" @onkeydown="@OnFilterKeyPress" value="@searchText" @onkeydown:stopPropagation="true"
@oninput=@((ChangeEventArgs args) => { searchText = $"{args.Value}"; SearchTextChanged.InvokeAsync(searchText);}) aria-label="@SearchAriaLabel" />
@oninput="@OnFilterInput" aria-label="@SearchAriaLabel" />
</text>
};
#endif

View File

@@ -100,12 +100,16 @@ namespace Radzen.Blazor
if (AllowFiltering && key.Length == 1 && JSRuntime != null)
{
await JSRuntime.InvokeVoidAsync("Radzen.focusElement", SearchID);
await JSRuntime.InvokeAsync<string>("Radzen.setInputValue", search, key);
if (JSRuntime is not IJSInProcessRuntime)
{
await JSRuntime.InvokeAsync<string>("Radzen.setInputValue", search, key);
}
}
await OnKeyPress(args, false);
}
private bool visibleChanged;
private bool disabledChanged;
private bool firstRender = true;

View File

@@ -6,7 +6,9 @@
@if (itemArgs?.Visible == true)
{
Disabled = itemArgs.Disabled;
<li class="@GetComponentCssClass()" aria-label="@PropertyAccess.GetItemOrValueFromProperty(Item, ListBox?.TextProperty)" @onclick="@SelectItem"
<li class="@GetComponentCssClass()" role="option" aria-selected="@(ListBox?.IsSelected(Item) == true)" tabindex="-1"
aria-label="@PropertyAccess.GetItemOrValueFromProperty(Item, ListBox?.TextProperty)" @onclick="@SelectItem"
aria-disabled="@(Disabled || ListBox?.Disabled == true ? "true" : "false")"
@attributes="@(itemArgs?.Attributes.Any() == true ? itemArgs.Attributes : Attributes)">
@if (ListBox?.Multiple == true)
{

View File

@@ -37,7 +37,7 @@
}
@if (AllowResetPassword)
{
<a tabindex="0" class="rz-link" @onclick=@OnReset>@ResetPasswordText</a>
<button type="button" class="rz-link" @onclick=@OnReset>@ResetPasswordText</button>
}
</RadzenStack>
@if (AllowRegister)
@@ -84,7 +84,7 @@
}
@if (AllowResetPassword)
{
<a tabindex="0" class="rz-link" @onclick=@OnReset>@ResetPasswordText</a>
<button type="button" class="rz-link" @onclick=@OnReset>@ResetPasswordText</button>
}
</div>
</div>

View File

@@ -2,13 +2,14 @@
@if (Visible)
{
<ul @ref=@Element style=@Style @attributes=@Attributes class=@GetCssClass() id=@GetId()
<ul @ref=@Element style=@Style @attributes=@Attributes class=@GetCssClass() id=@GetId() role="menubar" aria-label="@AriaLabel"
tabindex="0" @onkeydown="@OnKeyPress" @onkeydown:preventDefault=preventKeyPress @onkeydown:stopPropagation
@onfocus=@this.AsNonRenderingEventHandler(OnFocus)>
@if (Responsive)
{
<li class="rz-menu-toggle-item">
<button aria-label=@ToggleAriaLabel class="rz-menu-toggle" @onclick=@OnToggle>
<button type="button" aria-label=@ToggleAriaLabel class="rz-menu-toggle" @onclick=@OnToggle
aria-haspopup="menu" aria-expanded="@IsOpen.ToString().ToLowerInvariant()" aria-controls="@GetId()">
@if (IsOpen)
{
<i class="notranslate rzi">close</i>

View File

@@ -78,6 +78,13 @@ namespace Radzen.Blazor
[Parameter]
public EventCallback<MenuItemEventArgs> Click { get; set; }
/// <summary>
/// Gets or sets the menu aria label text.
/// </summary>
/// <value>The menu aria label text.</value>
[Parameter]
public string AriaLabel { get; set; } = "Menu";
[Inject]
NavigationManager? NavigationManager { get; set; }

View File

@@ -57,7 +57,7 @@
</div>
@if (ChildContent != null)
{
<ul class="rz-navigation-menu" style="display: none">
<ul id="@($"{GetId()}-submenu")" class="rz-navigation-menu" role="menu" style="display: none">
<CascadingValue Value=this>
@ChildContent
</CascadingValue>

View File

@@ -111,6 +111,8 @@ namespace Radzen.Blazor
[Parameter]
public RenderFragment? ChildContent { get; set; }
internal string? SubmenuId => ChildContent != null ? $"{GetId()}-submenu" : null;
/// <summary>
/// Gets or sets the click callback.
/// </summary>

View File

@@ -1,10 +1,16 @@
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@inherits RadzenComponentWithChildren
@if (Item?.Parent?.ClickToOpen == true)
{
<li style=@Style @attributes="@Attributes" @onclick="@Item.OnClick" @onclick:stopPropagation>
<li style=@Style @attributes="@Attributes" @onclick="@Item.OnClick" @onclick:stopPropagation role="menuitem"
tabindex="@(Item?.Disabled == true ? -1 : 0)" aria-disabled="@(Item?.Disabled == true ? "true" : "false")"
aria-haspopup="@(Item?.ChildContent != null ? "menu" : null)"
aria-controls="@Item?.SubmenuId"
aria-expanded="@(Item?.ChildContent != null ? "false" : null)"
@onkeydown="OnKeyDown">
@ChildContent
</li>
}
@@ -12,13 +18,20 @@ else
{
if (Item?.ChildContent != null)
{
<li style=@Style @attributes="@Attributes" onmouseenter="Radzen.toggleMenuItem(this, event, true, false)" onmouseleave="Radzen.toggleMenuItem(this, event, false, false)">
<li style=@Style @attributes="@Attributes" onmouseenter="Radzen.toggleMenuItem(this, event, true, false)" onmouseleave="Radzen.toggleMenuItem(this, event, false, false)"
role="menuitem" tabindex="@(Item?.Disabled == true ? -1 : 0)" aria-disabled="@(Item?.Disabled == true ? "true" : "false")"
aria-haspopup="menu" aria-controls="@Item?.SubmenuId" aria-expanded="false" @onkeydown="OnKeyDown">
@ChildContent
</li>
}
else
{
<li style=@Style @attributes="@Attributes" @onclick="@(Item!.OnClick)" @onclick:stopPropagation>
<li style=@Style @attributes="@Attributes" @onclick="@(Item!.OnClick)" @onclick:stopPropagation role="menuitem"
tabindex="@(Item?.Disabled == true ? -1 : 0)" aria-disabled="@(Item?.Disabled == true ? "true" : "false")"
aria-haspopup="@(Item?.ChildContent != null ? "menu" : null)"
aria-controls="@Item?.SubmenuId"
aria-expanded="@(Item?.ChildContent != null ? "false" : null)"
@onkeydown="OnKeyDown">
@ChildContent
</li>
}
@@ -32,4 +45,25 @@ else
/// <value>The menu item.</value>
[Parameter]
public RadzenMenuItem? Item { get; set; }
async Task OnKeyDown(KeyboardEventArgs args)
{
if (Item == null || Item.Disabled)
{
return;
}
var key = args.Code != null ? args.Code : args.Key;
if (key == "Enter" || key == "Space")
{
if (Item.ChildContent != null)
{
await Item.Toggle();
}
else
{
await Item.OnClick(new MouseEventArgs());
}
}
}
}

View File

@@ -1,20 +1,27 @@
@using System.Collections.Generic
@using System.Timers
@implements IDisposable
@if (Visible)
{
var classes = GetMessageCssClasses();
<div aria-live="polite" class="rz-notification-item-wrapper rz-open @classes.Item1" style="@(Message?.Click != null || Message?.CloseOnClick == true ? "cursor: pointer;" : "") @Style">
var clickable = Message?.Click != null || Message?.CloseOnClick == true;
var wrapperAttributes = clickable ? new Dictionary<string, object>
{
{ "role", "button" },
{ "tabindex", "0" }
} : null;
<div aria-live="polite" class="rz-notification-item-wrapper rz-open @classes.Item1" style="@(clickable ? "cursor: pointer;" : "") @Style"
@attributes="wrapperAttributes" @onclick="NotificationClicked" @onkeydown="OnKeyDown">
<div class="rz-notification-item">
<div class="rz-notification-message-wrapper">
<span class="notranslate rzi rz-notification-icon @classes.Item2" @onclick="NotificationClicked"></span>
<div class="rz-notification-message" @onclick="NotificationClicked">
<span class="notranslate rzi rz-notification-icon @classes.Item2"></span>
<div class="rz-notification-message">
<div class="rz-notification-title">@if (Message?.SummaryContent != null && Service != null) { @Message.SummaryContent(Service) } else { @Message?.Summary }</div>
<div class="rz-notification-content">@if (Message?.DetailContent != null && Service != null) { @Message.DetailContent(Service) } else { @Message?.Detail }</div>
</div>
</div>
<div class="notranslate rzi rz-notification-close" @onclick="@Close"></div>
<button type="button" class="notranslate rzi rz-notification-close" aria-label="@(Message?.CloseAriaLabel ?? "Close")" @onclick="@Close" @onclick:stopPropagation="true"></button>
</div>
<RadzenProgressBar ShowValue="false" Visible=@(Message?.Duration != null && Message.ShowProgress)
Value=@progress Max="@(Message?.Duration ?? 0)" ProgressBarStyle=@GetProgressBarStyle() />
@@ -103,12 +110,29 @@
private void NotificationClicked()
{
if (Message?.Click == null && Message?.CloseOnClick != true)
return;
if (Message?.CloseOnClick == true)
Close();
Message?.Click?.Invoke(Message);
}
private void OnKeyDown(KeyboardEventArgs args)
{
if (Message?.Click == null && Message?.CloseOnClick != true)
{
return;
}
var key = args.Code != null ? args.Code : args.Key;
if (key == "Enter" || key == "Space")
{
NotificationClicked();
}
}
public void Dispose()
{
timer?.Stop();

View File

@@ -1,9 +1,10 @@
@inherits RadzenComponent
@if (GetVisible())
{
<div @ref="@Element" @attributes="Attributes" class="@GetCssClass()" style="@Style" id="@GetId()"
@onkeydown="@OnKeyDown" @onkeydown:preventDefault="preventKeyDown" @onkeydown:stopPropagation="true" tabindex=0
@onfocus=@this.AsNonRenderingEventHandler(OnFocus)>
<nav @ref="@Element" @attributes="Attributes" class="@GetCssClass()" style="@Style" id="@GetId()"
aria-label="@NavigationAriaLabel"
@onkeydown="@OnKeyDown" @onkeydown:preventDefault="preventKeyDown" @onkeydown:stopPropagation="true" tabindex=0
@onfocus=@this.AsNonRenderingEventHandler(OnFocus)>
@if(ShowPagingSummary)
{
<span class="rz-pager-summary">
@@ -17,41 +18,53 @@
}
</span>
}
<a id="@(GetId() + "fp")" class="rz-pager-first rz-pager-element @(skip > 0 ? "": "rz-state-disabled") @(focusedIndex == -2 ? "rz-state-focused": "")" @onclick:preventDefault="true" @onclick="@(() => OnFirstPageClick())" aria-label="@FirstPageAriaLabel" role="button" title="@FirstPageTitle" disabled="@(CurrentPage <= 0)">
<span class="notranslate rz-pager-icon rzi rzi-step-backward"></span>
</a>
<a id="@(GetId() + "pp")" class="rz-pager-prev rz-pager-element @(skip > 0 ? "": "rz-state-disabled") @(focusedIndex == -1 ? "rz-state-focused": "")" @onclick:preventDefault="true" @onclick="@(() => OnPrevPageClick())" aria-label="@PrevPageAriaLabel" role="button" title="@PrevPageTitle" disabled="@(CurrentPage <= 0)">
<span class="notranslate rz-pager-icon rzi rzi-caret-left"></span>
@if (PrevPageLabel != null)
{
<span class="rz-pager-label">@PrevPageLabel</span>
}
</a>
<span class="rz-pager-pages">
<ul class="rz-pager-pages" role="list">
<li class="rz-pager-item">
<button id="@(GetId() + "fp")" type="button" class="rz-pager-first rz-pager-element @(skip > 0 ? "": "rz-state-disabled") @(focusedIndex == -2 ? "rz-state-focused": "")"
@onclick="@(() => OnFirstPageClick())" aria-label="@FirstPageAriaLabel" title="@FirstPageTitle" disabled="@(CurrentPage <= 0)" aria-disabled="@(CurrentPage <= 0 ? "true" : "false")">
<span class="notranslate rz-pager-icon rzi rzi-step-backward"></span>
</button>
</li>
<li class="rz-pager-item">
<button id="@(GetId() + "pp")" type="button" class="rz-pager-prev rz-pager-element @(skip > 0 ? "": "rz-state-disabled") @(focusedIndex == -1 ? "rz-state-focused": "")"
@onclick="@(() => OnPrevPageClick())" aria-label="@PrevPageAriaLabel" title="@PrevPageTitle" disabled="@(CurrentPage <= 0)" aria-disabled="@(CurrentPage <= 0 ? "true" : "false")">
<span class="notranslate rz-pager-icon rzi rzi-caret-left"></span>
@if (PrevPageLabel != null)
{
<span class="rz-pager-label">@PrevPageLabel</span>
}
</button>
</li>
@foreach (var i in Enumerable.Range(startPage, Math.Min(endPage + 1, PageNumbersCount)))
{
<a id="@(GetId() + i.ToString() + "p")" class="rz-pager-page rz-pager-element @(i == CurrentPage ? "rz-state-active" : "") @(startPage + focusedIndex == i ? "rz-state-focused": "")" @onclick:preventDefault="true" @onclick="@(() => OnPageClick(i, startPage))" aria-current="@(i == CurrentPage ? "page" : null)" aria-label="@string.Format(PageAriaLabelFormat, (i + 1).ToString())" role="button" title="@string.Format(PageTitleFormat, (i + 1).ToString())">@(i + 1)</a>
<li class="rz-pager-item">
<button id="@(GetId() + i.ToString() + "p")" type="button" class="rz-pager-page rz-pager-element @(i == CurrentPage ? "rz-state-active" : "") @(startPage + focusedIndex == i ? "rz-state-focused": "")"
@onclick="@(() => OnPageClick(i, startPage))" aria-current="@(i == CurrentPage ? "page" : null)" aria-label="@string.Format(PageAriaLabelFormat, (i + 1).ToString())"
title="@string.Format(PageTitleFormat, (i + 1).ToString())">@(i + 1)</button>
</li>
}
</span>
<a id="@(GetId() + "np")" class="rz-pager-next rz-pager-element @((CurrentPage != numberOfPages - 1) ? "": "rz-state-disabled") @(focusedIndex == Math.Min(endPage + 1, PageNumbersCount)? "rz-state-focused": "")" @onclick:preventDefault="true" @onclick="@(() => OnNextPageClick(endPage))" aria-label="@NextPageAriaLabel" role="button" title="@NextPageTitle" disabled="@(CurrentPage >= (numberOfPages - 1))">
@if (NextPageLabel != null)
{
<span class="rz-pager-label">@NextPageLabel</span>
}
<span class="notranslate rz-pager-icon rzi rzi-caret-right"></span>
</a>
<a id="@(GetId() + "lp")" class="rz-pager-last rz-pager-element @((CurrentPage != numberOfPages - 1) ? "": "rz-state-disabled") @(focusedIndex == Math.Min(endPage + 1, PageNumbersCount) + 1 ? "rz-state-focused": "")" @onclick:preventDefault="true" @onclick="@(() => OnLastPageClick(endPage))" aria-label="@LastPageAriaLabel" role="button" title="@LastPageTitle" disabled="@(CurrentPage >= (numberOfPages - 1))">
<span class="notranslate rz-pager-icon rzi rzi-step-forward"></span>
</a>
<li class="rz-pager-item">
<button id="@(GetId() + "np")" type="button" class="rz-pager-next rz-pager-element @((CurrentPage != numberOfPages - 1) ? "": "rz-state-disabled") @(focusedIndex == Math.Min(endPage + 1, PageNumbersCount)? "rz-state-focused": "")"
@onclick="@(() => OnNextPageClick(endPage))" aria-label="@NextPageAriaLabel" title="@NextPageTitle" disabled="@(CurrentPage >= (numberOfPages - 1))" aria-disabled="@(CurrentPage >= (numberOfPages - 1) ? "true" : "false")">
@if (NextPageLabel != null)
{
<span class="rz-pager-label">@NextPageLabel</span>
}
<span class="notranslate rz-pager-icon rzi rzi-caret-right"></span>
</button>
</li>
<li class="rz-pager-item">
<button id="@(GetId() + "lp")" type="button" class="rz-pager-last rz-pager-element @((CurrentPage != numberOfPages - 1) ? "": "rz-state-disabled") @(focusedIndex == Math.Min(endPage + 1, PageNumbersCount) + 1 ? "rz-state-focused": "")"
@onclick="@(() => OnLastPageClick(endPage))" aria-label="@LastPageAriaLabel" title="@LastPageTitle" disabled="@(CurrentPage >= (numberOfPages - 1))" aria-disabled="@(CurrentPage >= (numberOfPages - 1) ? "true" : "false")">
<span class="notranslate rz-pager-icon rzi rzi-step-forward"></span>
</button>
</li>
</ul>
@if(PageSizeOptions != null && PageSizeOptions.Any())
{
<RadzenDropDown TValue="int" Data="@PageSizeOptions" Value="@PageSize" Change="@OnPageSizeChanged" />
<span class="rz-pagesize-text">@PageSizeText</span>
}
</div>
</nav>
}

View File

@@ -165,6 +165,12 @@ namespace Radzen.Blazor
[Parameter]
public bool ShowPagingSummary { get; set; }
/// <summary>
/// Gets or sets the navigation aria-label.
/// </summary>
[Parameter]
public string NavigationAriaLabel { get; set; } = "Pagination";
/// <summary>
/// Gets or sets the pager summary format. <see cref="PagingSummaryTemplate" /> has preference over this property.
/// </summary>
@@ -349,6 +355,7 @@ namespace Radzen.Blazor
skip = page * PageSize;
await InvokeAsync(Reload);
await PageChanged.InvokeAsync(new PagerEventArgs() { Skip = skip, Top = PageSize, PageIndex = CurrentPage });
StateHasChanged();
}
}

View File

@@ -20,15 +20,15 @@
@HeaderTemplate
@if (AllowCollapse)
{
<a id="@(GetId() + "expc")" @onclick=@Toggle class="rz-panel-titlebar-icon rz-panel-titlebar-toggler"
@onclick:preventDefault="true" role="button" aria-controls="rz-panel-0-content"
aria-expanded="@(collapsed ? "false" : "true")" aria-label="@(collapsed ? ExpandAriaLabel : CollapseAriaLabel)" title="@(collapsed ? ExpandTitle : CollapseTitle)"
tabindex="0" @onkeypress="@(args => OnKeyPress(args, Toggle(new MouseEventArgs())))" @onkeypress:preventDefault=preventKeyPress @onkeypress:stopPropagation>
<button id="@(GetId() + "expc")" type="button" @onclick=@Toggle class="rz-panel-titlebar-icon rz-panel-titlebar-toggler"
aria-controls="@($"{GetId()}-content")"
aria-expanded="@(collapsed ? "false" : "true")" aria-label="@(collapsed ? ExpandAriaLabel : CollapseAriaLabel)" title="@(collapsed ? ExpandTitle : CollapseTitle)"
@onkeypress="@(args => OnKeyPress(args, Toggle(new MouseEventArgs())))" @onkeypress:preventDefault=preventKeyPress @onkeypress:stopPropagation>
<span class="notranslate rzi @(collapsed ? "rzi-plus" : "rzi-minus")"></span>
</a>
</button>
}
</div>
<Expander Expanded=@(!collapsed) CssClass="rz-panel-content-wrapper">
<Expander Expanded=@(!collapsed) CssClass="rz-panel-content-wrapper" id="@($"{GetId()}-content")">
<div class="rz-panel-content">
@ChildContent
</div>

View File

@@ -2,7 +2,8 @@
@if (Visible)
{
<ul @ref=@Element style=@Style @attributes=@Attributes class=@GetCssClass() id=@GetId() tabindex="0"
<ul @ref=@Element style=@Style @attributes=@Attributes class=@GetCssClass() id=@GetId()
role="menu" aria-label="@AriaLabel" tabindex="0"
@onkeydown=@OnKeyPress
@onkeydown:preventDefault=preventKeyPress @onkeydown:stopPropagation
@onfocus=@this.AsNonRenderingEventHandler(OnFocus)>

View File

@@ -38,6 +38,13 @@ namespace Radzen.Blazor
[Parameter]
public EventCallback<MenuItemEventArgs> Click { get; set; }
/// <summary>
/// Gets or sets the menu aria label text.
/// </summary>
/// <value>The menu aria label text.</value>
[Parameter]
public string AriaLabel { get; set; } = "Menu";
/// <summary>
/// Gets or sets a value representing the URL matching behavior.
/// </summary>

View File

@@ -3,7 +3,12 @@
@inherits RadzenComponent
@if (Visible)
{
<li @ref=@Element style=@Style @attributes=@Attributes class=@GetCssClass() id=@GetId() @onclick=@(this.AsNonRenderingEventHandler<MouseEventArgs>(OnClick)) @onclick:stopPropagation>
<li @ref=@Element style=@Style @attributes=@Attributes class=@GetCssClass() id=@GetId()
role="menuitem" tabindex="@(Disabled ? -1 : 0)" aria-disabled="@(Disabled ? "true" : "false")"
aria-haspopup="@(ChildContent != null ? "menu" : null)"
aria-controls="@(ChildContent != null ? $"{GetId()}-submenu" : null)"
aria-expanded="@(ChildContent != null ? expanded.ToString().ToLowerInvariant() : null)"
@onclick=@(this.AsNonRenderingEventHandler<MouseEventArgs>(OnClick)) @onclick:stopPropagation @onkeydown="OnKeyDown">
<div class=@WrapperClass>
@if (Path != null)
{
@@ -22,17 +27,21 @@
}
else if (Parent?.DisplayStyle == MenuItemDisplayStyle.Text || Parent?.DisplayStyle == MenuItemDisplayStyle.IconAndText)
{
<span class="rz-navigation-item-text" @onclick="@Toggle">@Text</span>
<span class="rz-navigation-item-text">@Text</span>
}
@if (ChildContent != null && Parent?.ShowArrow == true)
{
<i class=@ToggleClass @onclick="@Toggle" @onclick:preventDefault>keyboard_arrow_down</i>
<i class=@ToggleClass>keyboard_arrow_down</i>
}
</NavLink>
}
else
{
<div class="rz-navigation-item-link" style="@(Parent?.DisplayStyle == MenuItemDisplayStyle.Icon?"margin-inline-end:0px;":"")" @onclick="@Toggle">
<div class="rz-navigation-item-link" style="@(Parent?.DisplayStyle == MenuItemDisplayStyle.Icon?"margin-inline-end:0px;":"")"
role="button" tabindex="0" aria-expanded="@expanded.ToString().ToLowerInvariant()"
aria-controls="@(ChildContent != null ? $"{GetId()}-submenu" : null)"
aria-haspopup="@(ChildContent != null ? "menu" : null)"
@onclick="@Toggle" @onkeydown="OnToggleKeyDown">
@if (!string.IsNullOrEmpty(Icon) && (Parent?.DisplayStyle == MenuItemDisplayStyle.Icon || Parent?.DisplayStyle == MenuItemDisplayStyle.IconAndText))
{
<i class="notranslate rzi rz-navigation-item-icon" style="@getIconStyle()">@Icon</i>
@@ -59,7 +68,7 @@
@if (ChildContent != null)
{
<Expander Expanded=@expanded>
<ul class="rz-navigation-menu">
<ul id="@($"{GetId()}-submenu")" class="rz-navigation-menu" role="menu">
<CascadingValue Value=this>
@ChildContent
</CascadingValue>

View File

@@ -424,6 +424,34 @@ namespace Radzen.Blazor
}
}
async Task OnKeyDown(KeyboardEventArgs args)
{
if (Disabled)
{
return;
}
var key = args.Code != null ? args.Code : args.Key;
if (key == "Enter" || key == "Space")
{
await OnClick(new MouseEventArgs());
}
}
async Task OnToggleKeyDown(KeyboardEventArgs args)
{
if (Disabled)
{
return;
}
var key = args.Code != null ? args.Code : args.Key;
if (key == "Enter" || key == "Space")
{
await Toggle();
}
}
/// <inheritdoc />
public override void Dispose()
{

View File

@@ -98,7 +98,7 @@
class="rz-pivot-header-content @(isSortable ? "rz-sortable" : "")"
tabindex="@(isSortable ? "0" : null)"
role="@(isSortable ? "button" : null)"
aria-label="@(isSortable ? $"Sort by {row?.GetTitle()}" : null)">
aria-label="@(isSortable ? string.Format(SortAriaLabelFormat, row?.GetTitle()) : null)">
<span class="rz-pivot-header-text">@row?.GetTitle()</span>
@if (isSortable)
{
@@ -118,12 +118,14 @@
</div>
@if (isFilterable)
{
<i @ref=FilterIconRef[filterIconRefKey] @onclick:stopPropagation="true"
@onmousedown="@(() => { if (row != null) _ = ToggleFilter(row, filterIconRefKey); })"
class="@(row != null ? GetFilterIconCss(row) : "")"
title="Filter">
<button type="button" @ref=FilterIconRef[filterIconRefKey] @onclick:stopPropagation="true"
@onmousedown="@(() => { if (row != null) _ = ToggleFilter(row, filterIconRefKey); })"
@onclick="@(() => { if (row != null) _ = ToggleFilter(row, filterIconRefKey); })"
class="@(row != null ? GetFilterIconCss(row) : "")"
aria-label="@FilterText" aria-haspopup="dialog"
aria-controls="@($"pivot-filter-{row?.Property}")" aria-expanded="false">
@FilterIcon
</i>
</button>
}
</div>
</th>
@@ -151,7 +153,7 @@
class="rz-pivot-header-content @(isSortable ? "rz-sortable" : "")"
tabindex="@(isSortable ? "0" : null)"
role="@(isSortable ? "button" : null)"
aria-label="@(isSortable ? $"Sort by {column?.GetTitle()}" : null)">
aria-label="@(isSortable ? string.Format(SortAriaLabelFormat, column?.GetTitle()) : null)">
<span class="rz-pivot-header-text">@column?.GetTitle()</span>
@if (isSortable)
{
@@ -171,12 +173,14 @@
</div>
@if (isFilterable)
{
<i @ref=FilterIconRef[filterIconRefKey] @onclick:stopPropagation="true"
@onmousedown="@(() => { if (column != null) _ = ToggleFilter(column, filterIconRefKey); })"
class="@(column != null ? GetFilterIconCss(column) : "")"
title="Filter">
<button type="button" @ref=FilterIconRef[filterIconRefKey] @onclick:stopPropagation="true"
@onmousedown="@(() => { if (column != null) _ = ToggleFilter(column, filterIconRefKey); })"
@onclick="@(() => { if (column != null) _ = ToggleFilter(column, filterIconRefKey); })"
class="@(column != null ? GetFilterIconCss(column) : "")"
aria-label="@FilterText" aria-haspopup="dialog"
aria-controls="@($"pivot-filter-{column?.Property}")" aria-expanded="false">
@FilterIcon
</i>
</button>
}
</div>
</th>
@@ -204,7 +208,7 @@
class="rz-pivot-header-content @(isSortable ? "rz-sortable" : "")"
tabindex="@(isSortable ? "0" : null)"
role="@(isSortable ? "button" : null)"
aria-label="@(isSortable ? $"Sort by {column?.GetTitle()}" : null)">
aria-label="@(isSortable ? string.Format(SortAriaLabelFormat, column?.GetTitle()) : null)">
<span class="rz-pivot-header-text">@column?.GetTitle()</span>
@if (isSortable)
{
@@ -224,12 +228,14 @@
</div>
@if (isFilterable)
{
<i @ref=FilterIconRef[filterIconRefKey] @onclick:stopPropagation="true"
@onmousedown="@(() => { if (column != null) _ = ToggleFilter(column, filterIconRefKey); })"
class="@(column != null ? GetFilterIconCss(column) : "")"
title="Filter">
<button type="button" @ref=FilterIconRef[filterIconRefKey] @onclick:stopPropagation="true"
@onmousedown="@(() => { if (column != null) _ = ToggleFilter(column, filterIconRefKey); })"
@onclick="@(() => { if (column != null) _ = ToggleFilter(column, filterIconRefKey); })"
class="@(column != null ? GetFilterIconCss(column) : "")"
aria-label="@FilterText" aria-haspopup="dialog"
aria-controls="@($"pivot-filter-{column?.Property}")" aria-expanded="false">
@FilterIcon
</i>
</button>
}
</div>
</th>
@@ -295,6 +301,8 @@
{
<div class="rz-pivot-drill-down-header">
<span @onclick="@(() => ToggleColumnDrillDown(cell.PathKey))"
@onkeydown="@(args => OnDrillDownKeyDown(args, cell.PathKey))"
role="button" tabindex="0" aria-expanded="@(!cell.IsCollapsed).ToString().ToLowerInvariant()"
class="notranslate rz-tree-toggler rzi rzi-caret-@(cell.IsCollapsed ? "right" : "down")" style="margin-inline-start:0"></span>
<span class="rz-pivot-header-text">
@if (cell.HeaderTemplate != null && cell.Group != null)
@@ -431,6 +439,7 @@
var prefix = currentFilterField is RadzenPivotColumn<TItem> ? "col" : currentFilterField is RadzenPivotRow<TItem> ? "row" : "agg";
var filterIconRefKey = $"{prefix}{currentFilterField.GetFilterProperty()}";
<Popup @ref="filterPopup" id="@($"pivot-filter-{currentFilterField.Property}")" class="rz-overlaypanel"
role="dialog" aria-label="@FilterText"
style="display:none;min-width:250px;" @onkeydown=@(args => OnFilterPopupKeyPressed(args, filterIconRefKey))>
<div class="rz-overlaypanel-content">
@if (currentFilterField.FilterTemplate != null)

View File

@@ -190,6 +190,13 @@ namespace Radzen.Blazor
[Parameter]
public string AggregatesText { get; set; } = "Aggregates";
/// <summary>
/// Gets or sets the sort aria label format.
/// </summary>
/// <value>The sort aria label format.</value>
[Parameter]
public string SortAriaLabelFormat { get; set; } = "Sort by {0}";
/// <summary>
/// Gets or set the filter icon to use.
/// </summary>
@@ -1602,6 +1609,15 @@ namespace Radzen.Blazor
await HandleFieldSort(pivotRows, row);
}
async Task OnDrillDownKeyDown(KeyboardEventArgs args, string pathKey)
{
var key = args.Code != null ? args.Code : args.Key;
if (key == "Enter" || key == "Space")
{
await ToggleColumnDrillDown(pathKey);
}
}
private async Task HandleFieldSort<T>(List<T> allFields, T sortedField)
where T : RadzenPivotField<TItem>
{
@@ -1618,15 +1634,6 @@ namespace Radzen.Blazor
sortedField.SetSortOrderInternal(nextSortOrder);
// Clear other column sorts if single column sorting
if (sortedField.GetSortOrder() != null)
{
foreach (var col in allFields.Where(c => c != sortedField))
{
col.SetSortOrderInternal(null);
}
}
await Reload();
}

View File

@@ -3,9 +3,13 @@
@if (Visible)
{
<ul @ref="@Element" style="@Style" @attributes="Attributes" class="@GetCssClass()" id="@GetId()"
tabindex="0" @onkeydown="@OnKeyPress" @onkeydown:preventDefault=preventKeyPress @onkeydown:stopPropagation>
role="menu" aria-label="@ToggleAriaLabel" tabindex="0"
@onkeydown="@OnKeyPress" @onkeydown:preventDefault=preventKeyPress @onkeydown:stopPropagation>
<li class="rz-navigation-item">
<div class="rz-navigation-item-wrapper" onclick="Radzen.toggleMenuItem(this)">
<div class="rz-navigation-item-wrapper" role="button" tabindex="0" aria-haspopup="menu"
aria-label="@ToggleAriaLabel"
aria-expanded="@(!Collapsed).ToString().ToLowerInvariant()" aria-controls="@($"{GetId()}-menu")"
onclick="Radzen.toggleMenuItem(this)" @onkeydown="OnToggleKeyDown">
<div class="rz-navigation-item-link">
<div class="item-text" @onkeydown:stopPropagation>
@if (Template != null)
@@ -19,7 +23,8 @@
}
</div>
</div>
<ul class="rz-navigation-menu" style="@contentStyle" @onkeydown:stopPropagation>
<ul id="@($"{GetId()}-menu")" class="rz-navigation-menu" role="menu" style="@contentStyle"
aria-hidden="@(Collapsed ? "true" : "false")" @onkeydown:stopPropagation>
<CascadingValue Value=this>
@ChildContent
</CascadingValue>

View File

@@ -50,6 +50,13 @@ namespace Radzen.Blazor
[Parameter]
public bool ShowIcon { get; set; } = true;
/// <summary>
/// Gets or sets the toggle aria label text.
/// </summary>
/// <value>The toggle aria label text.</value>
[Parameter]
public string ToggleAriaLabel { get; set; } = "Profile menu";
string contentStyle = "display:none;position:absolute;z-index:1;";
/// <summary>
@@ -133,6 +140,15 @@ namespace Radzen.Blazor
}
}
async Task OnToggleKeyDown(KeyboardEventArgs args)
{
var key = args.Code != null ? args.Code : args.Key;
if (key == "Space" || key == "Enter")
{
await Toggle(new MouseEventArgs());
}
}
internal bool IsFocused(RadzenProfileMenuItem item)
{
return items.IndexOf(item) == focusedIndex && focusedIndex != -1;

View File

@@ -2,7 +2,7 @@
@inherits RadzenComponent
@if (Visible)
{
<li @attributes="Attributes" class="@GetItemCssClass()" style="@Style" @onclick="@OnClick">
<li @attributes="Attributes" class="@GetItemCssClass()" style="@Style" @onclick="@OnClick" role="menuitem" tabindex="0" @onkeydown="OnKeyDown">
<div class="rz-navigation-item-wrapper">
@if (Path != null)
{

View File

@@ -125,6 +125,15 @@ namespace Radzen.Blazor
}
}
async Task OnKeyDown(KeyboardEventArgs args)
{
var key = args.Code != null ? args.Code : args.Key;
if (key == "Enter" || key == "Space")
{
await OnClick(new MouseEventArgs());
}
}
RadzenProfileMenu? _parent;
/// <summary>
/// Gets or sets the parent.

View File

@@ -6,6 +6,8 @@
int n = modules.GetLength(0);
int vb = n + 8; // quiet zone
var backgroundFill = GetSvgFillParts(Background);
// --- Center Image math (viewBox units == modules incl. quiet zone) ---
bool hasImage = !string.IsNullOrWhiteSpace(Image);
// Clamp percent for scan reliability (5%..60% of QR inner box)
@@ -32,14 +34,14 @@
height="@Size"
@ref="@Element" style="@Style" @attributes="Attributes" class="@GetCssClass()" id="@GetId()">
<!-- Background -->
<rect x="0" y="0" width="@vb" height="@vb" fill="@Background" />
<rect x="0" y="0" width="@vb" height="@vb" fill="@backgroundFill.Color" fill-opacity="@Format(backgroundFill.Opacity)" />
@if (modules is not null)
{
<!-- finder patterns / eyes -->
@DrawEye(4, 4, EyeShapeTopLeft ?? EyeShape, EyeColorTopLeft ?? EyeColor ?? Foreground)
@DrawEye(vb - 11, 4, EyeShapeTopRight ?? EyeShape, EyeColorTopRight ?? EyeColor ?? Foreground)
@DrawEye(4, vb - 11, EyeShapeBottomLeft ?? EyeShape, EyeColorBottomLeft ?? EyeColor ?? Foreground)
@DrawEye(4, 4, EyeShapeTopLeft ?? EyeShape, EyeColorTopLeft ?? EyeColor ?? Foreground, $"{GetId()}-eye-0")
@DrawEye(vb - 11, 4, EyeShapeTopRight ?? EyeShape, EyeColorTopRight ?? EyeColor ?? Foreground, $"{GetId()}-eye-1")
@DrawEye(4, vb - 11, EyeShapeBottomLeft ?? EyeShape, EyeColorBottomLeft ?? EyeColor ?? Foreground, $"{GetId()}-eye-2")
<!-- data modules (skip eye regions) -->
@for (var r = 0; r < n; r++)
@@ -95,7 +97,7 @@
{
// Draw a 7x7 eye whose top-left screen coordinate (including quiet zone) is (x,y).
// NOTE: x,y are *SVG units* (already offset by quiet zone).
private RenderFragment DrawEye(double x, double y, QRCodeEyeShape shape, string color)
private RenderFragment DrawEye(double x, double y, QRCodeEyeShape shape, string color, string maskId)
{
return __builder =>
{
@@ -104,10 +106,14 @@
{
case QRCodeEyeShape.Rounded:
{
<!-- Outer 7x7 rounded ring -->
<rect x="@Format(x)" y="@Format(y)" width="@Format(7)" height="@Format(7)" rx="@Format(1.25)" ry="@Format(1.25)" fill="@color" />
<!-- Inner hole (background) -->
<rect x="@Format(x + 1)" y="@Format(y + 1)" width="@Format(5)" height="@Format(5)" rx="@Format(0.25)" ry="@Format(0.25)" fill="@Background" />
<defs>
<mask id="@maskId" maskUnits="userSpaceOnUse">
<rect x="@Format(x)" y="@Format(y)" width="@Format(7)" height="@Format(7)" rx="@Format(1.25)" ry="@Format(1.25)" fill="white" />
<rect x="@Format(x + 1)" y="@Format(y + 1)" width="@Format(5)" height="@Format(5)" rx="@Format(0.25)" ry="@Format(0.25)" fill="black" />
</mask>
</defs>
<!-- Outer 7x7 rounded ring with transparent hole -->
<rect x="@Format(x)" y="@Format(y)" width="@Format(7)" height="@Format(7)" rx="@Format(1.25)" ry="@Format(1.25)" fill="@color" mask="url(#@maskId)" />
<!-- Pupil 3x3 rounded -->
<rect x="@Format(x + 2)" y="@Format(y + 2)" width="@Format(3)" height="@Format(3)" rx="@Format(0.75)" ry="@Format(0.75)" fill="@color" />
}
@@ -115,9 +121,14 @@
case QRCodeEyeShape.Framed:
{
<!-- Bold square frame (thickness ~1.2) -->
<rect x="@Format(x)" y="@Format(y)" width="@Format(7)" height="@Format(7)" fill="@color" />
<rect x="@Format(x + 1.2)" y="@Format(y + 1.2)" width="@Format(7 - 2 * 1.2)" height="@Format(7 - 2 * 1.2)" fill="@Background" />
<defs>
<mask id="@maskId" maskUnits="userSpaceOnUse">
<rect x="@Format(x)" y="@Format(y)" width="@Format(7)" height="@Format(7)" fill="white" />
<rect x="@Format(x + 1.2)" y="@Format(y + 1.2)" width="@Format(7 - 2 * 1.2)" height="@Format(7 - 2 * 1.2)" fill="black" />
</mask>
</defs>
<!-- Bold square frame (thickness ~1.2) with transparent hole -->
<rect x="@Format(x)" y="@Format(y)" width="@Format(7)" height="@Format(7)" fill="@color" mask="url(#@maskId)" />
<!-- Pupil -->
<rect x="@Format(x + 2)" y="@Format(y + 2)" width="@Format(3)" height="@Format(3)" fill="@color" />
}
@@ -126,9 +137,14 @@
case QRCodeEyeShape.Square:
default:
{
<!-- Classic square: 7x7 outer, 5x5 inner (background), 3x3 pupil -->
<rect x="@Format(x)" y="@Format(y)" width="@Format(7)" height="@Format(7)" fill="@color" />
<rect x="@Format(x + 1)" y="@Format(y + 1)" width="@Format(5)" height="@Format(5)" fill="@Background" />
<defs>
<mask id="@maskId" maskUnits="userSpaceOnUse">
<rect x="@Format(x)" y="@Format(y)" width="@Format(7)" height="@Format(7)" fill="white" />
<rect x="@Format(x + 1)" y="@Format(y + 1)" width="@Format(5)" height="@Format(5)" fill="black" />
</mask>
</defs>
<!-- Classic square: 7x7 outer with transparent 5x5 hole -->
<rect x="@Format(x)" y="@Format(y)" width="@Format(7)" height="@Format(7)" fill="@color" mask="url(#@maskId)" />
<rect x="@Format(x + 2)" y="@Format(y + 2)" width="@Format(3)" height="@Format(3)" fill="@color" />
}
break;

View File

@@ -1,8 +1,9 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Radzen.Blazor.Rendering;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Threading.Tasks;
namespace Radzen.Blazor
{
@@ -88,19 +89,19 @@ namespace Radzen.Blazor
/// <value>The size in CSS units. Default is "100%".</value>
[Parameter] public string Size { get; set; } = "100%";
/// <summary>
/// Gets or sets the color of the QR code modules (the dark squares/dots).
/// Supports any valid CSS color. Use high contrast with background for best scanability.
/// </summary>
/// <value>The foreground color. Default is "#000" (black).</value>
[Parameter] public string Foreground { get; set; } = "#000";
/// <summary>
/// Gets or sets the color of the QR code modules (the dark squares/dots).
/// Supports any valid CSS color. Use high contrast with background for best scanability.
/// </summary>
/// <value>The foreground color. Default is "#000" (black).</value>
[Parameter] public string Foreground { get; set; } = "#000";
/// <summary>
/// Gets or sets the background color of the QR code.
/// Should contrast well with the foreground color for reliable scanning.
/// </summary>
/// <value>The background color. Default is "#FFF" (white).</value>
[Parameter] public string Background { get; set; } = "#FFF";
/// <summary>
/// Gets or sets the background color of the QR code.
/// Should contrast well with the foreground color for reliable scanning.
/// </summary>
/// <value>The background color. Default is "#FFF" (white).</value>
[Parameter] public string Background { get; set; } = "#FFF";
/// <summary>
/// Gets or sets the visual shape of the QR code modules (data squares).
@@ -156,6 +157,28 @@ namespace Radzen.Blazor
/// </summary>
protected override string GetComponentCssClass() => "rz-qrcode";
private static string Format(double v) => v.ToString(CultureInfo.InvariantCulture);
private static (string Color, double Opacity) GetSvgFillParts(string? color)
{
if (string.IsNullOrWhiteSpace(color))
{
return ("none", 1);
}
if (string.Equals(color, "transparent", StringComparison.OrdinalIgnoreCase))
{
return ("rgb(0, 0, 0)", 0);
}
var rgb = RGB.Parse(color);
if (rgb == null)
{
return (color, 1);
}
var opacity = Math.Clamp(rgb.Alpha, 0, 1);
var fill = $"rgb({Format(rgb.Red)}, {Format(rgb.Green)}, {Format(rgb.Blue)})";
return (fill, opacity);
}
private static bool IsFinderCell(int r, int c, int n)
{
bool inTL = r < 7 && c < 7;
@@ -163,6 +186,21 @@ namespace Radzen.Blazor
bool inBL = r >= n - 7 && c < 7;
return inTL || inTR || inBL;
}
/// <summary>
/// Returns the SVG markup of the rendered QR code as a string.
/// </summary>
/// <returns>
/// A <see cref="Task{String}"/> representing the asynchronous operation. The task result contains the SVG markup of the QR code.
/// </returns>
public async Task<string> ToSvg()
{
if (JSRuntime != null)
{
return await JSRuntime.InvokeAsync<string>("Radzen.outerHTML", Element);
}
return string.Empty;
}
}
}

View File

@@ -1,7 +1,8 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using System.Globalization;
using Radzen.Blazor.Rendering;
using System.Text;
namespace Radzen.Blazor;
@@ -29,7 +30,10 @@ public enum RadzenQREcc
High
}
internal static class RadzenQREncoder
/// <summary>
/// Provides QR encoding utilities for UTF-8 strings and raw bytes.
/// </summary>
public static class RadzenQREncoder
{
/// <summary>Encode a UTF-8 string into a QR module matrix.</summary>
public static bool[,] EncodeUtf8(string value, RadzenQREcc ecc, int minVersion = 1, int maxVersion = 40)
@@ -93,6 +97,8 @@ internal static class RadzenQREncoder
/// <summary>Encode raw bytes into a QR module matrix.</summary>
public static bool[,] EncodeBytes(byte[] data, RadzenQREcc ecc = RadzenQREcc.Medium, int minVersion = 1, int maxVersion = 40)
{
ArgumentNullException.ThrowIfNull(data);
if (minVersion < 1 || maxVersion > 40 || minVersion > maxVersion)
throw new ArgumentOutOfRangeException(nameof(minVersion), "Version range must be within 1..40");
@@ -152,24 +158,93 @@ internal static class RadzenQREncoder
}
/// <summary>Render a module matrix into an SVG string with a 4-module quiet zone.</summary>
public static string ToSvg(bool[,] modules, int moduleSize = 8, string foreground = "#000000", string background = "#FFFFFF")
public static string ToSvg(
bool[,] modules,
int moduleSize = 8,
string foreground = "#000000",
string background = "#FFFFFF",
QRCodeModuleShape moduleShape = QRCodeModuleShape.Square,
QRCodeEyeShape eyeShape = QRCodeEyeShape.Square,
QRCodeEyeShape? eyeShapeTopLeft = null,
QRCodeEyeShape? eyeShapeTopRight = null,
QRCodeEyeShape? eyeShapeBottomLeft = null,
string? eyeColor = null,
string? eyeColorTopLeft = null,
string? eyeColorTopRight = null,
string? eyeColorBottomLeft = null,
string? image = null,
string imageBackground = "#FFF")
{
ArgumentNullException.ThrowIfNull(modules);
ArgumentNullException.ThrowIfNull(foreground);
ArgumentNullException.ThrowIfNull(background);
ArgumentNullException.ThrowIfNull(imageBackground);
int n = modules.GetLength(0);
int vb = n + 8; // 4 modules of quiet zone on each side
int px = vb * moduleSize;
var sb = new StringBuilder(n * n + 1024);
sb.Append(CultureInfo.InvariantCulture, $"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{px}\" height=\"{px}\" viewBox=\"0 0 {vb} {vb}\" shape-rendering=\"crispEdges\">");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"0\" y=\"0\" width=\"{vb}\" height=\"{vb}\" fill=\"{background}\"/>");
var backgroundFill = GetSvgFillParts(background);
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"0\" y=\"0\" width=\"{vb}\" height=\"{vb}\" fill=\"{backgroundFill.Color}\" fill-opacity=\"{Format(backgroundFill.Opacity)}\"/>");
var baseEyeColor = eyeColor ?? foreground;
AppendEye(sb, 4, 4, eyeShapeTopLeft ?? eyeShape, eyeColorTopLeft ?? baseEyeColor, "eye-mask-0");
AppendEye(sb, vb - 11, 4, eyeShapeTopRight ?? eyeShape, eyeColorTopRight ?? baseEyeColor, "eye-mask-1");
AppendEye(sb, 4, vb - 11, eyeShapeBottomLeft ?? eyeShape, eyeColorBottomLeft ?? baseEyeColor, "eye-mask-2");
for (int r = 0; r < n; r++)
{
for (int c = 0; c < n; c++)
{
if (modules[r, c])
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{c + 4}\" y=\"{r + 4}\" width=\"1\" height=\"1\" fill=\"{foreground}\"/>");
if (!modules[r, c]) continue;
if (IsFinderCell(r, c, n)) continue;
var x = c + 4;
var y = r + 4;
if (moduleShape == QRCodeModuleShape.Circle)
{
sb.Append(CultureInfo.InvariantCulture, $"<circle cx=\"{Format(x + 0.5)}\" cy=\"{Format(y + 0.5)}\" r=\"0.5\" fill=\"{foreground}\"/>");
}
else if (moduleShape == QRCodeModuleShape.Rounded)
{
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x)}\" y=\"{Format(y)}\" width=\"1\" height=\"1\" rx=\"0.25\" ry=\"0.25\" fill=\"{foreground}\"/>");
}
else
{
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x)}\" y=\"{Format(y)}\" width=\"1\" height=\"1\" fill=\"{foreground}\"/>");
}
}
}
if (!string.IsNullOrWhiteSpace(image))
{
const double imageSizePercent = 20;
const double imagePaddingModules = 1.0;
const double imageCornerRadius = 0.75;
const double imageBackgroundOpacity = 1.0;
double pct = Math.Clamp(imageSizePercent, 5, 60);
double boxModules = Math.Max(5, Math.Round(n * (pct / 100.0)));
double pad = Math.Max(0, imagePaddingModules);
double cutoutW = boxModules + 2 * pad;
double cutoutH = boxModules + 2 * pad;
double centerX = vb / 2.0;
double centerY = vb / 2.0;
double cutoutX = centerX - cutoutW / 2.0;
double cutoutY = centerY - cutoutH / 2.0;
double imgX = centerX - boxModules / 2.0;
double imgY = centerY - boxModules / 2.0;
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(cutoutX)}\" y=\"{Format(cutoutY)}\" width=\"{Format(cutoutW)}\" height=\"{Format(cutoutH)}\" rx=\"{Format(imageCornerRadius)}\" ry=\"{Format(imageCornerRadius)}\" fill=\"{imageBackground}\" fill-opacity=\"{Format(Math.Clamp(imageBackgroundOpacity, 0, 1))}\"/>");
sb.Append(CultureInfo.InvariantCulture, $"<image x=\"{Format(imgX)}\" y=\"{Format(imgY)}\" width=\"{Format(boxModules)}\" height=\"{Format(boxModules)}\" preserveAspectRatio=\"xMidYMid meet\" href=\"{image}\"/>");
}
sb.Append("</svg>");
return sb.ToString();
}
@@ -1065,4 +1140,69 @@ internal static class RadzenQREncoder
for (int i = len - 1; i >= 0; i--) this.Add((val >> i) & 1);
}
}
private static bool IsFinderCell(int r, int c, int n)
{
bool inTL = r < 7 && c < 7;
bool inTR = r < 7 && c >= n - 7;
bool inBL = r >= n - 7 && c < 7;
return inTL || inTR || inBL;
}
private static void AppendEye(StringBuilder sb, double x, double y, QRCodeEyeShape shape, string color, string maskId)
{
switch (shape)
{
case QRCodeEyeShape.Rounded:
sb.Append(CultureInfo.InvariantCulture, $"<defs><mask id=\"{maskId}\" maskUnits=\"userSpaceOnUse\">");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x)}\" y=\"{Format(y)}\" width=\"7\" height=\"7\" rx=\"1.25\" ry=\"1.25\" fill=\"white\"/>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x + 1)}\" y=\"{Format(y + 1)}\" width=\"5\" height=\"5\" rx=\"0.25\" ry=\"0.25\" fill=\"black\"/>");
sb.Append("</mask></defs>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x)}\" y=\"{Format(y)}\" width=\"7\" height=\"7\" rx=\"1.25\" ry=\"1.25\" fill=\"{color}\" mask=\"url(#{maskId})\"/>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x + 2)}\" y=\"{Format(y + 2)}\" width=\"3\" height=\"3\" rx=\"0.75\" ry=\"0.75\" fill=\"{color}\"/>");
break;
case QRCodeEyeShape.Framed:
sb.Append(CultureInfo.InvariantCulture, $"<defs><mask id=\"{maskId}\" maskUnits=\"userSpaceOnUse\">");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x)}\" y=\"{Format(y)}\" width=\"7\" height=\"7\" fill=\"white\"/>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x + 1.2)}\" y=\"{Format(y + 1.2)}\" width=\"{Format(7 - 2 * 1.2)}\" height=\"{Format(7 - 2 * 1.2)}\" fill=\"black\"/>");
sb.Append("</mask></defs>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x)}\" y=\"{Format(y)}\" width=\"7\" height=\"7\" fill=\"{color}\" mask=\"url(#{maskId})\"/>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x + 2)}\" y=\"{Format(y + 2)}\" width=\"3\" height=\"3\" fill=\"{color}\"/>");
break;
case QRCodeEyeShape.Square:
default:
sb.Append(CultureInfo.InvariantCulture, $"<defs><mask id=\"{maskId}\" maskUnits=\"userSpaceOnUse\">");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x)}\" y=\"{Format(y)}\" width=\"7\" height=\"7\" fill=\"white\"/>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x + 1)}\" y=\"{Format(y + 1)}\" width=\"5\" height=\"5\" fill=\"black\"/>");
sb.Append("</mask></defs>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x)}\" y=\"{Format(y)}\" width=\"7\" height=\"7\" fill=\"{color}\" mask=\"url(#{maskId})\"/>");
sb.Append(CultureInfo.InvariantCulture, $"<rect x=\"{Format(x + 2)}\" y=\"{Format(y + 2)}\" width=\"3\" height=\"3\" fill=\"{color}\"/>");
break;
}
}
private static (string Color, double Opacity) GetSvgFillParts(string? color)
{
if (string.IsNullOrWhiteSpace(color))
{
return ("none", 1);
}
if (string.Equals(color, "transparent", StringComparison.OrdinalIgnoreCase))
{
return ("rgb(0, 0, 0)", 0);
}
var rgb = RGB.Parse(color);
if (rgb == null)
{
return (color, 1);
}
var opacity = Math.Clamp(rgb.Alpha, 0, 1);
var fill = $"rgb({Format(rgb.Red)}, {Format(rgb.Green)}, {Format(rgb.Blue)})";
return (fill, opacity);
}
private static string Format(double v) => v.ToString(CultureInfo.InvariantCulture);
}

View File

@@ -15,15 +15,19 @@
@if (Visible)
{
<div @ref="@Element" style="@Style" @attributes="Attributes" class="@GetCssClass()" id="@GetId()"
role="radiogroup" @onfocus=@OnFocus @onblur=@OnBlur
role="radiogroup" aria-disabled="@(Disabled ? "true" : "false")" @onfocus=@OnFocus @onblur=@OnBlur
tabindex="@(Disabled ? "-1" : $"{TabIndex}")" @onkeydown="@(args => OnKeyPress(args))" @onkeydown:preventDefault=preventKeyPress @onkeydown:stopPropagation>
<RadzenStack Orientation="@Orientation" JustifyContent="@JustifyContent" AlignItems="@AlignItems" Gap="@Gap" Wrap="@Wrap">
@foreach (var item in allItems.Where(i => i.Visible))
{
<div @ref="@item.Element" id="@item.GetItemId()" @onclick="@(args => SelectItem(item))" @attributes="item.Attributes" class="@item.GetItemCssClass()" style="@item.Style" role="radio" aria-checked=@(IsSelected(item)? "true" : "false") aria-label="@item.Text">
<div @ref="@item.Element" id="@item.GetItemId()" @onclick="@(args => SelectItem(item))" @onkeydown="@(args => OnItemKeyDown(args, item))"
@attributes="item.Attributes" class="@item.GetItemCssClass()" style="@item.Style"
role="radio" tabindex="@(Disabled || item.Disabled ? "-1" : "0")"
aria-checked=@(IsSelected(item)? "true" : "false") aria-label="@item.Text"
aria-disabled="@(Disabled || item.Disabled ? "true" : "false")">
<div class="rz-radiobutton">
<div class="rz-helper-hidden-accessible">
<input type="radio" disabled="@Disabled" name="@Name" value="@item.Value" tabindex="-1" aria-label="@(item.Text + " " + item.Value)" @attributes="item.InputAttributes">
<input type="radio" disabled="@(Disabled || item.Disabled)" name="@Name" value="@item.Value" tabindex="-1" aria-label="@(item.Text + " " + item.Value)" @attributes="item.InputAttributes">
</div>
<div class=@ItemClass(item)>
<span class=@IconClass(item)></span>

View File

@@ -250,6 +250,15 @@ namespace Radzen.Blazor
StateHasChanged();
}
async Task OnItemKeyDown(KeyboardEventArgs args, RadzenRadioButtonListItem<TValue> item)
{
var key = args.Code != null ? args.Code : args.Key;
if (key == "Enter" || key == "Space")
{
await SelectItem(item);
}
}
/// <summary>
/// Refreshes this instance.
/// </summary>

View File

@@ -6,19 +6,19 @@
<div @ref="@Element" style=@Style @attributes="Attributes" class="@GetCssClass()" id="@GetId()">
@if (!ReadOnly)
{
<a id="@(GetId() + "cl")" aria-label="@ClearAriaLabel" @onclick="@(args => SetValue(0))" @onclick:preventDefault="true" class="rz-rating-cancel"
tabindex="@(Disabled ? "-1" : $"{TabIndex}")"
<button type="button" id="@(GetId() + "cl")" aria-label="@ClearAriaLabel" @onclick="@(args => SetValue(0))" class="rz-rating-cancel"
tabindex="@(Disabled ? "-1" : $"{TabIndex}")"
@onkeypress="@(args => OnKeyPress(args, SetValue(0)))" @onkeypress:preventDefault=preventKeyPress @onkeypress:stopPropagation>
<span class="notranslate rz-rating-icon rzi rzi-ban"></span>
</a>
</button>
}
@foreach (var index in Enumerable.Range(1, Stars))
{
<a id="@(GetId() + index.ToString() + "r")" aria-label="@RateAriaLabel" @onclick="@(args => SetValue(index))" @onclick:preventDefault="true"
<button type="button" id="@(GetId() + index.ToString() + "r")" aria-label="@RateAriaLabel" @onclick="@(args => SetValue(index))"
tabindex="@(Disabled ? "-1" : $"{TabIndex}")"
@onkeypress="@(args => OnKeyPress(args, SetValue(index)))" @onkeypress:preventDefault=preventKeyPress @onkeypress:stopPropagation>
<span class="notranslate rz-rating-icon rzi @(index <= Value ? "rzi-star": "rzi-star-o")"></span>
</a>
</button>
}
</div>
}

View File

@@ -156,7 +156,7 @@ namespace Radzen.Blazor
/// <summary>
/// Specifies the property of <typeparamref name="TItem" /> which will set <see cref="AppointmentData.Text" />.
/// </summary>
/// <value>The name of the property. Must be a <c>DateTime</c> property.</value>
/// <value>The name of the property. Must be a <c>string</c> property.</value>
[Parameter]
public string? TextProperty { get; set; }

View File

@@ -14,13 +14,17 @@
@if (Visible)
{
<div @ref="@Element" style="@Style" @attributes="Attributes" class="@GetCssClass()" id="@GetId()"
role="toolbar" aria-orientation="horizontal" @onfocus=@this.AsNonRenderingEventHandler(OnFocus) @onblur=@this.AsNonRenderingEventHandler(OnBlur)
role="toolbar" aria-orientation="@(Orientation == Orientation.Vertical ? "vertical" : "horizontal")"
aria-disabled="@(Disabled ? "true" : "false")"
@onfocus=@this.AsNonRenderingEventHandler(OnFocus) @onblur=@this.AsNonRenderingEventHandler(OnBlur)
tabindex="@(Disabled ? "-1" : $"{TabIndex}")" @onkeydown="@(args => OnKeyPress(args))" @onkeydown:preventDefault=preventKeyPress @onkeydown:stopPropagation>
@foreach (var item in allItems.Where(i => i.Visible))
{
<div @ref="@item.Element" id="@item.GetItemId()"
<button type="button" @ref="@item.Element" id="@item.GetItemId()"
@onclick="@(args => SelectItem(item))"
@attributes="item.Attributes" style="@item.Style" class=@ButtonClass(item) aria-label="@item.Text" aria-selected=@(IsSelected(item)? "true" : "false")>
@attributes="item.Attributes" style="@item.Style" class=@ButtonClass(item)
aria-label="@item.Text" aria-pressed="@(IsSelected(item)? "true" : "false")"
disabled="@(Disabled || item.Disabled)">
@if (item.Template != null)
{
@item.Template(item)
@@ -37,7 +41,7 @@
}
<span class="rz-button-text">@item.Text</span>
}
</div>
</button>
}
</div>
}

View File

@@ -1,7 +1,7 @@
@inherits RadzenComponent
@if (Visible)
{
<button aria-label="@ToggleAriaLabel" tabindex="0" @ref="@Element" style="@Style" @attributes="Attributes" class="@GetCssClass()" @onclick="@OnClick" id="@GetId()">
<button type="button" aria-label="@ToggleAriaLabel" tabindex="0" @ref="@Element" style="@Style" @attributes="Attributes" class="@GetCssClass()" @onclick="@OnClick" id="@GetId()">
<i class="notranslate rzi rz-display-flex rz-align-items-center">@(!string.IsNullOrWhiteSpace(Icon) ? Icon : "menu")</i>
</button>
}

View File

@@ -0,0 +1,131 @@
@inherits RadzenComponent
@using Radzen.Blazor.Rendering
@if (Visible)
{
<CascadingValue Value="@this">
@ChildContent
</CascadingValue>
<CascadingValue Value="@this" IsFixed="true">
<div @ref="@Element" class="@GetCssClass()" style="@Style" @attributes="Attributes">
@{
var wrapperClass = ClassList.Create("rz-spider-chart-wrapper")
.Add($"rz-spider-legend-{Legend?.Position}".ToLowerInvariant(), Legend?.Visible == true)
.ToString();
}
<div class="@wrapperClass" @onmouseleave="OnChartMouseLeave">
@if (Width != null && Height != null && Series.Count > 0)
{
<svg viewBox="@($"0 0 {Width.Value.ToInvariantString()} {Height.Value.ToInvariantString()}")">
<g class="rz-spider-chart-grid">
@* Diagonal lines first (bottom layer) *@
@foreach (var category in GetAllCategories())
{
var index = GetCategoryIndex(category);
var (x1, y1) = GetPoint(index, MinValue);
var (x2, y2) = GetPoint(index, MaxValue);
<line x1="@x1.ToInvariantString()" y1="@y1.ToInvariantString()" x2="@x2.ToInvariantString()" y2="@y2.ToInvariantString()" class="rz-spider-chart-grid-line" />
}
@* Grid shapes second (top layer) *@
@for (int i = 1; i <= 5; i++)
{
@if (GridShape == SpiderChartGridShape.Circular)
{
var centerX = Width.Value / 2;
var centerY = Height.Value / 2;
var radius = Math.Min(centerX, centerY) * 0.8 * (i / 5.0);
<circle cx="@centerX.ToInvariantString()" cy="@centerY.ToInvariantString()" r="@radius.ToInvariantString()" class="rz-spider-chart-grid-circle" />
}
else
{
<path d="@GetGridPath(i / 5.0)" class="rz-spider-chart-grid-polygon" />
}
}
</g>
<g class="rz-spider-chart-areas">
@foreach (var series in VisibleSeries)
{
var path = GetSeriesPath(series);
var isHovered = HoveredSeries == series;
var shouldShow = !IsLegendHover || HoveredSeries == null || HoveredSeries == series;
var localSeries = series;
<path d="@path"
class="rz-spider-chart-area rz-series-@(series.Index) @(isHovered && IsLegendHover ? "rz-state-hover" : "") @(!shouldShow ? "rz-state-hidden" : "")"
data-stroke-width="@series.StrokeWidth.ToInvariantString()"
@onclick="@(args => InvokeSeriesClick(localSeries))"
@onmouseenter="@(args => { OnAreaMouseEnter((MouseEventArgs)args, localSeries); })"
@onmouseleave="OnAreaMouseLeave" />
}
</g>
@if (ShowMarkers)
{
@* First layer: Render all visible markers *@
<g class="rz-spider-chart-markers">
@foreach (var series in VisibleSeries)
{
@if (series.MarkersVisible)
{
var isHovered = series == HoveredSeries;
var shouldShow = !IsLegendHover || HoveredSeries == null || HoveredSeries == series;
@foreach (var category in GetAllCategories())
{
var value = GetSeriesValue(series, category);
var index = GetCategoryIndex(category);
var (x, y) = GetPoint(index, value);
<circle cx="@x.ToInvariantString()" cy="@y.ToInvariantString()" r="@series.MarkerSize.ToInvariantString()"
class="rz-spider-chart-marker rz-series-@(series.Index) @(isHovered && IsLegendHover ? "rz-state-hover" : "") @(!shouldShow ? "rz-state-hidden" : "")"
pointer-events="none" />
}
}
}
</g>
@* Second layer: Interactive hit areas rendered AFTER visual markers so they're on top *@
<g class="rz-spider-chart-hit-areas">
@foreach (var series in VisibleSeries)
{
@if (series.MarkersVisible)
{
@foreach (var category in GetAllCategories())
{
var value = GetSeriesValue(series, category);
var index = GetCategoryIndex(category);
var (x, y) = GetPoint(index, value);
var localSeries = series;
var localCategory = category;
var localValue = value;
<circle cx="@x.ToInvariantString()" cy="@y.ToInvariantString()" r="10"
fill="transparent"
stroke="transparent"
class="rz-spider-chart-hit-area"
@onclick="@(args => InvokeSeriesClick(localSeries, localCategory, localValue, localSeries.GetData(localCategory)))"
@onmouseenter="@(args => OnMarkerMouseEnter((MouseEventArgs)args, localSeries, localCategory, localValue))"
@onmouseleave="OnMarkerMouseLeave" />
}
}
}
</g>
}
<g class="rz-spider-chart-labels">
@RenderLabels()
</g>
</svg>
@if (Legend?.Visible == true)
{
<SpiderLegend />
}
}
</div>
</div>
</CascadingValue>
}

View File

@@ -0,0 +1,601 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
using Radzen.Blazor.Rendering;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
namespace Radzen.Blazor
{
/// <summary>
/// RadzenSpiderChart component displays multi-dimensional data in a spider web format.
/// </summary>
public partial class RadzenSpiderChart : RadzenComponent, IRadzenSpiderChart
{
/// <summary>
/// Gets the legend settings for the spider chart.
/// </summary>
internal RadzenSpiderLegend Legend { get; set; } = new RadzenSpiderLegend();
// Explicit IRadzenSpiderChart implementation to avoid exposing internal members as public API.
RadzenSpiderLegend IRadzenSpiderChart.Legend
{
get => Legend;
set => Legend = value;
}
/// <summary>
/// Gets or sets child content containing RadzenSpiderSeries components.
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
/// <summary>
/// Gets or sets the grid shape of the chart.
/// </summary>
[Parameter]
public SpiderChartGridShape GridShape { get; set; } = SpiderChartGridShape.Polygon;
/// <summary>
/// Gets or sets the color scheme of the chart.
/// Available schemes include Pastel (default), Palette, Monochrome, and custom color schemes.
/// </summary>
[Parameter]
public ColorScheme ColorScheme { get; set; }
/// <summary>
/// Gets or sets whether markers are visible.
/// </summary>
[Parameter]
public bool ShowMarkers { get; set; } = true;
/// <summary>
/// Gets or sets whether tooltips are shown.
/// </summary>
[Parameter]
public bool ShowTooltip { get; set; } = true;
/// <summary>
/// Event callback for when a series (or a marker) is clicked. Matches <see cref="RadzenChart.SeriesClick" /> behavior.
/// </summary>
[Parameter]
public EventCallback<Radzen.SeriesClickEventArgs> SeriesClick { get; set; }
[Inject]
private TooltipService TooltipService { get; set; } = default!;
private double? Width { get; set; }
private double? Height { get; set; }
/// <summary>
/// Gets the series collection.
/// </summary>
internal List<IRadzenSpiderSeries> Series { get; } = new();
private IRadzenSpiderSeries? HoveredSeries { get; set; }
private bool IsLegendHover { get; set; }
private double MinValue { get; set; }
private double MaxValue { get; set; } = 100;
/// <summary>
/// Gets the visible series.
/// </summary>
internal IEnumerable<IRadzenSpiderSeries> VisibleSeries => Series.Where(s => s.IsVisible);
/// <summary>
/// Adds a series to the chart.
/// </summary>
internal void AddSeries(IRadzenSpiderSeries series)
{
if (!Series.Contains(series))
{
series.Index = Series.Count;
Series.Add(series);
UpdateMinMax();
StateHasChanged();
}
}
/// <summary>
/// Removes a series from the chart.
/// </summary>
internal void RemoveSeries(IRadzenSpiderSeries series)
{
if (Series.Remove(series))
{
UpdateMinMax();
StateHasChanged();
}
}
/// <summary>
/// Updates the min and max values based on all series data.
/// </summary>
private void UpdateMinMax()
{
var allValues = Series.SelectMany(s => s.GetValues()).ToList();
if (allValues.Count > 0)
{
MinValue = Math.Min(0, allValues.Min());
MaxValue = Math.Max(100, allValues.Max());
}
}
/// <summary>
/// Gets all unique categories from all series.
/// </summary>
private List<string> GetAllCategories()
{
var categories = new HashSet<string>();
foreach (var series in Series)
{
foreach (var category in series.GetCategories())
{
if (!string.IsNullOrEmpty(category))
{
categories.Add(category);
}
}
}
return categories.ToList();
}
/// <summary>
/// Gets the index of a category.
/// </summary>
private int GetCategoryIndex(string category)
{
var categories = GetAllCategories();
return categories.IndexOf(category);
}
/// <summary>
/// Gets the value for a series at a specific category.
/// </summary>
private double GetSeriesValue(IRadzenSpiderSeries series, string category) => series.GetValue(category);
/// <summary>
/// Formats a value for display.
/// </summary>
private string FormatValue(IRadzenSpiderSeries series, double value) => series.FormatValue(value);
/// <summary>
/// Gets the grid path for a specific scale.
/// </summary>
private string GetGridPath(double scale)
{
var categories = GetAllCategories();
var points = new List<string>();
for (int i = 0; i < categories.Count; i++)
{
var (x, y) = GetPoint(i, MaxValue * scale);
points.Add($"{x.ToString("F2", CultureInfo.InvariantCulture)},{y.ToString("F2", CultureInfo.InvariantCulture)}");
}
return $"M{string.Join(" L", points)} Z";
}
/// <summary>
/// Gets the series path for rendering.
/// </summary>
private string GetSeriesPath(IRadzenSpiderSeries series)
{
var categories = GetAllCategories();
var points = new List<string>();
foreach (var category in categories)
{
var value = GetSeriesValue(series, category);
var index = GetCategoryIndex(category);
var (x, y) = GetPoint(index, value);
points.Add($"{x.ToString("F2", CultureInfo.InvariantCulture)},{y.ToString("F2", CultureInfo.InvariantCulture)}");
}
return points.Count > 0 ? $"M{string.Join(" L", points)} Z" : "";
}
/// <summary>
/// Calculates the point position for a given index and value.
/// </summary>
private (double x, double y) GetPoint(int index, double value)
{
if (!Width.HasValue || !Height.HasValue)
{
return (0, 0);
}
var categories = GetAllCategories();
if (categories.Count == 0)
{
return (Width.Value / 2, Height.Value / 2);
}
var angleStep = 2 * Math.PI / categories.Count;
var angle = index * angleStep - Math.PI / 2;
var normalizedValue = (value - MinValue) / (MaxValue - MinValue);
var centerX = Width.Value / 2;
var centerY = Height.Value / 2;
var maxRadius = Math.Min(centerX, centerY) * 0.8;
var radius = maxRadius * normalizedValue;
return (centerX + radius * Math.Cos(angle), centerY + radius * Math.Sin(angle));
}
/// <summary>
/// Gets the label position for a category.
/// </summary>
private (double x, double y) GetLabelPoint(int index)
{
if (!Width.HasValue || !Height.HasValue)
{
return (0, 0);
}
var categories = GetAllCategories();
if (categories.Count == 0)
{
return (Width.Value / 2, Height.Value / 2);
}
var angleStep = 2 * Math.PI / categories.Count;
var angle = index * angleStep - Math.PI / 2;
var centerX = Width.Value / 2;
var centerY = Height.Value / 2;
var maxRadius = Math.Min(centerX, centerY) * 0.8;
var labelRadius = maxRadius * 1.15; // Back to original distance
return (centerX + labelRadius * Math.Cos(angle), centerY + labelRadius * Math.Sin(angle));
}
/// <summary>
/// Gets the text anchor for a label based on its position.
/// </summary>
private string GetLabelAnchor(int index)
{
var categories = GetAllCategories();
if (categories.Count == 0) return "middle";
var angleStep = 2 * Math.PI / categories.Count;
var angle = index * angleStep - Math.PI / 2;
// More precise anchoring based on position
var x = Math.Cos(angle);
var y = Math.Sin(angle);
// Top and bottom positions
if (Math.Abs(x) < 0.1)
{
return "middle";
}
// Right side
else if (x > 0.1)
{
return "start";
}
// Left side
else
{
return "end";
}
}
/// <summary>
/// Gets the baseline for a label based on its position.
/// </summary>
private string GetLabelBaseline(int index)
{
var categories = GetAllCategories();
if (categories.Count == 0) return "middle";
var angleStep = 2 * Math.PI / categories.Count;
var angle = index * angleStep - Math.PI / 2;
var y = Math.Sin(angle);
// Top position
if (y < -0.7)
{
return "auto";
}
// Bottom position
else if (y > 0.7)
{
return "hanging";
}
// Sides
else
{
return "middle";
}
}
/// <summary>
/// Renders the category labels.
/// </summary>
private RenderFragment RenderLabels() => builder =>
{
var categories = GetAllCategories();
for (int i = 0; i < categories.Count; i++)
{
var (x, y) = GetLabelPoint(i);
var category = categories[i];
var anchor = GetLabelAnchor(i);
var baseline = GetLabelBaseline(i);
var xStr = x.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
var yStr = y.ToString("F2", System.Globalization.CultureInfo.InvariantCulture);
builder.OpenElement(0, "text");
builder.AddAttribute(1, "x", xStr);
builder.AddAttribute(2, "y", yStr);
builder.AddAttribute(3, "text-anchor", anchor);
builder.AddAttribute(4, "dominant-baseline", baseline);
builder.AddAttribute(5, "fill", "var(--rz-text-color)");
builder.AddAttribute(6, "class", "rz-spider-chart-label");
builder.AddContent(7, category);
builder.CloseElement();
}
};
/// <summary>
/// Handles series click events.
/// </summary>
private async Task InvokeSeriesClick(IRadzenSpiderSeries series, string? category = null, double? value = null, object? data = null)
{
if (!SeriesClick.HasDelegate)
{
return;
}
await SeriesClick.InvokeAsync(new Radzen.SeriesClickEventArgs
{
Title = series.Title,
Category = category,
Value = value,
Data = data
});
}
/// <summary>
/// Handles area mouse enter events.
/// </summary>
private void OnAreaMouseEnter(MouseEventArgs args, IRadzenSpiderSeries series)
{
HoveredSeries = series;
StateHasChanged();
}
/// <summary>
/// Handles area mouse leave events.
/// </summary>
private void OnAreaMouseLeave()
{
if (!IsLegendHover)
{
HoveredSeries = null;
StateHasChanged();
}
}
private IRadzenSpiderSeries? currentTooltipSeries;
private string? currentTooltipCategory;
/// <summary>
/// Handles marker mouse enter events.
/// </summary>
private async Task OnMarkerMouseEnter(MouseEventArgs args, IRadzenSpiderSeries series, string category, double value)
{
HoveredSeries = series;
await ShowMarkerTooltip(args, series, category, value);
}
/// <summary>
/// Handles marker mouse leave events.
/// </summary>
private void OnMarkerMouseLeave()
{
HoveredSeries = null;
HideTooltip();
}
/// <summary>
/// Shows tooltip for a marker.
/// </summary>
private async Task ShowMarkerTooltip(MouseEventArgs args, IRadzenSpiderSeries series, string category, double value)
{
if (!this.ShowTooltip || TooltipService == null)
return;
// Prevent duplicate tooltips
if (currentTooltipSeries == series && currentTooltipCategory == category)
return;
currentTooltipSeries = series;
currentTooltipCategory = category;
var valueStr = FormatValue(series, value);
// Open tooltip using TooltipService
// IMPORTANT: OffsetX/OffsetY are relative to the event target (SVG element) and are unreliable for positioning
// inside the chart container. Use ClientX/ClientY translated to chart-local coordinates.
double x = args.OffsetX;
double y = args.OffsetY;
if (JSRuntime != null)
{
var rect = await JSRuntime.InvokeAsync<Rect>("Radzen.clientRect", Element);
x = args.ClientX - rect.Left;
y = args.ClientY - rect.Top;
}
TooltipService.OpenChartTooltip(Element, x, y, tooltipService => builder =>
{
builder.OpenComponent<Rendering.ChartTooltip>(0);
builder.AddAttribute(1, "Title", series.Title ?? "Series");
builder.AddAttribute(2, "Label", category ?? "");
builder.AddAttribute(3, "Value", valueStr);
builder.CloseComponent();
}, new ChartTooltipOptions());
}
/// <summary>
/// Hides the tooltip.
/// </summary>
private void HideTooltip()
{
currentTooltipSeries = null;
currentTooltipCategory = null;
TooltipService?.Close();
}
/// <summary>
/// Handles chart mouse leave events.
/// </summary>
private void OnChartMouseLeave()
{
HoveredSeries = null;
IsLegendHover = false;
StateHasChanged();
}
/// <summary>
/// Refreshes the chart.
/// </summary>
internal async Task Refresh()
{
// Force all series to update to ensure legend items re-render
foreach (var series in Series)
{
series.ForceUpdate();
}
await InvokeAsync(StateHasChanged);
}
Task IRadzenSpiderChart.Refresh() => Refresh();
/// <summary>
/// Highlights a series on hover.
/// </summary>
internal void HighlightSeries(IRadzenSpiderSeries series)
{
HoveredSeries = series;
IsLegendHover = true;
InvokeAsync(StateHasChanged);
}
/// <summary>
/// Clears series highlight.
/// </summary>
internal void ClearHighlight()
{
HoveredSeries = null;
IsLegendHover = false;
InvokeAsync(StateHasChanged);
}
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender || !Width.HasValue || !Height.HasValue)
{
Rendering.Rect rect;
if (JSRuntime != null)
{
rect = await JSRuntime.InvokeAsync<Rendering.Rect>("Radzen.createResizable", Element, Reference);
}
else
{
// Fallback if JSRuntime is not available.
rect = new Rendering.Rect { Width = 0, Height = 0 };
}
if (!Width.HasValue && rect.Width > 0)
{
Width = rect.Width;
}
else if (!Width.HasValue)
{
// Fallback if JavaScript sizing fails
Width = 800;
}
if (!Height.HasValue && rect.Height > 0)
{
Height = rect.Height;
}
else if (!Height.HasValue)
{
// Fallback if JavaScript sizing fails
Height = 500;
}
// Legend width handled by flexbox layout
StateHasChanged();
}
}
/// <summary>
/// Called by JavaScript when the chart is resized.
/// </summary>
[JSInvokable]
public void Resize(double width, double height)
{
bool stateHasChanged = false;
if (Width != width)
{
Width = width;
stateHasChanged = true;
}
if (Height != height)
{
Height = height;
stateHasChanged = true;
}
// Legend width handled by flexbox layout
if (stateHasChanged)
{
StateHasChanged();
}
}
/// <inheritdoc />
protected override string GetComponentCssClass()
{
return $"rz-spider-chart rz-scheme-{ColorScheme.ToString().ToLowerInvariant()}";
}
}
/// <summary>
/// Spider chart grid shapes.
/// </summary>
public enum SpiderChartGridShape
{
/// <summary>
/// Polygon grid shape.
/// </summary>
Polygon,
/// <summary>
/// Circular grid shape.
/// </summary>
Circular
}
}

View File

@@ -0,0 +1,56 @@
using Microsoft.AspNetCore.Components;
using System.Threading.Tasks;
namespace Radzen.Blazor
{
/// <summary>
/// Configures the legend of a <see cref="RadzenSpiderChart"/>.
/// </summary>
public class RadzenSpiderLegend : ComponentBase
{
/// <summary>
/// Gets or sets the legend position.
/// </summary>
[Parameter]
public LegendPosition Position { get; set; } = LegendPosition.Right;
/// <summary>
/// Gets or sets a value indicating whether the legend is visible.
/// </summary>
[Parameter]
public bool Visible { get; set; } = true;
/// <summary>
/// Gets or sets the parent spider chart (non-generic contract).
/// </summary>
[CascadingParameter]
public IRadzenSpiderChart? Chart { get; set; }
/// <inheritdoc />
protected override void OnInitialized()
{
base.OnInitialized();
if (Chart != null)
{
Chart.Legend = this;
}
}
/// <inheritdoc />
public override async Task SetParametersAsync(ParameterView parameters)
{
var positionChanged = parameters.DidParameterChange(nameof(Position), Position);
var visibleChanged = parameters.DidParameterChange(nameof(Visible), Visible);
await base.SetParametersAsync(parameters);
if ((positionChanged || visibleChanged) && Chart != null)
{
await Chart.Refresh();
}
}
}
}

View File

@@ -0,0 +1,244 @@
@typeparam TItem
@inherits RadzenComponent
@implements IDisposable
@implements Radzen.Blazor.IRadzenSpiderSeries
@using Radzen.Blazor.Rendering
@using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.AspNetCore.Components.Web
@code {
/// <summary>
/// Gets or sets the data for this series.
/// </summary>
[Parameter]
public IEnumerable<TItem> Data { get; set; } = Enumerable.Empty<TItem>();
/// <summary>
/// Gets or sets the property that provides the category values for this series.
/// </summary>
[Parameter]
public string CategoryProperty { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the property that provides the values for this series.
/// </summary>
[Parameter]
public string ValueProperty { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the series title.
/// </summary>
[Parameter]
public string Title { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the value formatter function.
/// </summary>
[Parameter]
public Func<double, string> ValueFormatter { get; set; } = value => value.ToString("F1");
/// <summary>
/// Gets or sets the format string for values.
/// </summary>
[Parameter]
public string FormatString { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the stroke width for this series.
/// </summary>
[Parameter]
public double StrokeWidth { get; set; } = 2;
/// <summary>
/// Gets or sets whether markers are visible for this series.
/// </summary>
[Parameter]
public bool MarkersVisible { get; set; } = true;
/// <summary>
/// Gets or sets the marker size for this series.
/// </summary>
[Parameter]
public double MarkerSize { get; set; } = 6;
[CascadingParameter]
public RadzenSpiderChart? Chart { get; set; }
private bool _registered;
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
if (Chart != null && !_registered)
{
_registered = true;
Chart.AddSeries(this);
}
}
/// <summary>
/// Disposes the component and removes it from the parent chart.
/// </summary>
public new void Dispose()
{
if (Chart != null && _registered)
{
_registered = false;
Chart.RemoveSeries(this);
}
base.Dispose();
}
/// <summary>
/// Gets whether the series is visible.
/// </summary>
public bool IsVisible { get; set; } = true;
/// <summary>
/// Gets the series index in the chart.
/// </summary>
internal int Index { get; set; }
int IRadzenSpiderSeries.Index { get => Index; set => Index = value; }
string IRadzenSpiderSeries.Title => Title;
bool IRadzenSpiderSeries.IsVisible { get => IsVisible; set => IsVisible = value; }
bool IRadzenSpiderSeries.MarkersVisible => MarkersVisible;
double IRadzenSpiderSeries.MarkerSize => MarkerSize;
double IRadzenSpiderSeries.StrokeWidth => StrokeWidth;
/// <summary>
/// Measures the legend text width for this series.
/// </summary>
public double MeasureLegend()
{
if (string.IsNullOrEmpty(Title))
{
return 0;
}
return TextMeasurer.TextWidth(Title);
}
double IRadzenSpiderSeries.MeasureLegend() => MeasureLegend();
IEnumerable<string> IRadzenSpiderSeries.GetCategories()
{
if (Data == null || string.IsNullOrEmpty(CategoryProperty))
{
return Enumerable.Empty<string>();
}
var categoryGetter = PropertyAccess.Getter<TItem, string>(CategoryProperty);
return Data.Select(item => categoryGetter(item))
.Where(c => !string.IsNullOrEmpty(c))
.Distinct()
.ToList();
}
IEnumerable<double> IRadzenSpiderSeries.GetValues()
{
if (Data == null || string.IsNullOrEmpty(ValueProperty))
{
return Enumerable.Empty<double>();
}
var valueGetter = PropertyAccess.Getter<TItem, double>(ValueProperty);
return Data.Select(item => valueGetter(item)).ToList();
}
double IRadzenSpiderSeries.GetValue(string category)
{
if (Data == null || string.IsNullOrEmpty(CategoryProperty) || string.IsNullOrEmpty(ValueProperty))
{
return 0;
}
var categoryGetter = PropertyAccess.Getter<TItem, string>(CategoryProperty);
var valueGetter = PropertyAccess.Getter<TItem, double>(ValueProperty);
var item = Data.FirstOrDefault(d => categoryGetter(d) == category);
return item != null ? valueGetter(item) : 0;
}
object? IRadzenSpiderSeries.GetData(string category)
{
if (Data == null || string.IsNullOrEmpty(CategoryProperty))
{
return null;
}
var categoryGetter = PropertyAccess.Getter<TItem, string>(CategoryProperty);
return Data.FirstOrDefault(d => categoryGetter(d) == category);
}
string IRadzenSpiderSeries.FormatValue(double value)
{
if (ValueFormatter != null)
{
return ValueFormatter(value);
}
if (!string.IsNullOrEmpty(FormatString))
{
return value.ToString(FormatString, System.Globalization.CultureInfo.InvariantCulture);
}
return value.ToString("F1", System.Globalization.CultureInfo.InvariantCulture);
}
RenderFragment IRadzenSpiderSeries.RenderLegendItem() => RenderLegendItem();
void IRadzenSpiderSeries.ForceUpdate() => ForceUpdate();
/// <summary>
/// Renders the legend item for this series.
/// </summary>
public RenderFragment RenderLegendItem() => RenderLegendItem(true);
/// <summary>
/// Renders the legend item for this series.
/// </summary>
protected virtual RenderFragment RenderLegendItem(bool clickable) => builder =>
{
var style = new List<string>();
if (!IsVisible)
{
style.Add("text-decoration: line-through");
style.Add("opacity: 0.5");
}
builder.OpenComponent<LegendItem>(0);
builder.AddAttribute(1, nameof(LegendItem.Index), Index);
builder.AddAttribute(2, nameof(LegendItem.MarkerType), MarkerType.Circle);
builder.AddAttribute(3, nameof(LegendItem.Style), string.Join(";", style));
builder.AddAttribute(4, nameof(LegendItem.MarkerSize), MarkerSize);
builder.AddAttribute(5, nameof(LegendItem.Text), Title);
builder.AddAttribute(6, nameof(LegendItem.Click), EventCallback.Factory.Create(this, OnLegendItemClick));
builder.AddAttribute(7, nameof(LegendItem.Clickable), clickable);
builder.CloseComponent();
};
/// <summary>
/// Handles legend item click.
/// </summary>
private async Task OnLegendItemClick()
{
if (Chart != null)
{
IsVisible = !IsVisible;
await Chart.Refresh();
}
}
/// <summary>
/// Forces the series to update its display.
/// </summary>
internal void ForceUpdate()
{
StateHasChanged();
}
}

View File

@@ -43,11 +43,12 @@
}
</span>
</button>
<button tabindex="-1" disabled="@IsDisabled" onclick="@OpenPopupScript()" class=@PopupButtonClass type="button" aria-label="@OpenAriaLabel">
<button tabindex="-1" disabled="@IsDisabled" onclick="@OpenPopupScript()" class=@PopupButtonClass type="button"
aria-label="@OpenAriaLabel" aria-haspopup="menu" aria-controls="@PopupID" aria-expanded="false">
<RadzenIcon Icon="@DropDownIcon" />
</button>
<div id="@PopupID" class="rz-splitbutton-menu">
<ul class="rz-menu-list">
<ul class="rz-menu-list" role="menu" aria-label="@OpenAriaLabel">
<CascadingValue Value=this>
@ChildContent
</CascadingValue>

View File

@@ -1,8 +1,9 @@
@inherits RadzenComponent
@if (Visible)
{
<li class=@ItemClass role="menuitem" @onclick="@OnClick" @attributes="Attributes" style="@Style">
<a id="@(SplitButton?.SplitButtonId() + GetHashCode())" class="rz-menuitem-link">
<li class=@ItemClass role="menuitem" tabindex="@(Disabled ? "-1" : "0")" aria-disabled="@(Disabled ? "true" : "false")"
@onclick="@OnClick" @onkeydown="OnKeyDown" @attributes="Attributes" style="@Style">
<span id="@(SplitButton?.SplitButtonId() + GetHashCode())" class="rz-menuitem-link">
@if (!string.IsNullOrEmpty(Icon))
{
<span class="rz-menuitem-icon" style="@(!string.IsNullOrEmpty(IconColor) ? $"color:{IconColor}" : null)">@Icon</span>
@@ -11,6 +12,6 @@
{
<span class="rz-menuitem-text">@Text</span>
}
</a>
</span>
</li>
}

View File

@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Radzen.Blazor.Rendering;
using System;
using System.Threading.Tasks;
namespace Radzen.Blazor
{
@@ -81,6 +82,21 @@ namespace Radzen.Blazor
}
}
async Task OnKeyDown(KeyboardEventArgs args)
{
ArgumentNullException.ThrowIfNull(args);
if (Disabled)
{
return;
}
var key = args.Code != null ? args.Code : args.Key;
if (key == "Enter" || key == "Space")
{
await OnClick(new MouseEventArgs());
}
}
string ItemClass => ClassList.Create("rz-menuitem")
.AddDisabled(Disabled)
.Add("rz-state-highlight", SplitButton?.IsFocused(this) == true)

View File

@@ -21,10 +21,13 @@
var step = steps[i];
@if (step.Visible)
{
<li class="@step.GetItemCssClass()" @attributes="step.Attributes" style="@step.Style" tabindex="@(step.Disabled ? -1 : 0)" @onkeypress="@(args => OnKeyPress(args, SelectStep(step, true)))" @onkeypress:preventDefault=preventKeyPress @onkeypress:stopPropagation>
<a id="@(GetId() + i.ToString() + "s")" title="@step.Title" aria-label="@step.AriaLabel"
<li class="@step.GetItemCssClass()" @attributes="step.Attributes" style="@step.Style">
<button type="button" role="tab" id="@(GetId() + i.ToString() + "s")" title="@step.Title" aria-label="@step.AriaLabel"
aria-selected="@(IsSelected(i, step) ? "true" : "false")"
aria-controls="@($"{GetId()}-panel-{i}")"
@onclick="@(async (args) => { if (!step.Disabled && AllowStepSelect) { await SelectStep(step, true); } })"
@onclick:preventDefault="true" class="rz-menuitem-link">
@onkeypress="@(args => OnKeyPress(args, SelectStep(step, true)))" @onkeypress:preventDefault=preventKeyPress @onkeypress:stopPropagation
disabled="@step.Disabled" aria-disabled="@step.Disabled" class="rz-menuitem-link">
<span class="rz-steps-number">@(steps.Where(s => s.Visible).ToList().IndexOf(step) + 1)</span>
@if (step.Template != null)
{
@@ -34,7 +37,7 @@
{
<span class="rz-steps-title">@step.Text</span>
}
</a>
</button>
</li>
}
}
@@ -47,7 +50,7 @@
{
@if (IsSelected(i, step))
{
<div class="rz-widget-content">
<div class="rz-widget-content" role="tabpanel" id="@($"{GetId()}-panel-{i}")" aria-labelledby="@(GetId() + i.ToString() + "s")">
@if (step.ChildContent != null)
{
@step.ChildContent
@@ -59,14 +62,16 @@
@if(ShowStepsButtons)
{
<div class="rz-steps-buttons">
<a id="@(GetId() + "prev")" title="@PreviousTitle" aria-label="@PreviousAriaLabel" tabindex="@(IsFirstVisibleStep() ? -1 : 0)"
<button type="button" id="@(GetId() + "prev")" title="@PreviousTitle" aria-label="@PreviousAriaLabel"
class='@($"rz-steps-prev {(IsFirstVisibleStep() ? "rz-state-disabled" : "")}")'
@onkeypress="@(args => OnKeyPress(args, PrevStep()))" @onkeypress:preventDefault=preventKeyPress @onkeypress:stopPropagation
@onclick="@PrevStep" @onclick:preventDefault="true"><span class="notranslate rzi"></span>@PreviousText</a>
<a id="@(GetId() + "next")" title="@NextTitle" aria-label="@NextAriaLabel" tabindex="@(IsLastVisibleStep() ? -1 : 0)"
disabled="@(IsFirstVisibleStep())" aria-disabled="@(IsFirstVisibleStep())"
@onkeypress="@(args => OnKeyPress(args, PrevStep()))" @onkeypress:preventDefault=preventKeyPress @onkeypress:stopPropagation
@onclick="@PrevStep"><span class="notranslate rzi"></span>@PreviousText</button>
<button type="button" id="@(GetId() + "next")" title="@NextTitle" aria-label="@NextAriaLabel"
class='@($"rz-steps-next {(IsLastVisibleStep() ? "rz-state-disabled" : "")}")'
@onkeypress="@(args => OnKeyPress(args, NextStep()))" @onkeypress:preventDefault=preventKeyPress @onkeypress:stopPropagation
@onclick="@NextStep" @onclick:preventDefault="true">@NextText<span class="notranslate rzi"></span></a>
disabled="@(IsLastVisibleStep())" aria-disabled="@(IsLastVisibleStep())"
@onkeypress="@(args => OnKeyPress(args, NextStep()))" @onkeypress:preventDefault=preventKeyPress @onkeypress:stopPropagation
@onclick="@NextStep">@NextText<span class="notranslate rzi"></span></button>
</div>
}
</div>

View File

@@ -3,11 +3,12 @@
@inherits FormComponent<bool>
@if (Visible)
{
<div @ref="@Element" @attributes="Attributes" class="@GetCssClass()" id="@GetId()"
@onclick="@Toggle" @onkeypress="@(args => OnKeyPress(args, Toggle()))" @onkeypress:preventDefault=preventKeyPress @onkeypress:stopPropagation style="@Style" tabindex="@(Disabled ? "-1" : $"{TabIndex}")">
<div class="rz-helper-hidden-accessible">
<input type="checkbox" name="@Name" id="@Name" checked="@Value" value="@ValueAsString" tabindex="-1" aria-checked="@(Value.ToString().ToLowerInvariant())" @attributes=@InputAttributes>
</div>
<label @ref="@Element" @attributes="Attributes" class="@GetCssClass()" id="@GetId()" style="@Style">
<input type="checkbox" role="switch" name="@Name" id="@Name" checked="@Value"
value="@ValueAsString" tabindex="@(Disabled || ReadOnly ? "-1" : $"{TabIndex}")"
disabled="@(Disabled || ReadOnly)" aria-checked="@(Value.ToString().ToLowerInvariant())"
@onclick="@Toggle" @onkeydown="@OnKeyPress"
@onkeydown:stopPropagation class="rz-hidden-accessible" @attributes=@InputAttributes>
<span class="rz-switch-circle@(Disabled ? " rz-disabled" : ReadOnly ? " rz-readonly" : "")"></span>
</div>
</label>
}

View File

@@ -63,7 +63,7 @@ namespace Radzen.Blazor
/// Does nothing if the switch is disabled or read-only.
/// </summary>
/// <returns>A task representing the asynchronous toggle operation.</returns>
public async System.Threading.Tasks.Task Toggle()
public async Task Toggle()
{
if (Disabled || ReadOnly)
{
@@ -77,20 +77,13 @@ namespace Radzen.Blazor
await Change.InvokeAsync(Value);
}
bool preventKeyPress = true;
async Task OnKeyPress(KeyboardEventArgs args, Task task)
async Task OnKeyPress(KeyboardEventArgs args)
{
var key = args.Code != null ? args.Code : args.Key;
if (key == "Space" || key == "Enter")
if (key == "Enter")
{
preventKeyPress = true;
await task;
}
else
{
preventKeyPress = false;
await Toggle();
}
}
}

View File

@@ -6,7 +6,8 @@
{
<div @ref=@Element style=@Style @attributes=@Attributes class=@GetCssClass() id=@GetId()
tabindex=0 @onkeydown="@(args => OnKeyPress(args))" @onkeydown:preventDefault=preventKeyPress @onkeydown:stopPropagation>
<ul role="tablist" class="rz-tabview-nav">
<ul role="tablist" class="rz-tabview-nav"
aria-orientation="@(TabPosition == TabPosition.Left || TabPosition == TabPosition.Right ? "vertical" : "horizontal")">
@Tabs
</ul>
<div class="rz-tabview-panels">

View File

@@ -2,9 +2,15 @@
@implements IDisposable
@if ((Tabs?.RenderMode == TabRenderMode.Server ? Visible : true))
{
<li role="tab" @attributes=@Attributes style=@getStyle() class=@Class
aria-selected=@(IsSelected ? "true" : "false") aria-controls="@($"{Tabs?.Id ?? ""}-tabpanel-{Index}")">
<a @onclick=@OnClick @onclick:preventDefault="true" id="@($"{Tabs?.Id ?? ""}-tabpanel-{Index}-label")" @onkeydown:stopPropagation>
<li role="presentation" style=@getStyle() class=@Class>
<button type="button" role="tab" @attributes=@Attributes @onclick=@OnClick
id="@($"{Tabs?.Id ?? ""}-tabpanel-{Index}-label")"
aria-selected="@(IsSelected ? "true" : "false")"
aria-controls="@($"{Tabs?.Id ?? ""}-tabpanel-{Index}")"
aria-disabled="@(Disabled ? "true" : "false")"
disabled="@Disabled"
tabindex="@(Disabled ? "-1" : (IsSelected ? "0" : "-1"))"
@onkeydown:stopPropagation>
@if (!string.IsNullOrEmpty(Icon))
{
<span class="notranslate rzi rz-tabview-icon" style="@(!string.IsNullOrEmpty(IconColor) ? $"color:{IconColor}" : null)">@Icon</span>
@@ -17,6 +23,6 @@
{
<span class="rz-tabview-title">@Text</span>
}
</a>
</button>
</li>
}

View File

@@ -29,18 +29,20 @@
@onkeydown:preventDefault="@preventKeyPress" @onkeydown:stopPropagation />
@if (ShowPopupButton)
{
<button type="button" aria-label="@TogglePopupAriaLabel" disabled="@Disabled"
tabindex="-1" class="@GetTogglePopupButtonClass()"
<button type="button" aria-label="@TogglePopupAriaLabel" aria-haspopup="dialog"
aria-expanded="@isPopupOpen.ToString().ToLowerInvariant()" aria-controls="@($"{GetId()}-popup")"
disabled="@Disabled" tabindex="-1" class="@GetTogglePopupButtonClass()"
@onclick="@ClickPopupButton">
<span aria-hidden="true" class="notranslate rz-button-icon-left rzi rzi-timespan"></span><span class="rz-button-text"></span>
</button>
}
@if (AllowClear && HasValue && (_isNullable || ConfirmedValue != DefaultNonNullValue) && !Disabled && !ReadOnly)
{
<i class="notranslate rz-dropdown-clear-icon rzi rzi-times" @onclick="@Clear" @onclick:stopPropagation="true"></i>
<button type="button" class="notranslate rz-dropdown-clear-icon rzi rzi-times" aria-label="@ClearAriaLabel" @onclick="@Clear" @onclick:stopPropagation="true"></button>
}
<Popup @ref="@popup" Lazy="@(PopupRenderMode == PopupRenderMode.OnDemand)"
<Popup id="@($"{GetId()}-popup")" @ref="@popup" Lazy="@(PopupRenderMode == PopupRenderMode.OnDemand)"
Open="@OnPopupOpen" Close="@OnPopupClose"
role="dialog" aria-label="@PopupAriaLabel"
class="rz-timespanpicker-popup-container">
@RenderPanel()
</Popup>

View File

@@ -161,6 +161,19 @@ namespace Radzen.Blazor
/// </summary>
[Parameter]
public string TogglePopupAriaLabel { get; set; } = "Toggle popup";
/// <summary>
/// Specifies the aria label for the popup.
/// </summary>
[Parameter]
public string PopupAriaLabel { get; set; } = "Time span picker";
/// <summary>
/// Gets or sets the clear button aria label text.
/// </summary>
/// <value>The clear button aria label text.</value>
[Parameter]
public string ClearAriaLabel { get; set; } = "Clear";
#endregion
#region Parameters: panel config
@@ -716,6 +729,8 @@ namespace Radzen.Blazor
private Task ClosePopup()
=> Inline ? Task.CompletedTask : popup?.CloseAsync(Element) ?? Task.CompletedTask;
private bool isPopupOpen;
private async Task PopupKeyDown(KeyboardEventArgs args)
{
var key = args.Code ?? args.Key;
@@ -728,11 +743,13 @@ namespace Radzen.Blazor
private void OnPopupOpen()
{
isPopupOpen = true;
ResetUnconfirmedValue();
preventKeyPress = true;
}
private void OnPopupClose()
{
isPopupOpen = false;
ResetUnconfirmedValue();
preventKeyPress = false;
}

View File

@@ -1,7 +1,16 @@
@inherits RadzenComponentWithChildren
<li role="presentation" @ref=@Element style=@Style @attributes=@Attributes class=@GetCssClass() id=@GetId()>
<div class=@WrapperClass>
<a href=@Selector class=@LinkClass @onclick:preventDefault @onclick=@OnClickAsync>@if (Template is null) {@Text} else {@Template}</a>
<button type="button" class=@LinkClass @onclick=@OnClickAsync aria-current="@(selected ? "location" : null)">
@if (Template is null)
{
@Text
}
else
{
@Template
}
</button>
</div>
@if (ChildContent != null)
{

View File

@@ -6,10 +6,12 @@
type="@((Enum.GetName(typeof(ButtonType), ButtonType) ?? ButtonType.ToString()).ToLower())"
@attributes="Attributes" class="@GetCssClass()" id="@GetId()"
@onclick="@OnClick" tabindex="@(Disabled ? "-1" : $"{TabIndex}")"
aria-label="@AriaLabel">
<div class="rz-helper-hidden-accessible">
<input type="checkbox" name="@Name" id="@Name" checked="@Value" value="@(Value.ToString().ToLower())" tabindex="-1" aria-checked="@(Value.ToString().ToLowerInvariant())" @attributes=@InputAttributes>
</div>
aria-label="@AriaLabel" aria-pressed="@(Value.ToString().ToLowerInvariant())"
aria-expanded="@AriaExpanded" aria-controls="@AriaControls" aria-haspopup="@AriaHasPopup">
@if (!string.IsNullOrEmpty(Name))
{
<input type="hidden" name="@Name" id="@Name" value="@(Value.ToString().ToLowerInvariant())" @attributes=@InputAttributes />
}
<span class="rz-button-box">
@if (ChildContent != null)
{

Some files were not shown because too many files have changed in this diff Show More