Compare commits

...

68 Commits

Author SHA1 Message Date
Vladimir Enchev
7ae64ba919 Version updated 2026-02-12 12:22:54 +02:00
Vladimir Enchev
6e94a7c65b DataGrid column defaults lost on clear filter 2026-02-12 12:22:18 +02:00
yordanov
44e20b2b5f Update premium themes 2026-02-12 11:49:41 +02:00
Atanas Korchev
d109c2e295 Remove NET7 preprocessor guards from Blazor components.
Assume .NET 7+ paths by default and delete legacy fallback-only event handler declarations.
2026-02-12 11:22:24 +02:00
yordanov
0feffad278 Fix secondary text button colors in dark themes 2026-02-12 11:20:39 +02:00
Vladimir Enchev
3be412643f Revert "DataGrid column initialization logic improved"
This reverts commit 547a681878.
2026-02-12 11:10:01 +02:00
Atanas Korchev
6d8c4ceb16 Remove NET6-specific conditions from Blazor library sources.
Drop net6 package references and inline NET6_0_OR_GREATER code paths now that .NET 6 support is no longer targeted.
2026-02-12 10:46:45 +02:00
Atanas Korchev
40b7d84224 Remove .NET 6 instructions from onboarding demo pages.
Keep tab navigation stable by defaulting to the first tab when legacy version routes are requested.
2026-02-12 10:46:45 +02:00
Atanas Korchev
4511c654f9 Normalize demo routes and enforce canonical URL redirects.
Use middleware to permanently redirect legacy/docs aliases and trailing-slash URLs to canonical paths, and keep only canonical @page directives on demo pages to prevent duplicate content and route drift.
2026-02-12 10:46:45 +02:00
Vladimir Enchev
547a681878 DataGrid column initialization logic improved 2026-02-12 10:38:46 +02:00
yordanov
cad80d533d Fix RadzenLogin tests 2026-02-12 10:29:50 +02:00
yordanov
f485420ba9 Fix RadzenLink sizing and RadzenLogin button styles 2026-02-12 10:23:33 +02:00
Vladimir Enchev
ac61235a48 ExpressionParser support for qualified array type fixed 2026-02-12 09:34:32 +02:00
Vladimir Enchev
61bd1db8b9 More possible DataGrid memory leaks fixed 2026-02-11 22:41:37 +02:00
Vladimir Enchev
5992a97ef9 RadzenDropDownDataGrid will not search as you type
Fix #2444
2026-02-11 22:31:08 +02:00
Vladimir Enchev
157070c922 DropDownBase selection not cleared in some cases 2026-02-11 22:03:10 +02:00
Vladimir Enchev
14e39f665f Version updated 2026-02-11 11:36:55 +02:00
Vladimir Enchev
2f77a0b849 Fixed Invalid operation exception with complex filter queries
Expression.Default support added to ExpressionSerializer
2026-02-11 11:36:35 +02:00
Vladimir Enchev
c3af020b81 Tooltip position in RTL mode improved 2026-02-11 11:16:53 +02:00
Vladimir Enchev
19460b899b Fix to possible DotNetObjectReference instance was already disposed 2026-02-11 10:50:41 +02:00
Vladimir Enchev
3cf12c82bf Version updated 2026-02-10 18:09:08 +02:00
Vladimir Enchev
adf86b2896 DataGrid CheckBoxList Filter crashes page in Firefox
Fixed null value for attributes and edge cases in Blazor HTML update under FF #2442
2026-02-10 18:08:28 +02:00
Atanas Korchev
7b96fe3cb4 Fix llms.txt generator: preserve @code blocks, fix @page regex, remove noise
- Add \b word boundary to directive regex so @page no longer matches
  @pageSizeOptions, @pageIndex etc., which was stripping attribute values
  and leaving broken Razor markup in the output.
- Stop stripping @code blocks from example files — they contain the C#
  code (methods, fields, event handlers) needed to understand examples.
- Exclude AccessibilityPage, GetStarted, and ThemeServicePage from
  generation (generic setup/WCAG content, not component documentation).
- Skip "Keyboard Navigation" sections (shortcuts rendered from C# data
  that the generator cannot extract, leaving empty placeholders).
- Skip "Radzen Blazor Studio" sections (IDE-specific, not component API).
2026-02-09 20:17:33 +02:00
Atanas Korchev
9a58c3a35a Exclude sub-component pages from llms.txt sections
Sub-component .razor files without @page directives were getting
standalone sections with rendered placeholder text instead of being
embedded as code snippets via <RadzenExample> in their parent pages.
2026-02-09 17:35:55 +02:00
Atanas Korchev
876c248a8e Cleanup llms.txt from marketing content and other noise. 2026-02-09 17:12:48 +02:00
Vladimir Enchev
0ce1c06178 Version updated 2026-02-09 15:44:41 +02:00
Vladimir Enchev
16f4f1839a Fixed EF Core keyless entities exception (SQL View) 2026-02-09 15:44:24 +02:00
Vladimir Enchev
b5e30cbfe0 changelog update with v9 rendering changes 2026-02-09 10:34:46 +02:00
Vladimir Enchev
604fdfb28a net6.0 support removed 2026-02-09 10:34:27 +02:00
Vladimir Enchev
d5b40d5e9b Version updated 2026-02-09 09:56:38 +02:00
yordanov
13e1a55ee8 Update premium themes 2026-02-09 09:52:09 +02:00
Vladimir Enchev
28e9090d45 Various accessibility issues fixes (#2423)
* 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

* Add rz-hidden-accessible styles

* Update RadzenRating styles

* RadzenSwitch fixed

* RadzenFabMenu fixed

* tests updated

* Update expand/collapse button styles in RadzenDataGrid

* Fix responsive pager styles

---------

Co-authored-by: yordanov <vasil@yordanov.info>
2026-02-09 09:25:34 +02:00
Vladimir Enchev
d03020062e Various possible memory leaks fixed 2026-02-09 09:07:29 +02:00
Atanas Korchev
fc6fe54635 Fix 302 redirect during SSR that hurt SEO for clean URLs
During server-side prerendering the ThemeChanged handler would call
NavigateTo which resulted in a 302 redirect (e.g. /datagrid → /datagrid?theme=material3).
This conflicted with the canonical tag pointing back to the clean URL,
confusing Google and causing ranking drops. Unsubscribe from ThemeChanged
during SSR so the redirect no longer occurs. Interactive theme switching
is unaffected since WebAssembly uses a separate scoped instance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 21:33:34 +02:00
Vladimir Enchev
505ffa7fe4 Splitter ChangeStateOnResize added 2026-02-06 12:36:29 +02:00
Vladimir Enchev
4f82ac9599 Accessing sub properties of nullable types improved 2026-02-06 10:36:53 +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
257 changed files with 6261 additions and 2286 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

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
@@ -1026,6 +1026,36 @@ public class ExpressionParserTests
Assert.True(func(new Person { BirthDate = DateTime.Parse("5/5/2000 12:00:00 AM") }));
}
class EmployeeWithHireDate
{
public DateTime? HireDate { get; set; }
public DateOnly? HireDateOnly { get; set; }
}
[Fact]
public void Should_SupportNullableDateTimeArrayWithSpecifyKindAndNullableProperty()
{
var predicate = "x => new System.DateTime?[] { DateTime.SpecifyKind(DateTime.Parse(\"2012-04-01\", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind), DateTimeKind.Unspecified) }.Contains((x.HireDate ?? null))";
var expression = ExpressionParser.ParsePredicate<EmployeeWithHireDate>(predicate);
var func = expression.Compile();
var hireDate = DateTime.SpecifyKind(DateTime.Parse("2012-04-01", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind), DateTimeKind.Unspecified);
Assert.True(func(new EmployeeWithHireDate { HireDate = hireDate }));
Assert.False(func(new EmployeeWithHireDate { HireDate = DateTime.Parse("2013-01-01") }));
}
[Fact]
public void Should_SupportNullableDateOnlyArrayAndNullableProperty()
{
var predicate = "x => new System.DateOnly?[] { DateOnly.Parse(\"2012-04-01\") }.Contains((x.HireDateOnly ?? null))";
var expression = ExpressionParser.ParsePredicate<EmployeeWithHireDate>(predicate);
var func = expression.Compile();
var hireDate = DateOnly.Parse("2012-04-01");
Assert.True(func(new EmployeeWithHireDate { HireDateOnly = hireDate }));
Assert.False(func(new EmployeeWithHireDate { HireDateOnly = DateOnly.Parse("2013-01-01") }));
}
[Fact]
public void Should_SupportNumericConversion()
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -319,5 +319,35 @@ namespace Radzen.Blazor.Tests
Expression<Func<TestEntity, bool>> expr = e => !e.Tags.Contains("Member");
Assert.Equal("e => (!(e.Tags.Contains(\"Member\")))", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_DefaultExpression_ReferenceType()
{
// Simulates the NullPropagate pattern: x.Address == null ? default(string) : x.Address.City
var param = Expression.Parameter(typeof(TestEntity), "x");
var address = Expression.Property(param, "Address");
var city = Expression.Property(address, "City");
var isNull = Expression.Equal(address, Expression.Constant(null, typeof(Address)));
var whenNull = Expression.Default(typeof(string));
var conditional = Expression.Condition(isNull, whenNull, city);
var lambda = Expression.Lambda<Func<TestEntity, string>>(conditional, param);
Assert.Equal("x => ((x.Address == null) ? null : x.Address.City)", _serializer.Serialize(lambda));
}
[Fact]
public void Serializes_DefaultExpression_ValueType()
{
// Simulates a conditional with a default value type
var param = Expression.Parameter(typeof(TestEntity), "x");
var address = Expression.Property(param, "Address");
var age = Expression.Property(param, "Age");
var isNull = Expression.Equal(address, Expression.Constant(null, typeof(Address)));
var whenNull = Expression.Default(typeof(int));
var conditional = Expression.Condition(isNull, whenNull, age);
var lambda = Expression.Lambda<Func<TestEntity, int>>(conditional, param);
Assert.Equal("x => ((x.Address == null) ? default(int) : x.Age)", _serializer.Serialize(lambda));
}
}
}

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?</span>", 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</span>", component.Markup);
}
[Fact]
@@ -195,7 +195,7 @@ namespace Radzen.Blazor.Tests
parameters.Add(p => p.Register, args => { clicked = true; });
});
component.Find(".rz-secondary").Click();
component.Find(".rz-secondary.rz-variant-flat").Click();
Assert.True(clicked);
}
@@ -215,7 +215,7 @@ namespace Radzen.Blazor.Tests
parameters.Add(p => p.ResetPassword, args => { clicked = true; });
});
component.Find("a").Click();
component.Find(".rz-secondary.rz-variant-text").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(".rz-secondary.rz-variant-text").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

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using Xunit;
@@ -114,6 +114,11 @@ namespace Radzen.Blazor.Tests
public List<string> Values { get; set; }
}
public class Order
{
public DateTime? OrderDate { get; set; }
}
[Fact]
public void GetProperty_Should_Resolve_DescriptionProperty()
{
@@ -137,6 +142,14 @@ namespace Radzen.Blazor.Tests
Assert.NotNull(idProperty);
}
[Fact]
public void GetPropertyType_Resolves_NullableDateTime_Date()
{
var propertyType = PropertyAccess.GetPropertyType(typeof(Order), "OrderDate.Date");
Assert.Equal(typeof(DateTime), propertyType);
}
interface ISimpleInterface : ISimpleNestedInterface
{
string Description { get; set; }

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using Xunit;
using Radzen;
using System.Linq.Expressions;
namespace Radzen.Blazor.Tests
{
@@ -24,6 +25,11 @@ namespace Radzen.Blazor.Tests
public int Priority { get; set; }
}
private class Order
{
public DateTime? OrderDate { get; set; }
}
private List<TestItem> GetTestData()
{
return new List<TestItem>
@@ -36,6 +42,24 @@ namespace Radzen.Blazor.Tests
};
}
[Fact]
public void GetNestedPropertyExpression_Handles_NullableDateTime_Date()
{
var parameter = Expression.Parameter(typeof(Order), "x");
var expression = QueryableExtension.GetNestedPropertyExpression(parameter, "OrderDate.Date");
var getter = Expression.Lambda<Func<Order, DateTime>>(expression, parameter).Compile();
var order = new Order { OrderDate = new DateTime(2024, 2, 3, 14, 30, 0) };
var result = getter(order);
Assert.Equal(order.OrderDate.Value.Date, result);
var nullOrder = new Order { OrderDate = null };
var nullResult = getter(nullOrder);
Assert.Equal(default(DateTime), nullResult);
}
// OrderBy tests
[Fact]
public void OrderBy_SortsAscending_ByDefault()

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

@@ -105,12 +105,10 @@ namespace Radzen.Blazor
throw new ArgumentException($"Property {propertyName} does not exist");
}
#if NET6_0_OR_GREATER
if(PropertyAccess.IsDateOnly(property))
{
return false;
}
#endif
return PropertyAccess.IsDate(property);
}

View File

@@ -55,7 +55,7 @@ namespace Radzen
/// <summary>
/// Persist the current theme in a cookie. Requires <see cref="ThemeService" /> to be registered in the DI container.
/// </summary>
public class CookieThemeService
public class CookieThemeService : IDisposable
{
private readonly CookieThemeServiceOptions options;
private readonly IJSRuntime jsRuntime;
@@ -121,6 +121,13 @@ namespace Radzen
_ = jsRuntime.InvokeVoidAsync("eval", $"document.cookie = \"{cookie}\"");
}
/// <inheritdoc />
public void Dispose()
{
themeService.ThemeChanged -= OnThemeChanged;
GC.SuppressFinalize(this);
}
}
/// <summary>

View File

@@ -9,6 +9,7 @@ namespace Radzen;
internal class Debouncer : IDisposable
{
private System.Timers.Timer? timer;
private System.Timers.ElapsedEventHandler? timerElapsedHandler;
private DateTime timerStarted { get; set; } = DateTime.UtcNow.AddYears(-1);
/// <summary>
@@ -18,11 +19,9 @@ internal class Debouncer : IDisposable
/// <param name="action">The action to debounce.</param>
public void Debounce(int interval, Func<Task> action)
{
timer?.Stop();
timer = null;
ClearTimer();
timer = new System.Timers.Timer() { Interval = interval, Enabled = false, AutoReset = false };
timer.Elapsed += (s, e) =>
timerElapsedHandler = (s, e) =>
{
if (timer == null)
{
@@ -41,6 +40,7 @@ internal class Debouncer : IDisposable
//
}
};
timer.Elapsed += timerElapsedHandler;
timer.Start();
}
@@ -52,9 +52,7 @@ internal class Debouncer : IDisposable
/// <param name="action">The action to throttle.</param>
public void Throttle(int interval, Func<Task> action)
{
timer?.Stop();
timer = null;
ClearTimer();
var curTime = DateTime.UtcNow;
if (curTime.Subtract(timerStarted).TotalMilliseconds < interval)
@@ -63,7 +61,7 @@ internal class Debouncer : IDisposable
}
timer = new System.Timers.Timer() { Interval = interval, Enabled = false, AutoReset = false };
timer.Elapsed += (s, e) =>
timerElapsedHandler = (s, e) =>
{
if (timer == null)
{
@@ -82,6 +80,7 @@ internal class Debouncer : IDisposable
//
}
};
timer.Elapsed += timerElapsedHandler;
timer.Start();
timerStarted = curTime;
@@ -90,11 +89,24 @@ internal class Debouncer : IDisposable
/// <inheritdoc />
public void Dispose()
{
if(timer != null)
ClearTimer();
}
private void ClearTimer()
{
if (timer == null)
{
timer.Stop();
timer.Dispose();
timer = null;
return;
}
if (timerElapsedHandler != null)
{
timer.Elapsed -= timerElapsedHandler;
timerElapsedHandler = null;
}
timer.Stop();
timer.Dispose();
timer = null;
}
}

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

@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.JSInterop;
using System;
@@ -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>
@@ -1037,6 +1086,10 @@ namespace Radzen
selectedItems.Clear();
}
}
else if (internalValue == null && Multiple && selectedItems.Count > 0)
{
selectedItems.Clear();
}
SelectItemFromValue(internalValue);
@@ -1269,9 +1322,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

@@ -1,17 +0,0 @@
using System;
using Microsoft.AspNetCore.Components;
namespace Radzen;
#if NET7_0_OR_GREATER
#else
/// <summary>
/// Enables "onmouseenter" and "onmouseleave" event support in Blazor. Not for public use.
/// </summary>
[EventHandler("onmouseenter", typeof(EventArgs), true, true)]
[EventHandler("onmouseleave", typeof(EventArgs), true, true)]
public static class EventHandlers
{
}
#endif

View File

@@ -414,6 +414,11 @@ public class ExpressionParser
return ParseStaticMemberAccess(type, parameter);
}
if (TryParseQualifiedType(out var qualifiedType))
{
return ParseStaticMemberAccess(qualifiedType, parameter);
}
if (Peek(1).Type == TokenType.OpenParen)
{
Advance(1);
@@ -542,19 +547,10 @@ public class ExpressionParser
else
{
Type? elementType = null;
var nullable = false;
if (token.Type == TokenType.Identifier)
if (TryParseQualifiedArrayType(out var parsedElementType))
{
var typeName = token.Value;
elementType = GetWellKnownType(typeName);
Advance(1);
if (Peek().Type == TokenType.QuestionMark)
{
nullable = true;
Advance(1);
}
elementType = parsedElementType;
}
Expect(TokenType.OpenBracket);
@@ -585,11 +581,6 @@ public class ExpressionParser
elementType = elements.Count > 0 ? elements[0].Type : typeof(object);
}
if (nullable)
{
elementType = typeof(Nullable<>).MakeGenericType(elementType);
}
return Expression.NewArrayInit(elementType, elements.Select(e => ConvertIfNeeded(e, elementType)));
}
default:
@@ -653,7 +644,7 @@ public class ExpressionParser
{
var name = typeName.ToString();
var type = GetWellKnownType(name) ?? typeResolver?.Invoke(name) ?? throw new InvalidOperationException($"Could not resolve type: {typeName}");
var type = ResolveType(name) ?? throw new InvalidOperationException($"Could not resolve type: {typeName}");
if (nullable && type.IsValueType)
{
@@ -729,6 +720,156 @@ public class ExpressionParser
return Expression.Call(null, method, arguments);
}
/// <summary>
/// Tries to parse a qualified type name (e.g. System.DateTime or System.DateTime?) and returns the resolved type.
/// Advances position past the type name. The next token will be . for member access or [ for array.
/// Uses backtracking to find the longest resolvable type prefix (e.g. System.DateTime in System.DateTime.SpecifyKind).
/// </summary>
private bool TryParseQualifiedType(out Type type)
{
type = null!;
var token = Peek();
if (token.Type != TokenType.Identifier)
{
return false;
}
var startPosition = position;
var parts = new List<string> { token.Value };
Advance(1);
while (Peek().Type == TokenType.Dot)
{
Advance(1);
token = Peek();
if (token.Type != TokenType.Identifier)
{
return false;
}
parts.Add(token.Value);
Advance(1);
}
var nullable = false;
if (Peek().Type == TokenType.QuestionMark)
{
nullable = true;
Advance(1);
}
for (var i = parts.Count; i >= 1; i--)
{
var typeName = string.Join(".", parts.Take(i));
var resolvedType = ResolveType(typeName);
if (resolvedType != null)
{
if (nullable && resolvedType.IsValueType)
{
resolvedType = typeof(Nullable<>).MakeGenericType(resolvedType);
}
position = startPosition;
var tokensToConsume = (i * 2) - 1 + (nullable ? 1 : 0);
for (var t = 0; t < tokensToConsume; t++)
{
Advance(1);
}
type = resolvedType;
return true;
}
}
position = startPosition;
return false;
}
/// <summary>
/// Tries to parse a qualified array element type (e.g. System.DateTime? or DateTime) before [].
/// Advances position past the type name to the [. Returns the element type for the array.
/// </summary>
private bool TryParseQualifiedArrayType(out Type? elementType)
{
elementType = null;
var token = Peek();
if (token.Type != TokenType.Identifier)
{
return false;
}
var startPosition = position;
var parts = new List<string> { token.Value };
Advance(1);
while (Peek().Type == TokenType.Dot)
{
Advance(1);
token = Peek();
if (token.Type != TokenType.Identifier)
{
position = startPosition;
return false;
}
parts.Add(token.Value);
Advance(1);
}
var nullable = false;
if (Peek().Type == TokenType.QuestionMark)
{
nullable = true;
Advance(1);
}
if (Peek().Type != TokenType.OpenBracket)
{
position = startPosition;
return false;
}
for (var i = parts.Count; i >= 1; i--)
{
var typeName = string.Join(".", parts.Take(i));
var resolvedType = ResolveType(typeName);
if (resolvedType != null)
{
if (nullable && resolvedType.IsValueType)
{
resolvedType = typeof(Nullable<>).MakeGenericType(resolvedType);
}
elementType = resolvedType;
return true;
}
}
position = startPosition;
return false;
}
/// <summary>
/// Resolves a type name using well-known types, the optional type resolver, or by searching loaded assemblies.
/// This allows any type from loaded assemblies to be resolved without hardcoding.
/// </summary>
private Type? ResolveType(string typeName)
{
return GetWellKnownType(typeName)
?? typeResolver?.Invoke(typeName)
?? ResolveTypeFromAssemblies(typeName);
}
private static Type? ResolveTypeFromAssemblies(string typeName)
{
return AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes())
.FirstOrDefault(t =>
{
var fullName = t.FullName;
return fullName != null && fullName.Replace("+", ".", StringComparison.Ordinal) == typeName;
});
}
private static Type? GetWellKnownType(string typeName)
{
return typeName switch

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
@@ -272,6 +272,21 @@ public class ExpressionSerializer : ExpressionVisitor
return node;
}
/// <inheritdoc/>
protected override Expression VisitDefault(DefaultExpression node)
{
ArgumentNullException.ThrowIfNull(node);
if (!node.Type.IsValueType || Nullable.GetUnderlyingType(node.Type) != null)
{
_sb.Append("null");
}
else
{
_sb.Append(CultureInfo.InvariantCulture, $"default({node.Type.DisplayName(true).Replace("+", ".", StringComparison.Ordinal)})");
}
return node;
}
/// <summary>
/// Maps an ExpressionType to its corresponding C# operator.
/// </summary>

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

@@ -133,11 +133,16 @@ namespace Radzen
{
if (_data != value)
{
if (_data != null && _data is INotifyCollectionChanged oldCollection)
{
oldCollection.CollectionChanged -= OnCollectionChanged;
}
_data = value;
if (_data != null && _data is INotifyCollectionChanged)
if (_data != null && _data is INotifyCollectionChanged newCollection)
{
((INotifyCollectionChanged)_data).CollectionChanged += OnCollectionChanged;
newCollection.CollectionChanged += OnCollectionChanged;
}
OnDataChanged();

View File

@@ -26,61 +26,79 @@ public static class PropertyAccess
if (propertyName.Contains('[', StringComparison.Ordinal))
{
var arg = Expression.Parameter(typeof(TItem));
return Expression.Lambda<Func<TItem, TValue>>(QueryableExtension.GetNestedPropertyExpression(arg, propertyName ?? string.Empty, type), arg).Compile();
var arg0 = Expression.Parameter(typeof(TItem), "x");
return Expression.Lambda<Func<TItem, TValue>>(
QueryableExtension.GetNestedPropertyExpression(arg0, propertyName, type),
arg0
).Compile();
}
else
var arg = Expression.Parameter(typeof(TItem), "x");
Expression body = arg;
if (type != null)
body = Expression.Convert(body, type);
Expression AccessNoInterface(Expression instance, string memberName)
{
var arg = Expression.Parameter(typeof(TItem));
Expression body = arg;
if (type != null)
if (instance.Type.IsInterface)
{
body = Expression.Convert(body, type);
var declaringType =
new[] { instance.Type }
.Concat(instance.Type.GetInterfaces())
.FirstOrDefault(t => t.GetProperty(memberName) != null);
if (declaringType == null)
throw new InvalidOperationException($"Member '{memberName}' not found on interface '{instance.Type}'.");
return Expression.Property(instance, declaringType, memberName);
}
foreach (var member in propertyName.Split("."))
try
{
if (body.Type.IsInterface)
{
body = Expression.Property(body,
new[] { body.Type }.Concat(body.Type.GetInterfaces()).FirstOrDefault(t => t.GetProperty(member) != null)!,
member
);
}
else
{
try
{
body = Expression.PropertyOrField(body, member);
}
catch (AmbiguousMatchException)
{
var property = body.Type.GetProperty(member, BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);
if (property != null)
{
body = Expression.Property(body, property);
}
else
{
var field = body.Type.GetField(member, BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);
if (field != null)
{
body = Expression.Field(body, field);
}
}
}
}
return Expression.PropertyOrField(instance, memberName);
}
catch (AmbiguousMatchException)
{
var prop = instance.Type.GetProperty(memberName,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);
if (prop != null) return Expression.Property(instance, prop);
body = Expression.Convert(body, typeof(TValue));
var field = instance.Type.GetField(memberName,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);
if (field != null) return Expression.Field(instance, field);
return Expression.Lambda<Func<TItem, TValue>>(body, arg).Compile();
throw;
}
}
Expression AccessWithNullPropagation(Expression instance, string memberName)
{
var underlying = Nullable.GetUnderlyingType(instance.Type);
if (underlying != null && memberName is not ("Value" or "HasValue"))
{
var hasValue = Expression.Property(instance, "HasValue");
var value = Expression.Property(instance, "Value");
var accessed = AccessNoInterface(value, memberName);
return Expression.Condition(hasValue, accessed, Expression.Default(accessed.Type));
}
if (!instance.Type.IsValueType)
{
var notNull = Expression.NotEqual(instance, Expression.Constant(null, instance.Type));
var accessed = AccessNoInterface(instance, memberName);
return Expression.Condition(notNull, accessed, Expression.Default(accessed.Type));
}
return AccessNoInterface(instance, memberName);
}
foreach (var member in propertyName.Split('.'))
body = AccessWithNullPropagation(body, member);
body = Expression.Convert(body, typeof(TValue));
return Expression.Lambda<Func<TItem, TValue>>(body, arg).Compile();
}
/// <summary>
@@ -97,12 +115,10 @@ public static class PropertyAccess
{
return true;
}
#if NET6_0_OR_GREATER
if (type == typeof(DateOnly))
{
return true;
}
#endif
return false;
}
@@ -116,12 +132,10 @@ public static class PropertyAccess
if (source == null) return false;
var type = source.IsGenericType ? source.GetGenericArguments()[0] : source;
#if NET6_0_OR_GREATER
if (type == typeof(DateOnly))
{
return true;
}
#endif
return false;
}
@@ -133,9 +147,7 @@ public static class PropertyAccess
public static object? DateOnlyFromDateTime(DateTime source)
{
object? result = null;
#if NET6_0_OR_GREATER
result = DateOnly.FromDateTime(source);
#endif
return result;
}
@@ -402,7 +414,7 @@ public static class PropertyAccess
if (type != null)
{
return !type.IsInterface ?
type.GetProperty(property ?? "")?.PropertyType :
(Nullable.GetUnderlyingType(type) ?? type).GetProperty(property ?? "")?.PropertyType :
new Type[] { type }
.Concat(type.GetInterfaces())
.FirstOrDefault(t => t.GetProperty(property ?? "") != null)?

View File

@@ -38,9 +38,7 @@ namespace Radzen
private readonly NavigationManager navigationManager;
private readonly ThemeService themeService;
#if NET7_0_OR_GREATER
private readonly IDisposable? registration;
#endif
private readonly QueryStringThemeServiceOptions? options;
private readonly PropertyInfo? hasAttachedJSRuntimeProperty;
@@ -74,23 +72,23 @@ namespace Radzen
themeService.ThemeChanged += OnThemeChanged;
#if NET7_0_OR_GREATER
try
{
registration = navigationManager.RegisterLocationChangingHandler(OnLocationChanging);
}
catch (NotSupportedException)
{
// HttpNavigationManager does not support that
// HttpNavigationManager does not support RegisterLocationChangingHandler.
// This means we are server-side rendering. Unsubscribe from ThemeChanged to
// avoid calling NavigateTo which would cause a 302 redirect during prerendering.
themeService.ThemeChanged -= OnThemeChanged;
}
#endif
}
private bool RequiresChange((string? theme, bool? wcag, bool? rightToLeft) state) =>
(state.theme != null && !string.Equals(themeService.Theme, state.theme, StringComparison.OrdinalIgnoreCase)) ||
themeService.Wcag != state.wcag || themeService.RightToLeft != state.rightToLeft;
#if NET7_0_OR_GREATER
private ValueTask OnLocationChanging(LocationChangingContext context)
{
var state = GetStateFromQueryString(context.TargetLocation);
@@ -104,7 +102,6 @@ namespace Radzen
return ValueTask.CompletedTask;
}
#endif
private (string? theme, bool? wcag, bool? rightToLeft) GetStateFromQueryString(string uri)
{
@@ -155,9 +152,7 @@ namespace Radzen
{
themeService.ThemeChanged -= OnThemeChanged;
#if NET7_0_OR_GREATER
registration?.Dispose();
#endif
GC.SuppressFinalize(this);
}

View File

@@ -1,4 +1,4 @@
using Radzen;
using Radzen;
using Radzen.Blazor;
using System;
using System.Collections;
@@ -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.
@@ -419,79 +478,156 @@ namespace Radzen
internal static Expression GetNestedPropertyExpression(Expression expression, string property, Type? type = null)
{
ArgumentNullException.ThrowIfNull(expression);
if (string.IsNullOrWhiteSpace(property)) return Expression.Constant(null, typeof(object));
var parts = property.Split(separator, 2);
string currentPart = parts[0];
var currentPart = parts[0];
static Expression BuildIsNull(Expression expr)
{
var underlying = Nullable.GetUnderlyingType(expr.Type);
if (underlying != null)
{
return Expression.Not(Expression.Property(expr, "HasValue"));
}
if (!expr.Type.IsValueType)
{
return Expression.Equal(expr, Expression.Constant(null, expr.Type));
}
return Expression.Constant(false);
}
static Expression UnwrapNullableIfNeeded(Expression expr)
{
return Nullable.GetUnderlyingType(expr.Type) != null
? Expression.Property(expr, "Value")
: expr;
}
static Expression AccessMember(Expression instance, string memberName)
{
var t = instance.Type;
if (t.IsInterface)
{
var declaring =
new[] { t }.Concat(t.GetInterfaces())
.FirstOrDefault(i => i.GetProperty(memberName) != null);
if (declaring == null)
throw new InvalidOperationException($"Member '{memberName}' not found on interface '{t}'.");
return Expression.Property(instance, declaring, memberName);
}
try
{
return Expression.PropertyOrField(instance, memberName);
}
catch (AmbiguousMatchException)
{
var prop = t.GetProperty(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);
if (prop != null) return Expression.Property(instance, prop);
var field = t.GetField(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);
if (field != null) return Expression.Field(instance, field);
throw;
}
}
static Expression NullPropagate(Expression parent, Expression accessed)
{
var isNull = BuildIsNull(parent);
var whenNull = Expression.Default(accessed.Type);
return Expression.Condition(isNull, whenNull, accessed);
}
// Skip null propagation for ParameterExpression (the root LINQ parameter is never null).
// Adding a null check like `x == null ? default : x.Property` on the root parameter
// causes EF Core to fail for keyless entity types (e.g. SQL views) because it cannot
// translate `==` on entities without a primary key.
var shouldNullPropagate = expression is not ParameterExpression;
Expression member;
if (expression.Type.IsGenericType && typeof(IDictionary<,>).IsAssignableFrom(expression.Type.GetGenericTypeDefinition()) ||
typeof(IDictionary).IsAssignableFrom(expression.Type) || typeof(System.Data.DataRow).IsAssignableFrom(expression.Type))
var parentForAccess = UnwrapNullableIfNeeded(expression);
if ((parentForAccess.Type.IsGenericType && typeof(IDictionary<,>).IsAssignableFrom(parentForAccess.Type.GetGenericTypeDefinition())) ||
typeof(IDictionary).IsAssignableFrom(parentForAccess.Type) ||
typeof(System.Data.DataRow).IsAssignableFrom(parentForAccess.Type))
{
var key = currentPart.Split('"')[1];
var typeString = currentPart.Split('(')[0];
var indexer = typeof(System.Data.DataRow).IsAssignableFrom(expression.Type) ?
Expression.Property(expression, expression.Type.GetProperty("Item", new[] { typeof(string) })!, Expression.Constant(key)) :
Expression.Property(expression, expression.Type.GetProperty("Item")!, Expression.Constant(key));
member = Expression.Convert(
indexer,
parts.Length > 1 ? indexer.Type : type ?? Type.GetType(typeString.EndsWith('?') ? $"System.Nullable`1[System.{typeString.TrimEnd('?')}]" : $"System.{typeString}") ?? typeof(object));
var indexer =
typeof(System.Data.DataRow).IsAssignableFrom(parentForAccess.Type)
? Expression.Property(parentForAccess, parentForAccess.Type.GetProperty("Item", new[] { typeof(string) })!, Expression.Constant(key))
: Expression.Property(parentForAccess, parentForAccess.Type.GetProperty("Item")!, Expression.Constant(key));
var targetType =
parts.Length > 1
? indexer.Type
: type
?? Type.GetType(
typeString.EndsWith('?')
? $"System.Nullable`1[System.{typeString.TrimEnd('?')}]"
: $"System.{typeString}"
)
?? typeof(object);
member = Expression.Convert(indexer, targetType);
member = shouldNullPropagate ? NullPropagate(expression, member) : member;
}
else if (currentPart.Contains('[', StringComparison.Ordinal)) // Handle array or list indexing
else if (currentPart.Contains('[', StringComparison.Ordinal))
{
var indexStart = currentPart.IndexOf('[', StringComparison.Ordinal);
var propertyName = currentPart.Substring(0, indexStart);
var indexString = currentPart.Substring(indexStart + 1, currentPart.Length - indexStart - 2);
member = Expression.PropertyOrField(expression, propertyName);
if (int.TryParse(indexString, out int index))
var collection = AccessMember(parentForAccess, propertyName);
collection = shouldNullPropagate ? NullPropagate(expression, collection) : collection;
if (!int.TryParse(indexString, out var index))
throw new ArgumentException($"Invalid index format: {indexString}");
var underlyingCollection = Nullable.GetUnderlyingType(collection.Type) != null
? Expression.Property(collection, "Value")
: collection;
Expression indexed;
if (underlyingCollection.Type.IsArray)
{
if (member.Type.IsArray)
{
member = Expression.ArrayIndex(member, Expression.Constant(index));
}
else if (member.Type.IsGenericType &&
(member.Type.GetGenericTypeDefinition() == typeof(List<>) ||
typeof(IList<>).IsAssignableFrom(member.Type.GetGenericTypeDefinition())))
{
var itemProperty = member.Type.GetProperty("Item");
if (itemProperty != null)
{
member = Expression.Property(member, itemProperty, Expression.Constant(index));
}
}
indexed = Expression.ArrayIndex(underlyingCollection, Expression.Constant(index));
}
else
{
throw new ArgumentException($"Invalid index format: {indexString}");
var itemProp = underlyingCollection.Type.GetProperty("Item");
if (itemProp == null)
throw new InvalidOperationException($"Type '{underlyingCollection.Type}' has no indexer 'Item'.");
indexed = Expression.Property(underlyingCollection, itemProp, Expression.Constant(index));
}
}
else if (expression != null && expression.Type != null && expression.Type.IsInterface)
{
member = Expression.Property(expression,
new[] { expression.Type }.Concat(expression.Type.GetInterfaces()).FirstOrDefault(t => t.GetProperty(currentPart) != null)!,
currentPart
);
member = NullPropagate(collection, indexed);
}
else
{
if (expression == null || string.IsNullOrEmpty(currentPart))
{
return Expression.Constant(null, typeof(object));
var accessed = AccessMember(parentForAccess, currentPart);
member = shouldNullPropagate ? NullPropagate(expression, accessed) : accessed;
}
var p = expression.Type?.GetProperty(currentPart, BindingFlags.Public | BindingFlags.Instance);
member = p != null ? Expression.Property(expression, p) : Expression.PropertyOrField(expression, currentPart);
}
if (parts.Length > 1)
return GetNestedPropertyExpression(member, parts[1], type);
if (expression != null && expression.Type != null && expression.Type.IsValueType && Nullable.GetUnderlyingType(expression.Type) == null)
{
expression = Expression.Convert(expression, typeof(object));
}
return parts.Length > 1 ? GetNestedPropertyExpression(member, parts[1], type) :
(Nullable.GetUnderlyingType(member.Type) != null || member.Type == typeof(string)) ?
expression != null ? Expression.Condition(Expression.Equal(expression, Expression.Constant(null)), Expression.Constant(null, member.Type), member) : member :
member;
return member;
}
internal static Expression GetExpression<T>(ParameterExpression parameter, FilterDescriptor filter, FilterCaseSensitivity filterCaseSensitivity, Type type)
@@ -1695,7 +1831,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

@@ -1,17 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<NoWarn>BL9993;BL0007;BL0005</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks>net7.0;net8.0;net9.0;net10.0</TargetFrameworks>
<RazorLangVersion>7.0</RazorLangVersion>
<LangVersion>latest</LangVersion>
<OutputType>Library</OutputType>
<IsPackable>true</IsPackable>
<PackageId>Radzen.Blazor</PackageId>
<Product>Radzen.Blazor</Product>
<Version>8.6.4</Version>
<Version>9.0.4</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>
@@ -36,8 +36,6 @@
<ItemGroup>
<PackageReference Include="DartSassBuilder" Version="1.1.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.Components" Condition="'$(TargetFramework)' == 'net6.0'" Version="6.0.25" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Condition="'$(TargetFramework)' == 'net6.0'" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components" Condition="'$(TargetFramework)' == 'net7.0'" Version="7.0.14" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Condition="'$(TargetFramework)' == 'net7.0'" Version="7.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components" Condition="'$(TargetFramework)' == 'net8.0'" Version="8.0.0" />

View File

@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
using Radzen.Blazor.Rendering;
@@ -323,11 +323,13 @@ namespace Radzen.Blazor
return;
IsLoading = true;
var previousCts = cts;
#if NET8_0_OR_GREATER
await cts.CancelAsync();
await previousCts.CancelAsync();
#else
cts.Cancel();
previousCts.Cancel();
#endif
previousCts.Dispose();
cts = new CancellationTokenSource();
// Ensure we have a session ID

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

@@ -326,8 +326,6 @@ namespace Radzen
debouncer?.Dispose();
debouncer = null;
reference?.Dispose();
reference = null;
if (IsJSRuntimeAvailable && JSRuntime != null && !string.IsNullOrEmpty(UniqueID))
{
@@ -346,6 +344,9 @@ namespace Radzen
JSRuntime.InvokeVoid("Radzen.removeMouseLeave", UniqueID);
}
}
reference?.Dispose();
reference = null;
}
/// <summary>

View File

@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -10,9 +10,7 @@ namespace Radzen.Blazor
/// RadzenDataFilter component.
/// </summary>
/// <typeparam name="TItem">The type of the item.</typeparam>
#if NET6_0_OR_GREATER
[CascadingTypeParameter(nameof(TItem))]
#endif
public partial class RadzenDataFilter<TItem> : RadzenComponent
{
/// <inheritdoc />

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;
@@ -82,9 +83,7 @@ namespace Radzen.Blazor
/// }
/// </code>
/// </example>
#if NET6_0_OR_GREATER
[CascadingTypeParameter(nameof(TItem))]
#endif
public partial class RadzenDataGrid<TItem> : PagedDataBoundComponent<TItem> where TItem : notnull
{
private static readonly string[] DefaultGroupProperty = new string[] { "it" };
@@ -892,7 +891,7 @@ namespace Radzen.Blazor
}
}
void ToggleColumns()
async Task ToggleColumns()
{
if (selectedColumns == null)
{
@@ -906,8 +905,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 +1631,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 +2027,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 +2508,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)
{
@@ -3568,6 +3580,11 @@ namespace Radzen.Blazor
groups.CollectionChanged -= GroupsCollectionChanged;
}
if (sortDescriptors != null)
{
sortDescriptors.CollectionChanged -= SortsCollectionChanged;
}
if (IsJSRuntimeAvailable && JSRuntime != null)
{
foreach (var column in allColumns.ToList().Where(c => c.GetVisible()))
@@ -3576,6 +3593,68 @@ 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;
_view = null;
_groupedPagedView = null;
if (Query != null)
{
Query.GetFilter = null;
Query.Filters = null;
Query.Sorts = null;
}
GC.SuppressFinalize(this);
}

View File

@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System;
using System.Collections;
@@ -188,76 +188,80 @@ namespace Radzen.Blazor
if (Grid != null)
{
Grid.AddColumn(this);
SetColumnDefaults();
}
}
var canSetFilterPropertyType = (FilterMode ?? Grid.FilterMode) == Radzen.FilterMode.CheckBoxList && FilterTemplate == null;
private void SetColumnDefaults()
{
var canSetFilterPropertyType = (FilterMode ?? Grid.FilterMode) == Radzen.FilterMode.CheckBoxList && FilterTemplate == null;
if (canSetFilterPropertyType)
if (canSetFilterPropertyType)
{
if (Type == null)
{
if (Type == null)
{
var fp = GetFilterProperty();
var pt = !string.IsNullOrEmpty(fp) ?
PropertyAccess.GetPropertyType(typeof(TItem), fp) : typeof(object);
var fp = GetFilterProperty();
var pt = !string.IsNullOrEmpty(fp) ?
PropertyAccess.GetPropertyType(typeof(TItem), fp) : typeof(object);
_filterPropertyType = typeof(IEnumerable<>).MakeGenericType(pt!);
}
if (GetFilterOperator() == FilterOperator.Equals)
{
SetFilterOperator(FilterOperator.Contains);
}
Grid.FilterPopupRenderMode = PopupRenderMode.OnDemand;
_filterPropertyType = typeof(IEnumerable<>).MakeGenericType(pt!);
}
var property = GetFilterProperty();
if (!string.IsNullOrEmpty(property) && Type == null)
{
_propertyType = PropertyAccess.GetPropertyType(typeof(TItem), property);
}
if (!string.IsNullOrEmpty(property) && Type == null && !canSetFilterPropertyType)
{
_filterPropertyType = _propertyType;
}
if (_filterPropertyType == null)
{
_filterPropertyType = Type;
}
else if (!string.IsNullOrEmpty(Property))
{
propertyValueGetter = PropertyAccess.Getter<TItem, object>(Property);
}
if (!string.IsNullOrEmpty(Property) && (typeof(TItem).IsGenericType && typeof(IDictionary<,>).IsAssignableFrom(typeof(TItem).GetGenericTypeDefinition()) ||
typeof(IDictionary).IsAssignableFrom(typeof(TItem)) || typeof(System.Data.DataRow).IsAssignableFrom(typeof(TItem))))
{
propertyValueGetter = PropertyAccess.Getter<TItem, object>(Property);
}
if (_filterPropertyType == typeof(string) && filterOperator != FilterOperator.Custom && filterOperator == null && _filterOperator == null)
if (GetFilterOperator() == FilterOperator.Equals)
{
SetFilterOperator(FilterOperator.Contains);
}
if (!string.IsNullOrEmpty(Property) && !string.IsNullOrEmpty(FilterProperty))
UniqueID = $"{Property}.{FilterProperty}"; // To be sure the column uniqueID is unique even when filtering on sub property.
else
UniqueID = !string.IsNullOrEmpty(Property) ? Property : FilterProperty;
if (UseDisplayName && !string.IsNullOrEmpty(Property))
Grid.FilterPopupRenderMode = PopupRenderMode.OnDemand;
}
var property = GetFilterProperty();
if (!string.IsNullOrEmpty(property) && Type == null)
{
_propertyType = PropertyAccess.GetPropertyType(typeof(TItem), property);
}
if (!string.IsNullOrEmpty(property) && Type == null && !canSetFilterPropertyType)
{
_filterPropertyType = _propertyType;
}
if (_filterPropertyType == null)
{
_filterPropertyType = Type;
}
else if (!string.IsNullOrEmpty(Property))
{
propertyValueGetter = PropertyAccess.Getter<TItem, object>(Property);
}
if (!string.IsNullOrEmpty(Property) && (typeof(TItem).IsGenericType && typeof(IDictionary<,>).IsAssignableFrom(typeof(TItem).GetGenericTypeDefinition()) ||
typeof(IDictionary).IsAssignableFrom(typeof(TItem)) || typeof(System.Data.DataRow).IsAssignableFrom(typeof(TItem))))
{
propertyValueGetter = PropertyAccess.Getter<TItem, object>(Property);
}
if (_filterPropertyType == typeof(string) && filterOperator != FilterOperator.Custom && filterOperator == null && _filterOperator == null)
{
SetFilterOperator(FilterOperator.Contains);
}
if (!string.IsNullOrEmpty(Property) && !string.IsNullOrEmpty(FilterProperty))
UniqueID = $"{Property}.{FilterProperty}"; // To be sure the column uniqueID is unique even when filtering on sub property.
else
UniqueID = !string.IsNullOrEmpty(Property) ? Property : FilterProperty;
if (UseDisplayName && !string.IsNullOrEmpty(Property))
{
var propInfo = typeof(TItem).GetProperty(Property);
if (propInfo != null)
{
var propInfo = typeof(TItem).GetProperty(Property);
if (propInfo != null)
var displayAttr = propInfo.GetCustomAttributes(typeof(DisplayAttribute), true)
.FirstOrDefault() as DisplayAttribute;
if (displayAttr?.Name != null)
{
var displayAttr = propInfo.GetCustomAttributes(typeof(DisplayAttribute), true)
.FirstOrDefault() as DisplayAttribute;
if (displayAttr?.Name != null)
{
Title = displayAttr.Name;
}
Title = displayAttr.Name;
}
}
}
@@ -1310,7 +1314,7 @@ namespace Radzen.Blazor
internal bool HasCustomFilter()
{
return GetFilterOperator() == FilterOperator.Custom && GetCustomFilterExpression() != null;
return GetFilterOperator() == FilterOperator.Custom && !string.IsNullOrEmpty(GetCustomFilterExpression());
}
internal bool HasActiveFilter()
@@ -1353,7 +1357,7 @@ namespace Radzen.Blazor
/// </summary>
public virtual void ClearFilters()
{
var fo = FilterOperator == FilterOperator.Custom
var fo = GetFilterOperator() == FilterOperator.Custom
? FilterOperator.Custom
: typeof(System.Collections.IEnumerable).IsAssignableFrom(FilterPropertyType)
? !string.IsNullOrEmpty(FilterProperty) && FilterProperty != Property ? FilterOperator.In : FilterOperator.Contains
@@ -1373,6 +1377,7 @@ namespace Radzen.Blazor
LogicalFilterOperator = default(LogicalFilterOperator);
SetColumnDefaults();
}
FilterOperator? _filterOperator;

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)
@@ -167,7 +173,7 @@
}
else
{
@(!string.IsNullOrEmpty(Column.FormatString) ? string.Format(Column.FormatProvider ?? Grid?.Culture ?? CultureInfo.CurrentCulture, Column.FormatString, context ?? string.Empty) : Convert.ToString(context, Grid?.Culture ?? CultureInfo.CurrentCulture))
@(!string.IsNullOrEmpty(Column.FormatString) ? string.Format(Column.FormatProvider ?? Grid?.Culture ?? CultureInfo.CurrentCulture, Column.FormatString, context ?? string.Empty) : Convert.ToString(context ?? string.Empty, Grid?.Culture ?? CultureInfo.CurrentCulture))
}
</Template>
</RadzenListBox>

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

@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using System.Linq;
using System.Threading.Tasks;
@@ -35,9 +35,7 @@ namespace Radzen.Blazor
/// &lt;/RadzenDataList&gt;
/// </code>
/// </example>
#if NET6_0_OR_GREATER
[CascadingTypeParameter(nameof(TItem))]
#endif
public partial class RadzenDataList<TItem> : PagedDataBoundComponent<TItem>
{
/// <inheritdoc />

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

@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
@@ -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>
@@ -321,13 +356,11 @@ namespace Radzen.Blazor
UpdateYearsAndMonths(Min, Max);
#if NET6_0_OR_GREATER
if (typeof(TValue) == typeof(TimeOnly) || typeof(TValue) == typeof(TimeOnly?))
{
TimeOnly = true;
ShowTime = true;
}
#endif
}
void UpdateYearsAndMonths(DateTime? min, DateTime? max)
@@ -453,6 +486,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>
@@ -497,7 +539,6 @@ namespace Radzen.Blazor
_value = dtEnum;
_dateTimeValue = selectedDates.LastOrDefault();
}
#if NET6_0_OR_GREATER
else if (value is IEnumerable<DateTime?> ndtEnum)
{
selectedDates = ndtEnum.Where(d => d.HasValue && d.Value != default(DateTime))
@@ -508,19 +549,6 @@ namespace Radzen.Blazor
_value = ndtEnum;
_dateTimeValue = selectedDates.LastOrDefault();
}
#else
else if (value is IEnumerable<System.Nullable<DateTime>> ndtEnum)
{
selectedDates = ndtEnum.Where(d => d.HasValue && d.Value != default(DateTime))
.Select(d => DateTime.SpecifyKind(d.Value.Date, Kind))
.Distinct()
.OrderBy(d => d)
.ToList();
_value = ndtEnum;
_dateTimeValue = selectedDates.LastOrDefault();
}
#endif
#if NET6_0_OR_GREATER
else if (value is IEnumerable<DateOnly> doEnum)
{
selectedDates = doEnum.Select(d => d.ToDateTime(System.TimeOnly.MinValue, Kind).Date)
@@ -530,7 +558,6 @@ namespace Radzen.Blazor
_value = doEnum;
_dateTimeValue = selectedDates.LastOrDefault();
}
#endif
else if (value is IEnumerable<DateTimeOffset?> dtoEnum)
{
selectedDates = dtoEnum.Where(o => o.HasValue)
@@ -556,7 +583,6 @@ namespace Radzen.Blazor
_value = dtoEnum;
_dateTimeValue = selectedDates.LastOrDefault();
}
#if NET6_0_OR_GREATER
else if (value is IEnumerable<DateOnly?> doNullableEnum)
{
selectedDates = doNullableEnum.Where(d => d.HasValue)
@@ -579,7 +605,6 @@ namespace Radzen.Blazor
_value = toNullableEnum;
_dateTimeValue = selectedDates.LastOrDefault();
}
#endif
else if (value is DateTime dt && dt != default(DateTime))
{
selectedDates = new List<DateTime> { DateTime.SpecifyKind(dt.Date, Kind) };
@@ -621,7 +646,6 @@ namespace Radzen.Blazor
{
_dateTimeValue = DateTime.SpecifyKind(dateTime, Kind);
}
#if NET6_0_OR_GREATER
else if (value is DateOnly dateOnly)
{
_dateTimeValue = dateOnly.ToDateTime(System.TimeOnly.MinValue, Kind);
@@ -631,7 +655,6 @@ namespace Radzen.Blazor
_dateTimeValue = new DateTime(CurrentDate.Year, CurrentDate.Month, CurrentDate.Day, timeOnly.Hour, timeOnly.Minute, timeOnly.Second, timeOnly.Millisecond, Kind);
_currentDate = _dateTimeValue.Value;
}
#endif
else
{
_dateTimeValue = null;
@@ -646,7 +669,6 @@ namespace Radzen.Blazor
private static object? ConvertToTValue(object? value)
{
#if NET6_0_OR_GREATER
var typeofTValue = typeof(TValue);
if (value is DateTime dt)
{
@@ -661,7 +683,6 @@ namespace Radzen.Blazor
return (TValue)value;
}
}
#endif
return value;
}
@@ -1341,18 +1362,10 @@ namespace Radzen.Blazor
}
newValue = list;
}
#if NET6_0_OR_GREATER
else if (typeof(IEnumerable<DateTime?>).IsAssignableFrom(typeofTValue))
{
newValue = selectedDates.Select(d => (DateTime?)d).ToList();
}
#else
else if (typeof(System.Collections.Generic.IEnumerable<System.Nullable<DateTime>>).IsAssignableFrom(typeofTValue))
{
newValue = selectedDates.Select(d => (DateTime?)d).ToList();
}
#endif
#if NET6_0_OR_GREATER
else if (typeof(IEnumerable<DateOnly>).IsAssignableFrom(typeofTValue))
{
newValue = selectedDates.Select(d => DateOnly.FromDateTime(d)).ToList();
@@ -1365,7 +1378,6 @@ namespace Radzen.Blazor
{
newValue = selectedDates.Select(d => (TimeOnly?)new TimeOnly(d.Hour, d.Minute, d.Second, d.Millisecond)).ToList();
}
#endif
else
{
newValue = selectedDates.ToList();

View File

@@ -1,8 +1,29 @@
@implements IDisposable
@implements IAsyncDisposable
@using Microsoft.JSInterop
@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 +33,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>
}
}
}
@@ -141,11 +163,15 @@
/// <inheritdoc />
public void Dispose()
{
if (Service is null) return;
Service.OnOpen -= OnOpen;
Service.OnClose -= OnClose;
Service.OnSideOpen -= OnSideOpen;
Service.OnSideClose -= OnSideClose;
if (Service != null)
{
Service.OnOpen -= OnOpen;
Service.OnClose -= OnClose;
Service.OnSideOpen -= OnSideOpen;
Service.OnSideClose -= OnSideClose;
}
_ = DisposeSideDialogResizeHandleAsync();
}
/// <inheritdoc />
@@ -175,7 +201,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
@@ -295,17 +323,36 @@
/// <returns>A ValueTask that represents the asynchronous disposal operation.</returns>
public async ValueTask DisposeAsync()
{
Dispose();
await DisposeSideDialogResizeHandleAsync();
}
private async Task DisposeSideDialogResizeHandleAsync()
{
if (sideDialogResizeHandleJsModule == null)
{
return;
}
try
{
if (sideDialogResizeHandleJsModule != null)
{
await sideDialogResizeHandleJsModule.InvokeVoidAsync("dispose");
}
await sideDialogResizeHandleJsModule.InvokeVoidAsync("dispose");
}
catch
{
/* Ignore */
}
try
{
await sideDialogResizeHandleJsModule.DisposeAsync();
}
catch
{
/* Ignore */
}
sideDialogResizeHandleJsModule = null;
}

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>
@@ -119,7 +127,12 @@
}
else
{
@RenderFilter()
<div class="rz-dropdown-filter-container">
<input aria-label="@SearchAriaLabel" id="@SearchID" @ref="@search" tabindex="@(Disabled ? "-1" : $"{TabIndex}")" placeholder="@(FilterPlaceholder ?? string.Empty)" class="rz-dropdown-filter rz-inputtext" autocomplete="@FilterAutoCompleteType" aria-autocomplete="none" type="text"
@onchange="@OnFilter" @onkeydown="@OnFilterKeyPress" value="@(searchText ?? string.Empty)"
@oninput="@OnFilterInput" />
<span class="notranslate rz-dropdown-filter-icon rzi rzi-search"></span>
</div>
}
}
@if (Multiple && (AllowSelectAll || AllowFiltering || HeaderTemplate != null))
@@ -133,9 +146,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 +161,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 ?? string.Empty)" 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,35 +203,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>
}
@code {
internal RenderFragment RenderFilter()
{
#if NET7_0_OR_GREATER
return __builder => {
<text>
<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);})" />
<span class="notranslate rz-dropdown-filter-icon rzi rzi-search"></span>
</div>
</text>
};
#else
return __builder => {
<text>
<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);})" />
<span class="notranslate rz-dropdown-filter-icon rzi rzi-search"></span>
</div>
</text>
};
#endif
}
}

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>
@@ -123,16 +130,20 @@
else
{
<div class="rz-lookup-search">
@RenderFilter()
<input aria-label="@SearchAriaLabel" class="rz-lookup-search-input" id="@SearchID" @ref="@search" tabindex="0" placeholder="@(SearchTextPlaceholder ?? string.Empty)"
@onchange="@OnFilter" @onkeydown="@OnFilterKeyPress" value="@(searchText ?? string.Empty)" style="@(ShowSearch ? "" : "margin-right:0px;")"
@oninput="@OnFilterInput" />
@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,29 +203,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>
}
@code {
internal RenderFragment RenderFilter()
{
#if NET7_0_OR_GREATER
return __builder => {
<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);}) />
</text>
};
#else
return __builder => {
<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);}) />
</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);
}
}
}
@@ -1022,6 +1016,26 @@ namespace Radzen.Blazor
}
}
/// <inheritdoc />
protected override async Task OnFilterInput(ChangeEventArgs args)
{
ArgumentNullException.ThrowIfNull(args);
searchText = $"{args.Value}";
if (FilterAsYouType)
{
await SearchTextChanged.InvokeAsync(searchText);
if (ResetSelectedIndexOnFilter)
{
selectedIndex = -1;
}
Debounce(DebounceFilter, FilterDelay);
}
}
/// <summary>
/// Handles the filter event.
/// </summary>

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 ? "true" : "false")"
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 ? "true" : "false")"
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

@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
@@ -7,9 +7,7 @@ namespace Radzen.Blazor
/// <summary>
/// RadzenDropZoneContainer component.
/// </summary>
#if NET6_0_OR_GREATER
[CascadingTypeParameter(nameof(TItem))]
#endif
public partial class RadzenDropZoneContainer<TItem> : RadzenComponentWithChildren
{
/// <summary>

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 Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Radzen.Blazor.Rendering;
using System;
@@ -32,7 +32,7 @@ namespace Radzen.Blazor
/// &lt;/RadzenLayout&gt;
/// </code>
/// </example>
public partial class RadzenLayout : RadzenComponentWithChildren
public partial class RadzenLayout : RadzenComponentWithChildren, IDisposable
{
[Inject]
private IServiceProvider? ServiceProvider { get; set; }
@@ -57,6 +57,18 @@ namespace Radzen.Blazor
StateHasChanged();
}
/// <inheritdoc />
public override void Dispose()
{
if (themeService != null)
{
themeService.ThemeChanged -= OnThemeChanged;
}
base.Dispose();
GC.SuppressFinalize(this);
}
/// <inheritdoc />
protected override string GetComponentCssClass() => ClassList.Create("rz-layout")
.Add($"rz-{themeService?.Theme}", themeService != null)

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>
@@ -40,7 +43,9 @@
@if (AllowFiltering)
{
<div class="rz-listbox-filter-container">
@RenderFilter()
<input id="@SearchID" @ref="@search" disabled="@Disabled" class="rz-inputtext" role="textbox" type="text" placeholder="@(Placeholder ?? string.Empty)"
@onchange="@OnFilter" @onkeydown="@OnFilterKeyPress" value="@(searchText ?? string.Empty)" @onkeydown:stopPropagation="true"
@oninput="@OnFilterInput" aria-label="@SearchAriaLabel" />
<span class="notranslate rz-listbox-filter-icon rzi rzi-search"></span>
</div>
}
@@ -50,32 +55,11 @@
@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>
}
</div>
}
@code {
internal RenderFragment RenderFilter()
{
#if NET7_0_OR_GREATER
return __builder => {
<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" />
</text>
};
#else
return __builder => {
<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" />
</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 ? "true" : "false")" 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

@@ -33,18 +33,18 @@
<RadzenStack style="margin-top:1rem;" Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" JustifyContent="JustifyContent.End" Gap="0.5rem">
@if(ShowLoginButton)
{
<RadzenButton ButtonStyle="ButtonStyle.Primary" ButtonType="ButtonType.Submit" Text=@LoginText Click=@OnLogin />
<RadzenButton ButtonType="ButtonType.Submit" ButtonStyle="ButtonStyle.Primary" Text=@LoginText Click=@OnLogin />
}
@if (AllowResetPassword)
{
<a tabindex="0" class="rz-link" @onclick=@OnReset>@ResetPasswordText</a>
<RadzenButton ButtonType="ButtonType.Button" ButtonStyle="ButtonStyle.Secondary" Variant="Variant.Text" Text=@ResetPasswordText Click=@OnReset />
}
</RadzenStack>
@if (AllowRegister)
{
<div class="rz-register">
@RegisterMessageText
<RadzenButton ButtonType="ButtonType.Button" Variant="Variant.Flat" ButtonStyle="ButtonStyle.Secondary" Shade="Shade.Lighter" Text=@RegisterText Click=@OnRegister />
<RadzenButton ButtonType="ButtonType.Button" ButtonStyle="ButtonStyle.Secondary" Variant="Variant.Flat" Shade="Shade.Lighter" Text=@RegisterText Click=@OnRegister />
</div>
}
}
@@ -80,11 +80,11 @@
<div class="rz-form-input-wrapper rz-login-buttons">
@if (ShowLoginButton)
{
<RadzenButton ButtonStyle="ButtonStyle.Primary" ButtonType="ButtonType.Submit" Text=@LoginText Click=@OnLogin />
<RadzenButton ButtonType="ButtonType.Submit" ButtonStyle="ButtonStyle.Primary" Text=@LoginText Click=@OnLogin />
}
@if (AllowResetPassword)
{
<a tabindex="0" class="rz-link" @onclick=@OnReset>@ResetPasswordText</a>
<RadzenButton ButtonType="ButtonType.Button" ButtonStyle="ButtonStyle.Secondary" Variant="Variant.Text" Text=@ResetPasswordText Click=@OnReset />
}
</div>
</div>
@@ -93,7 +93,7 @@
{
<div class="rz-register">
@RegisterMessageText
<RadzenButton ButtonType="ButtonType.Button" Variant="Variant.Flat" ButtonStyle="ButtonStyle.Secondary" Shade="Shade.Lighter" Text=@RegisterText Click=@OnRegister />
<RadzenButton ButtonType="ButtonType.Button" ButtonStyle="ButtonStyle.Secondary" Variant="Variant.Flat" Shade="Shade.Lighter" Text=@RegisterText Click=@OnRegister />
</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,28 @@
@using System.Collections.Generic
@using System.Timers
@using System.Threading
@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() />
@@ -85,33 +93,87 @@
}
double progress = 0;
Timer? timer;
System.Timers.Timer? timer;
ElapsedEventHandler? timerElapsedHandler;
CancellationTokenSource? closeCts;
protected override void OnInitialized()
{
if (Message?.ShowProgress == true)
{
timer = new Timer() { Enabled = true };
timer.Elapsed += (sender, args) =>
{
progress = progress + 100;
InvokeAsync(StateHasChanged);
};
timer = new System.Timers.Timer() { Enabled = true };
timerElapsedHandler = OnTimerElapsed;
timer.Elapsed += timerElapsedHandler;
}
closeCts = new CancellationTokenSource();
_ = CloseAfterDelayAsync(Convert.ToInt32(Message?.Duration ?? 3000), closeCts.Token);
}
private void OnTimerElapsed(object? sender, ElapsedEventArgs args)
{
progress = progress + 100;
_ = InvokeAsync(StateHasChanged);
}
private async Task CloseAfterDelayAsync(int duration, CancellationToken token)
{
try
{
await Task.Delay(duration, token);
if (!token.IsCancellationRequested)
{
await InvokeAsync(Close);
}
}
catch (TaskCanceledException)
{
// Ignore
}
System.Threading.Tasks.Task.Delay(Convert.ToInt32(Message?.Duration ?? 3000)).ContinueWith(r => InvokeAsync(Close));
}
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()
{
if (closeCts != null)
{
closeCts.Cancel();
closeCts.Dispose();
closeCts = null;
}
if (timer != null && timerElapsedHandler != null)
{
timer.Elapsed -= timerElapsedHandler;
timerElapsedHandler = null;
}
timer?.Stop();
timer?.Dispose();
timer = null;
}
}

View File

@@ -1,4 +1,4 @@
@using Radzen
@using Radzen
@using Microsoft.JSInterop
@using Microsoft.AspNetCore.Components.Forms
@using System.Globalization
@@ -8,7 +8,12 @@
@if (Visible)
{
<span @ref="@Element" style="@Style" @attributes="Attributes" class="@GetCssClass()" id="@GetId()">
@RenderInput()
<input @ref="@input" inputmode="decimal" @attributes="InputAttributes" type="text" name="@Name" disabled="@Disabled" readonly="@ReadOnly"
class="@GetInputCssClass()" tabindex="@(Disabled ? "-1" : $"{TabIndex}")" id="@Name"
placeholder="@CurrentPlaceholder" autocomplete="@AutoCompleteAttribute" aria-autocomplete="@AriaAutoCompleteAttribute" @bind:get="FormattedValue" @bind:set="SetValue"
onkeypress="Radzen.numericKeyPress(event, @IsInteger().ToString().ToLower(), '@Culture.NumberFormat.NumberDecimalSeparator')"
onblur="@GetOnInput()" onpaste="@GetOnPaste()" maxlength="@MaxLength"
@onkeydown="@(args => OnKeyPress(args))" @onkeydown:preventDefault=preventKeyPress @onkeydown:stopPropagation />
@if (ShowUpDown)
{
<button aria-label=@UpAriaLabel type="button" class="rz-numeric-button rz-numeric-up rz-button" tabindex="-1"
@@ -21,32 +26,4 @@
</button>
}
</span>
}
@code {
internal RenderFragment RenderInput()
{
#if NET7_0_OR_GREATER
return __builder => {
<text>
<input @ref="@input" inputmode="decimal" @attributes="InputAttributes" type="text" name="@Name" disabled="@Disabled" readonly="@ReadOnly"
class="@GetInputCssClass()" tabindex="@(Disabled ? "-1" : $"{TabIndex}")" id="@Name"
placeholder="@CurrentPlaceholder" autocomplete="@AutoCompleteAttribute" aria-autocomplete="@AriaAutoCompleteAttribute" @bind:get="FormattedValue" @bind:set="SetValue"
onkeypress="Radzen.numericKeyPress(event, @IsInteger().ToString().ToLower(), '@Culture.NumberFormat.NumberDecimalSeparator')"
onblur="@GetOnInput()" onpaste="@GetOnPaste()" maxlength="@MaxLength"
@onkeydown="@(args => OnKeyPress(args))" @onkeydown:preventDefault=preventKeyPress @onkeydown:stopPropagation />
</text>
};
#else
return __builder => {
<text>
<input @ref="@input" inputmode="decimal" @attributes="InputAttributes" type="text" name="@Name" disabled="@Disabled" readonly="@ReadOnly"
class="@GetInputCssClass()" tabindex="@(Disabled ? "-1" : $"{TabIndex}")" id="@Name"
placeholder="@CurrentPlaceholder" autocomplete="@AutoCompleteAttribute" aria-autocomplete="@AriaAutoCompleteAttribute" value="@FormattedValue" @onchange="@OnChange"
onkeypress="Radzen.numericKeyPress(event, @IsInteger().ToString().ToLower(), '@Culture.NumberFormat.NumberDecimalSeparator')"
onblur="@GetOnInput()" onpaste="@GetOnPaste()" maxlength="@MaxLength"
@onkeydown="@(args => OnKeyPress(args))" @onkeydown:preventDefault=preventKeyPress @onkeydown:stopPropagation />
</text>
};
#endif
}
}

View File

@@ -118,7 +118,6 @@ namespace Radzen.Blazor
_ => false
};
#if NET7_0_OR_GREATER
private TNum SumFloating<TNum>(TNum value1, TNum value2)
{
ArgumentNullException.ThrowIfNull(value1);
@@ -174,7 +173,6 @@ namespace Radzen.Blazor
return newValue;
}
#endif
async System.Threading.Tasks.Task UpdateValueWithStep(bool stepUp)
{
@@ -186,7 +184,6 @@ namespace Radzen.Blazor
var step = string.IsNullOrEmpty(Step) || Step == "any" ? 1 : decimal.Parse(Step.Replace(",", ".", StringComparison.Ordinal), System.Globalization.CultureInfo.InvariantCulture);
TValue? newValue;
#if NET7_0_OR_GREATER
if (IsNumericType(Value))
{
// cannot call UpdateValueWithStepNumeric directly because TValue is not value type constrained
@@ -196,7 +193,6 @@ namespace Radzen.Blazor
newValue = dynamicWrapper(Value, stepUp, step);
}
else
#endif
{
var valueToUpdate = ConvertToDecimal(Value);

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

@@ -38,9 +38,7 @@ namespace Radzen.Blazor
/// &lt;/RadzenPivotDataGrid&gt;
/// </code>
/// </example>
#if NET6_0_OR_GREATER
[CascadingTypeParameter(nameof(TItem))]
#endif
public partial class RadzenPivotDataGrid<TItem> : PagedDataBoundComponent<TItem>
{
private class RowHeaderCell
@@ -190,6 +188,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 +1607,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>
{

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>

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