diff --git a/src/Components/Charts/Chart/Annotations/ChartAnnotations.cs b/src/Components/Charts/Chart/Annotations/ChartAnnotations.cs index 3a53ae1..4edb408 100644 --- a/src/Components/Charts/Chart/Annotations/ChartAnnotations.cs +++ b/src/Components/Charts/Chart/Annotations/ChartAnnotations.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using Microsoft.AspNetCore.Components; +using Syncfusion.Blazor.Toolkit.Charts.Internal; namespace Syncfusion.Blazor.Toolkit.Charts { @@ -38,9 +39,23 @@ public class ChartAnnotations : ChartSubComponent, ISubcomponentTracker /// /// /// Use this property to provide an accessibility role for the . + /// The value, if non-empty, must be a valid WAI-ARIA role (for example + /// "region", "status", "group"). Invalid values are + /// rejected at component initialization to prevent invalid role + /// attributes from reaching the DOM. /// [Parameter] - public string AccessibilityRole { get; set; } = string.Empty; + public string AccessibilityRole + { + get => _accessibilityRole; + set + { + DataVizCommonHelper.AriaRoleValidator.EnsureValidRole(value, nameof(AccessibilityRole)); + _accessibilityRole = value; + } + } + + private string _accessibilityRole = string.Empty; /// /// Gets or sets the accessibility keyboard navigation focus option for the . @@ -253,4 +268,4 @@ public override void AddRenderer(IChartElementRenderer renderer) } #endregion } -} \ No newline at end of file +} diff --git a/src/Components/Charts/Chart/ChartArea/ChartSubTitleStyle.cs b/src/Components/Charts/Chart/ChartArea/ChartSubTitleStyle.cs index cfa21dc..8270f57 100644 --- a/src/Components/Charts/Chart/ChartArea/ChartSubTitleStyle.cs +++ b/src/Components/Charts/Chart/ChartArea/ChartSubTitleStyle.cs @@ -115,6 +115,10 @@ public class ChartSubTitleStyle : ChartDefaultFont /// /// /// Use this property to provide an accessibility role for the chart subtitle. + /// The value, if non-empty, must be a valid WAI-ARIA role (for example + /// "heading", "region"). Invalid values are rejected at + /// component initialization to prevent invalid role attributes from + /// reaching the DOM. /// /// /// @@ -127,7 +131,17 @@ public class ChartSubTitleStyle : ChartDefaultFont /// /// [Parameter] - public string AccessibilityRole { get; set; } = string.Empty; + public string AccessibilityRole + { + get => _accessibilityRole; + set + { + DataVizCommonHelper.AriaRoleValidator.EnsureValidRole(value, nameof(AccessibilityRole)); + _accessibilityRole = value; + } + } + + private string _accessibilityRole = string.Empty; /// /// Gets or sets the accessibility keyboard navigation focus option for the chart subtitle. diff --git a/src/Components/Charts/Chart/ChartArea/ChartTitleStyle.cs b/src/Components/Charts/Chart/ChartArea/ChartTitleStyle.cs index 09ef7d2..cb8d809 100644 --- a/src/Components/Charts/Chart/ChartArea/ChartTitleStyle.cs +++ b/src/Components/Charts/Chart/ChartArea/ChartTitleStyle.cs @@ -207,6 +207,10 @@ public class ChartTitleStyle : ChartDefaultFont /// /// /// Use this property to provide an accessibility role for the chart title. + /// The value, if non-empty, must be a valid WAI-ARIA role (for example + /// "heading", "region"). Invalid values are rejected at + /// component initialization to prevent invalid role attributes from + /// reaching the DOM. /// /// /// @@ -219,7 +223,17 @@ public class ChartTitleStyle : ChartDefaultFont /// /// [Parameter] - public string AccessibilityRole { get; set; } = string.Empty; + public string AccessibilityRole + { + get => _accessibilityRole; + set + { + DataVizCommonHelper.AriaRoleValidator.EnsureValidRole(value, nameof(AccessibilityRole)); + _accessibilityRole = value; + } + } + + private string _accessibilityRole = string.Empty; /// /// Gets or sets the accessibility keyboard navigation focus option for the chart title. diff --git a/src/Components/Charts/Chart/Legend/LegendSettings.cs b/src/Components/Charts/Chart/Legend/LegendSettings.cs index f23bb00..ff38efa 100644 --- a/src/Components/Charts/Chart/Legend/LegendSettings.cs +++ b/src/Components/Charts/Chart/Legend/LegendSettings.cs @@ -1,6 +1,6 @@ -using Microsoft.AspNetCore.Components; +using System.ComponentModel; +using Microsoft.AspNetCore.Components; using Syncfusion.Blazor.Toolkit.Charts.Internal; -using System.ComponentModel; namespace Syncfusion.Blazor.Toolkit.Charts { @@ -250,6 +250,10 @@ public string Height /// /// /// Use this property to provide an accessibility role for the . + /// The value, if non-empty, must be a valid WAI-ARIA role (for example + /// "region", "group", "list"). Invalid values are + /// rejected at component initialization to prevent invalid role + /// attributes from reaching the DOM. /// /// /// @@ -265,7 +269,17 @@ public string Height /// /// [Parameter] - public string AccessibilityRole { get; set; } = string.Empty; + public string AccessibilityRole + { + get => _accessibilityRole; + set + { + DataVizCommonHelper.AriaRoleValidator.EnsureValidRole(value, nameof(AccessibilityRole)); + _accessibilityRole = value; + } + } + + private string _accessibilityRole = string.Empty; /// /// Gets or sets the accessibility keyboard navigation focus option for the . diff --git a/src/Components/Charts/Chart/Renderer/AxisRenderer/ChartAxisRenderer.cs b/src/Components/Charts/Chart/Renderer/AxisRenderer/ChartAxisRenderer.cs index 4ad9d55..5fa93c6 100644 --- a/src/Components/Charts/Chart/Renderer/AxisRenderer/ChartAxisRenderer.cs +++ b/src/Components/Charts/Chart/Renderer/AxisRenderer/ChartAxisRenderer.cs @@ -1820,7 +1820,7 @@ internal void CustomizeGridRenderingOptions(string key) foreach (PathOptions option in axisGridOptions) { option.Stroke = Axis?.MajorGridLines.Color ?? string.Empty; - option.StrokeWidth = Axis?.Renderer?.MajorGridLinesWidth ?? 0; + option.StrokeWidth = Axis?.MajorGridLines.Width ?? 0; option.StrokeDashArray = Axis?.MajorGridLines.DashArray ?? string.Empty; } } @@ -1829,7 +1829,7 @@ internal void CustomizeGridRenderingOptions(string key) foreach (CircleOptions option in AxisRenderInfo.MajorGridCircleOptions) { option.Stroke = Axis?.MajorGridLines.Color ?? string.Empty; - option.StrokeWidth = Axis?.Renderer?.MajorGridLinesWidth ?? 0; + option.StrokeWidth = Axis?.MajorGridLines.Width ?? 0; option.StrokeDashArray = Axis?.MajorGridLines.DashArray ?? string.Empty; } } diff --git a/src/Components/Charts/Chart/Series/ChartSeries.cs b/src/Components/Charts/Chart/Series/ChartSeries.cs index 8817721..7588549 100644 --- a/src/Components/Charts/Chart/Series/ChartSeries.cs +++ b/src/Components/Charts/Chart/Series/ChartSeries.cs @@ -1,11 +1,11 @@ -using Microsoft.AspNetCore.Components; -using Syncfusion.Blazor.Toolkit.Charts.Internal; -using Syncfusion.Blazor.Toolkit.Data; -using Syncfusion.Blazor.Toolkit.Internal; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Globalization; +using Microsoft.AspNetCore.Components; +using Syncfusion.Blazor.Toolkit.Charts.Internal; +using Syncfusion.Blazor.Toolkit.Data; +using Syncfusion.Blazor.Toolkit.Internal; namespace Syncfusion.Blazor.Toolkit.Charts { @@ -328,6 +328,10 @@ public double Width /// A string that defines the accessibility role for the . /// /// Use this property to specify the accessibility role for the root element. + /// The value, if non-empty, must be a valid WAI-ARIA role (for example + /// "region", "img", "list"). Invalid values are + /// rejected at component initialization to prevent invalid role + /// attributes from reaching the DOM. /// /// /// @@ -340,7 +344,17 @@ public double Width /// /// [Parameter] - public string AccessibilityRole { get; set; } = string.Empty; + public string AccessibilityRole + { + get => _accessibilityRole; + set + { + DataVizCommonHelper.AriaRoleValidator.EnsureValidRole(value, nameof(AccessibilityRole)); + _accessibilityRole = value; + } + } + + private string _accessibilityRole = string.Empty; /// /// Gets or sets a value indicating whether the series root can receive keyboard focus. diff --git a/src/Components/Charts/Chart/Series/Trendline/ChartTrendline.cs b/src/Components/Charts/Chart/Series/Trendline/ChartTrendline.cs index 8805657..d9ddcb4 100644 --- a/src/Components/Charts/Chart/Series/Trendline/ChartTrendline.cs +++ b/src/Components/Charts/Chart/Series/Trendline/ChartTrendline.cs @@ -258,7 +258,13 @@ public TrendlineTypes Type /// /// Gets or sets the ARIA role for the trendline. /// - /// A string representing the ARIA role. Default is empty. + /// A string representing the ARIA role. Default is empty. + /// + /// The value, if non-empty, must be a valid WAI-ARIA role (for example + /// "region", "group"). Invalid values are rejected at + /// component initialization to prevent invalid role attributes from + /// reaching the DOM. + /// /// /// /// /// [Parameter] - public string AccessibilityRole { get; set; } = string.Empty; + public string AccessibilityRole + { + get => _accessibilityRole; + set + { + DataVizCommonHelper.AriaRoleValidator.EnsureValidRole(value, nameof(AccessibilityRole)); + _accessibilityRole = value; + } + } + + private string _accessibilityRole = string.Empty; /// /// Gets or sets whether the trendline is focusable via keyboard navigation. diff --git a/src/Components/Charts/Chart/SfChart.razor.LifeCycle.cs b/src/Components/Charts/Chart/SfChart.razor.LifeCycle.cs index aa43653..4e47dd8 100644 --- a/src/Components/Charts/Chart/SfChart.razor.LifeCycle.cs +++ b/src/Components/Charts/Chart/SfChart.razor.LifeCycle.cs @@ -1,7 +1,7 @@ -using Microsoft.JSInterop; -using Syncfusion.Blazor.Toolkit.Charts.Internal; -using System.Collections.Specialized; +using System.Collections.Specialized; using System.ComponentModel; +using Microsoft.JSInterop; +using Syncfusion.Blazor.Toolkit.Charts.Internal; namespace Syncfusion.Blazor.Toolkit.Charts { @@ -304,6 +304,12 @@ protected override async ValueTask DisposeAsyncCore() _chartJsModule = null; _chartJsInProcessModule = null; } + + // Clear instance-level font measurement caches to prevent memory retention + // after the chart is disposed. This ensures per-circuit/per-chart isolation. + _fontSizeCache?.Clear(); + _requestedFontKeys?.Clear(); + await base.DisposeAsyncCore().ConfigureAwait(true); } diff --git a/src/Components/Charts/Chart/SfChart.razor.Members.cs b/src/Components/Charts/Chart/SfChart.razor.Members.cs index 6fdfc88..640d4d8 100644 --- a/src/Components/Charts/Chart/SfChart.razor.Members.cs +++ b/src/Components/Charts/Chart/SfChart.razor.Members.cs @@ -1,8 +1,8 @@ -using Microsoft.AspNetCore.Components; -using Syncfusion.Blazor.Toolkit.Internal; +using System.Collections.Specialized; using System.ComponentModel; +using Microsoft.AspNetCore.Components; using Syncfusion.Blazor.Toolkit.Charts.Internal; -using System.Collections.Specialized; +using Syncfusion.Blazor.Toolkit.Internal; namespace Syncfusion.Blazor.Toolkit.Charts { @@ -178,6 +178,10 @@ internal class ChartDataState /// /// /// Use this property to provide an accessibility role for the Chart component. + /// The value, if non-empty, must be a valid WAI-ARIA role (for example + /// "region", "figure", "img"). Invalid values are + /// rejected at component initialization to prevent invalid role + /// attributes from reaching the DOM. /// /// /// @@ -189,7 +193,17 @@ internal class ChartDataState /// /// [Parameter] - public string AccessibilityRole { get; set; } = string.Empty; + public string AccessibilityRole + { + get => _accessibilityRole; + set + { + DataVizCommonHelper.AriaRoleValidator.EnsureValidRole(value, nameof(AccessibilityRole)); + _accessibilityRole = value; + } + } + + private string _accessibilityRole = string.Empty; /// /// Gets or sets the accessibility keyboard navigation focus option for the Chart component. diff --git a/src/Components/Charts/Chart/SfChart.razor.cs b/src/Components/Charts/Chart/SfChart.razor.cs index 022984c..e085022 100644 --- a/src/Components/Charts/Chart/SfChart.razor.cs +++ b/src/Components/Charts/Chart/SfChart.razor.cs @@ -1,15 +1,16 @@ -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Rendering; -using Microsoft.JSInterop; -using Syncfusion.Blazor.Toolkit.Charts.Internal; +using System.Collections.Concurrent; +using System.ComponentModel; using System.Globalization; +using System.Reflection; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; -using System.Reflection; -using System.ComponentModel; -using Syncfusion.Blazor.Toolkit.Data; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; using Microsoft.Extensions.Localization; -using System.Runtime.CompilerServices; +using Microsoft.JSInterop; +using Syncfusion.Blazor.Toolkit.Charts.Internal; +using Syncfusion.Blazor.Toolkit.Data; [assembly: InternalsVisibleTo("Syncfusion.Blazor.Toolkit.BUnitTest")] namespace Syncfusion.Blazor.Toolkit.Charts @@ -178,6 +179,9 @@ internal class JsInteropState internal List _seriesBorders = []; internal List _axes = []; internal List _highLightPatternCollection = []; + // Instance-level font measurement caches + internal ConcurrentDictionary _fontSizeCache = new(); + internal ConcurrentDictionary _requestedFontKeys = new(); internal ChartAnnotations _annotations = new(); internal DomRect _elementOffset = new(); /*To store the SVGElement's dimensions value.*/ @@ -1244,9 +1248,9 @@ private async Task GetOtherLanguageCharSizeAsync(bool _updateDataSource) /// The text to analyze. /// The font options for the text. /// The collection to populate with distinct character keys. - private static void GetDistinctCharacter(string text, ChartFontOptions font, List distinctKeys) + private void GetDistinctCharacter(string text, ChartFontOptions font, List distinctKeys) { - ChartHelper.GetDistinctCharacter(text, font, distinctKeys); + ChartHelper.GetDistinctCharacter(text, font, distinctKeys, this); } /// @@ -1271,7 +1275,7 @@ private async Task LoadCharacterDictionaryAsync(List distinctKeys) Dictionary charSizeList = JsonSerializer.Deserialize>(result) ?? null!; foreach (KeyValuePair charSize in charSizeList) { - _ = ChartHelper.SizePerCharacter.TryAdd(charSize.Key, new Size { Width = charSize.Value.X, Height = charSize.Value.Y }); + _ = _fontSizeCache.TryAdd(charSize.Key, new Size { Width = charSize.Value.X, Height = charSize.Value.Y }); } } @@ -1832,10 +1836,9 @@ internal async Task GetCharSizeListAsync(List fontKeys) List uniqueKeys = []; foreach (string fontKey in fontKeys) { - if (!ChartHelper.ChartFontKeys.Contains(fontKey)) + if (_requestedFontKeys.TryAdd(fontKey, 0)) { uniqueKeys.Add(fontKey); - ChartHelper.ChartFontKeys.Add(fontKey); } } @@ -1855,7 +1858,7 @@ internal async Task GetCharSizeListAsync(List fontKeys) int i = 33, j = 0; foreach (string width in result_2) { - _ = ChartHelper.SizePerCharacter.TryAdd(Convert.ToChar(i) + Constants.Underscore + uniqueKeys[j], new Size { Width = Convert.ToInt16(width, null), Height = 133 }); + _ = _fontSizeCache.TryAdd(Convert.ToChar(i) + Constants.Underscore + uniqueKeys[j], new Size { Width = Convert.ToInt16(width, null), Height = 133 }); i++; if (i > 590) { @@ -2590,17 +2593,6 @@ protected override void OnParametersSet() PushSubcomponent(); } - /// - /// Performs cleanup operations when the component is being disposed. - /// - /// - [EditorBrowsable(EditorBrowsableState.Never)] - [Browsable(false)] - protected override ValueTask DisposeAsyncCore() - { - return base.DisposeAsyncCore(); - } - #endregion #region Protected Methods diff --git a/src/Components/Charts/Common/ChartUtils/ChartHelper.cs b/src/Components/Charts/Common/ChartUtils/ChartHelper.cs index 76d12ef..fd27661 100644 --- a/src/Components/Charts/Common/ChartUtils/ChartHelper.cs +++ b/src/Components/Charts/Common/ChartUtils/ChartHelper.cs @@ -1,13 +1,12 @@ +using System.Collections.ObjectModel; +using System.Drawing; using System.Dynamic; +using System.Globalization; using System.Text.Json; using System.Text.RegularExpressions; -using Microsoft.AspNetCore.Components.Rendering; -using System.Drawing; -using System.Globalization; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; using Syncfusion.Blazor.Toolkit.Internal; -using System.Collections.ObjectModel; -using System.Collections.Concurrent; namespace Syncfusion.Blazor.Toolkit.Charts.Internal { @@ -28,34 +27,6 @@ public class ChartHelper #region Properties - /// - /// Gets a process-wide, thread-safe cache of measured character sizes indexed by font characteristics. - /// - /// - /// - /// Glyph metrics for a given (character, FontWeight, FontStyle, FontFamily) tuple are deterministic - /// for the lifetime of the process (the JavaScript side measures the actual rendered glyph), so the cache - /// is shared across all circuits and chart instances on this process to avoid redundant JS interop. - /// - /// - /// Do NOT clear this cache on chart disposal: clearing on one chart's teardown would invalidate entries - /// still in use by every other live chart on the process, forcing them to re-measure. The cache is - /// intrinsically bounded by the set of characters × font weights × font styles × font families - /// the process ever renders, which is small in practice. - /// - /// - internal static ConcurrentDictionary SizePerCharacter { get; } = new ConcurrentDictionary(); - - /// - /// Gets the set of font keys for which character sizes have already been requested from JavaScript. - /// - /// - /// Mirrors keys minus the character prefix; used to short-circuit - /// duplicate JS-interop requests on the same circuit. See the remarks on - /// for why this is process-wide and not cleared per-chart. - /// - internal static List ChartFontKeys { get; } = new List(); - /// /// Gets a read-only lookup table mapping characters to their approximate pixel widths. /// @@ -118,6 +89,33 @@ private static Size MeasureBreakText(string originalText, ChartFontOptions font) return new Size(width, height); } + /// + /// Measures multi-line text (split by <br>) using a chart instance's cache to avoid process-wide memory leak. + /// + /// The text that may contain line breaks. + /// The font settings used during measurement. + /// The chart instance owning the cache. + /// The measured size encompassing all lines. + private static Size MeasureBreakText(string originalText, ChartFontOptions font, object chart) + { + originalText = originalText.Replace("
", "
", StringComparison.InvariantCulture); + List textCollection = originalText.Split("
").ToList(); + double width = 0; + double height = 0; + + foreach (string text in textCollection) + { + Size size = MeasureText(text, font, chart); + if (size is not null) + { + width = Math.Max(width, size.Width); + height += size.Height; + } + } + + return new Size(width, height); + } + /// /// Retrieves the cached character size for a font and caches the value if missing. /// @@ -126,27 +124,55 @@ private static Size MeasureBreakText(string originalText, ChartFontOptions font) /// The measured character size. private static Size GetCharSize(char character, ChartFontOptions font) { + if (FontWidthLookup.TryGetValue(character, out double charWidth)) + { + return new Size { Width = charWidth * 6.25, Height = 130 }; + } + + return new Size { Width = 50, Height = 130 }; + } + + /// + /// Retrieves the cached character size for an SfChart caller, or computes the fallback value for non-chart callers. + /// + /// + /// This method uses instance-level caching (per SfChart) to keep font measurements isolated to the chart + /// lifetime and avoid unbounded process-wide memory retention. Non-chart callers use the same approximate + /// dimensions without retaining them in a shared cache. + /// + /// + /// Sharing measurements across charts could reduce repeated work, but a bounded shared cache would be a + /// separate optimization with explicit capacity and eviction behavior. + /// + /// The chart instance owning the cache. + /// The character to measure. + /// The font settings used during measurement. + /// The measured character size. + private static Size GetCharSize(object chart, char character, ChartFontOptions font) + { + var sfChart = chart as Charts.SfChart; + if (sfChart is null) + return GetCharSize(character, font); + + var fontCache = sfChart._fontSizeCache; string key = character + Constants.Underscore + font.FontWeight + Constants.Underscore + font.FontStyle + Constants.Underscore + font.FontFamily; try { - if (SizePerCharacter.TryGetValue(key, out Size? charSize)) + if (fontCache.TryGetValue(key, out Size? charSize)) { return charSize ?? null!; } double charWidth; if (FontWidthLookup.TryGetValue(character, out charWidth)) { - // Create the new size for this character Size newSize = new Size { Width = charWidth * 6.25, Height = 130 }; - // Thread-safe add operation using GetOrAdd (available in all .NET versions) - Size result = SizePerCharacter.GetOrAdd(key, newSize); + Size result = fontCache.GetOrAdd(key, newSize); return result ?? null!; } else { - // Default size for characters not in Font dictionary Size defaultSize = new Size { Width = 50, Height = 130 }; - Size result = SizePerCharacter.GetOrAdd(key, defaultSize); + Size result = fontCache.GetOrAdd(key, defaultSize); return result ?? null!; } } @@ -1163,10 +1189,44 @@ internal static Size MeasureText(string text, ChartFontOptions font) double width = 0, height = 0, fontSize = PixelToNumber(font.Size); Size charSize; + for (int i = 0; i < text.Length; i++) + { + charSize = GetCharSize(text[i], font); + if (charSize is not null) + { + width += charSize.Width > 0 ? charSize.Width : 100; + height = Math.Max(charSize.Height, height); + } + } + + return new Size((width * fontSize) / 100, (height * fontSize) / 100); + } + + /// + /// Measures text using a chart instance's font cache to avoid process-wide memory accumulation. + /// + /// + /// This overload uses the chart's instance-level _fontSizeCache, ensuring per-circuit/per-chart memory isolation. + /// + /// The text to measure. + /// The font options for the text. + /// The chart instance owning the cache. + /// The measured text size. + internal static Size MeasureText(string text, ChartFontOptions font, object chart) + { + if (text.Contains("
", StringComparison.InvariantCulture) || text.Contains("
", StringComparison.InvariantCulture)) + { + return MeasureBreakText(text, font, chart); + } + + double width = 0, height = 0, fontSize = PixelToNumber(font.Size); + Size charSize; + if (IsRTLText(text)) { string key = text + Constants.Underscore + font.FontWeight + Constants.Underscore + font.FontStyle + Constants.Underscore + font.FontFamily; - if (SizePerCharacter.TryGetValue(key, out Size? value)) + var sfChart = chart as Charts.SfChart; + if (sfChart is not null && sfChart._fontSizeCache.TryGetValue(key, out Size? value)) { charSize = value; return new Size(charSize.Width * (fontSize / 100), charSize.Height * (fontSize / 100)); @@ -1175,7 +1235,7 @@ internal static Size MeasureText(string text, ChartFontOptions font) for (int i = 0; i < text.Length; i++) { - charSize = GetCharSize(text[i], font); + charSize = GetCharSize(chart, text[i], font); if (charSize is not null) { width += charSize.Width > 0 ? charSize.Width : 100; @@ -2358,7 +2418,8 @@ internal static List GetDistinctList(List xValuesList) /// The text to analyze. /// The font options. /// The output list of distinct keys. - internal static void GetDistinctCharacter(string text, ChartFontOptions font, List distinctKeys) + /// The chart instance that owns the font caches. + internal static void GetDistinctCharacter(string text, ChartFontOptions font, List distinctKeys, SfChart chart) { if (!string.IsNullOrEmpty(text)) { @@ -2366,20 +2427,18 @@ internal static void GetDistinctCharacter(string text, ChartFontOptions font, Li if (IsRTLText(text)) { key = text + Constants.Underscore + font.FontWeight + Constants.Underscore + font.FontStyle + Constants.Underscore + font.FontFamily; - if (!SizePerCharacter.ContainsKey(key) && !ChartFontKeys.Contains(key)) + if (!chart._fontSizeCache.ContainsKey(key) && chart._requestedFontKeys.TryAdd(key, 0)) { distinctKeys.Add(key); - ChartFontKeys.Add(key); } else { foreach (char character in text) { key = character + Constants.Underscore + font.FontWeight + Constants.Underscore + font.FontStyle + Constants.Underscore + font.FontFamily; - if (!SizePerCharacter.ContainsKey(key) && !ChartFontKeys.Contains(key)) + if (!chart._fontSizeCache.ContainsKey(key) && chart._requestedFontKeys.TryAdd(key, 0)) { distinctKeys.Add(key); - ChartFontKeys.Add(key); } } } @@ -2387,23 +2446,6 @@ internal static void GetDistinctCharacter(string text, ChartFontOptions font, Li } } - /// - /// Clears static font measurement caches. - /// - /// - /// Previously called from SfChart.UnWireEventsAsync on chart teardown. That call was removed: - /// clearing a process-wide cache from a single chart's disposal evicted entries still in use by every - /// other live chart on the process, forcing them to re-measure via JS interop. The cache is now left - /// in place for the lifetime of the process and is bounded by the (character × font) tuple space. - /// - [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] - [System.ComponentModel.Browsable(false)] - internal static void ClearStaticStorage() - { - SizePerCharacter.Clear(); - ChartFontKeys.Clear(); - } - #endregion } } diff --git a/src/Components/Charts/Common/SvgComponents/SvgPath.razor b/src/Components/Charts/Common/SvgComponents/SvgPath.razor index 5b04400..5baec02 100644 --- a/src/Components/Charts/Common/SvgComponents/SvgPath.razor +++ b/src/Components/Charts/Common/SvgComponents/SvgPath.razor @@ -2,4 +2,4 @@ @using System.ComponentModel; @inherits SvgClass - \ No newline at end of file + diff --git a/src/Components/Charts/Common/SvgComponents/SvgPath.razor.cs b/src/Components/Charts/Common/SvgComponents/SvgPath.razor.cs index a127d3d..6cf8979 100644 --- a/src/Components/Charts/Common/SvgComponents/SvgPath.razor.cs +++ b/src/Components/Charts/Common/SvgComponents/SvgPath.razor.cs @@ -125,10 +125,30 @@ public partial class SvgPath [Parameter] public string Title { get; set; } = "Path Element"; + /// + /// Gets the effective tabindex value to emit on the element. When the path has no + /// accessible name ( is empty), the tabindex attribute + /// is suppressed so the node is not pulled into the keyboard tab order with no + /// accessible name. + /// + private string EffectiveTabIndex => + string.IsNullOrEmpty(AccessibilityText) ? string.Empty : TabIndex; + + /// + /// Gets the effective ARIA role to emit on the element. When the path has no + /// accessible name ( is empty), the role="img" + /// attribute is suppressed — advertising an image role on a node that has no + /// accessible name trips the axe aria-allowed-attr rule and creates an + /// unlabeled-image finding for screen-reader users. + /// + private string EffectiveRole => + string.IsNullOrEmpty(AccessibilityText) ? string.Empty : "img"; + + #endregion #region Fields private CultureInfo _culture { get; set; } = CultureInfo.InvariantCulture; #endregion } -} \ No newline at end of file +} diff --git a/src/Components/Charts/Common/SvgComponents/SvgRect.razor b/src/Components/Charts/Common/SvgComponents/SvgRect.razor index d31ef11..81a65fa 100644 --- a/src/Components/Charts/Common/SvgComponents/SvgRect.razor +++ b/src/Components/Charts/Common/SvgComponents/SvgRect.razor @@ -1,4 +1,4 @@ @namespace Syncfusion.Blazor.Toolkit.Charts.Internal @inherits SvgClass - \ No newline at end of file + diff --git a/src/Components/Charts/Common/SvgComponents/SvgRect.razor.cs b/src/Components/Charts/Common/SvgComponents/SvgRect.razor.cs index 9cee7e7..86e343d 100644 --- a/src/Components/Charts/Common/SvgComponents/SvgRect.razor.cs +++ b/src/Components/Charts/Common/SvgComponents/SvgRect.razor.cs @@ -1,5 +1,5 @@ -using Microsoft.AspNetCore.Components; -using System.Globalization; +using System.Globalization; +using Microsoft.AspNetCore.Components; namespace Syncfusion.Blazor.Toolkit.Charts.Internal { @@ -155,6 +155,23 @@ public partial class SvgRect /// [Parameter] public string AccessibilityText { get; set; } = string.Empty; + + /// + /// Gets the effective tabindex value to emit on the element. When the rectangle is hidden + /// from assistive technology ( == "true"), the attribute is + /// suppressed so the node is not pulled into the keyboard tab order with no accessible name. + /// + private string EffectiveTabIndex => + string.Equals(AriaHidden, "true", System.StringComparison.OrdinalIgnoreCase) ? string.Empty : TabIndex; + + /// + /// Gets the effective ARIA role to emit on the element. When the rectangle is hidden + /// from assistive technology ( == "true"), the role="img" + /// attribute is suppressed — advertising an image role on a node that is hidden from AT + /// is contradictory and trips the axe aria-allowed-attr rule. + /// + private string EffectiveRole => + string.Equals(AriaHidden, "true", System.StringComparison.OrdinalIgnoreCase) ? string.Empty : "img"; #endregion } -} \ No newline at end of file +} diff --git a/src/Components/Charts/Common/Utils/Helper.cs b/src/Components/Charts/Common/Utils/Helper.cs index d68cc79..1d1469a 100644 --- a/src/Components/Charts/Common/Utils/Helper.cs +++ b/src/Components/Charts/Common/Utils/Helper.cs @@ -1,4 +1,6 @@ -using System.Dynamic; +using System; +using System.Collections.Generic; +using System.Dynamic; using System.Globalization; using System.Text.Json; @@ -75,5 +77,90 @@ public static double StringToNumber(string size, double containerSize) : double.NaN; } #endregion + + #region Nested Validator + + /// + /// Validates a string value against the WAI-ARIA 1.2 abstract role list. + /// + /// + /// + /// The Chart component family exposes an AccessibilityRole string parameter + /// on six different types (, + /// , + /// , + /// , + /// , + /// , + /// ). The role is + /// forwarded verbatim into the DOM role attribute, so an unknown + /// value (for example "count") becomes an invalid ARIA role at + /// runtime. This validator runs at the parameter setter and rejects + /// unknown non-empty values with so the + /// bug is caught at component initialization rather than at audit time. + /// + /// + /// Empty / null values are always considered valid — the component's + /// own renderers fall back to "region" when the role is unset, + /// and existing valid values (region, status, group, img, heading, + /// button, link, list, listitem, navigation, presentation, none, etc.) + /// are unchanged. + /// + /// + internal static class AriaRoleValidator + { + // WAI-ARIA 1.2 abstract roles, compared case-insensitively. + // Sourced from + // https://www.w3.org/TR/wai-aria-1.2/#role_definitions — the same + // set used by the audit's Full Assessment spec. + private static readonly HashSet ValidRoles = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "alert", "alertdialog", "application", "article", "banner", + "button", "cell", "checkbox", "columnheader", "combobox", + "complementary", "contentinfo", "definition", "dialog", + "directory", "document", "feed", "figure", "form", "grid", + "gridcell", "group", "heading", "img", "input", "link", "list", + "listbox", "listitem", "log", "main", "marquee", "math", "menu", + "menubar", "menuitem", "menuitemcheckbox", "menuitemradio", + "navigation", "none", "note", "option", "presentation", + "progressbar", "radio", "radiogroup", "region", "row", "rowgroup", + "rowheader", "scrollbar", "search", "searchbox", "separator", + "slider", "spinbutton", "status", "switch", "tab", "table", + "tablist", "tabpanel", "term", "textbox", "timer", "toolbar", + "tooltip", "tree", "treegrid", "treeitem" + }; + + /// + /// + /// Ensures the supplied role string is a valid WAI-ARIA abstract role. + /// + /// + /// The role string supplied by the consumer. May be null or empty. + /// The parameter name to surface in any thrown exception. + /// + /// Thrown when is non-empty and is not a recognized + /// WAI-ARIA role. Empty / null values are accepted unchanged. + /// + internal static void EnsureValidRole(string? value, string paramName) + { + if (string.IsNullOrEmpty(value)) + { + return; + } + + if (!ValidRoles.Contains(value)) + { + throw new ArgumentException( + $"'{value}' is not a valid WAI-ARIA role for {paramName}. " + + "Use one of the roles listed at " + + "https://www.w3.org/TR/wai-aria-1.2/#role_definitions " + + "(for example: region, status, group, img, heading, button, link, " + + "list, listitem, navigation, presentation, none). " + + "Leave the property empty to use the component default.", + paramName); + } + } + } + #endregion } -} \ No newline at end of file +} diff --git a/tests/Syncfusion.Blazor.Playwright.Test/Blazor.Toolkit.Playwright.Samples/Components/Pages/Charts/Others/Annotation.razor b/tests/Syncfusion.Blazor.Playwright.Test/Blazor.Toolkit.Playwright.Samples/Components/Pages/Charts/Others/Annotation.razor index 23f95d5..1d470e5 100644 --- a/tests/Syncfusion.Blazor.Playwright.Test/Blazor.Toolkit.Playwright.Samples/Components/Pages/Charts/Others/Annotation.razor +++ b/tests/Syncfusion.Blazor.Playwright.Test/Blazor.Toolkit.Playwright.Samples/Components/Pages/Charts/Others/Annotation.razor @@ -7,7 +7,7 @@

Checking Chart Annotation

- +
Highest Medal Count
@@ -71,4 +71,4 @@ font-size: 18px; font-weight: 600; } - \ No newline at end of file + diff --git a/tests/Syncfusion.Blazor.Playwright.Test/Blazor.Toolkit.Playwright.Samples/Components/Pages/Charts/Others/ChartBasics.razor b/tests/Syncfusion.Blazor.Playwright.Test/Blazor.Toolkit.Playwright.Samples/Components/Pages/Charts/Others/ChartBasics.razor index dcd9032..8a19dda 100644 --- a/tests/Syncfusion.Blazor.Playwright.Test/Blazor.Toolkit.Playwright.Samples/Components/Pages/Charts/Others/ChartBasics.razor +++ b/tests/Syncfusion.Blazor.Playwright.Test/Blazor.Toolkit.Playwright.Samples/Components/Pages/Charts/Others/ChartBasics.razor @@ -20,7 +20,7 @@

Checking Title Rendering

- + diff --git a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Charts/Chart/Annotation/Annotation.razor b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Charts/Chart/Annotation/Annotation.razor index 6398008..f2f3e04 100644 --- a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Charts/Chart/Annotation/Annotation.razor +++ b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Charts/Chart/Annotation/Annotation.razor @@ -15,7 +15,7 @@ - +
Highest Medal Count
@@ -45,7 +45,7 @@ Assert.True(annotation.Instance.Description == "chart annotation"); Assert.True(annotation.Instance.Region == Regions.Series); Assert.Equal("Medal count", annotations.Instance.AccessibilityDescription); - Assert.Equal("count", annotations.Instance.AccessibilityRole); + Assert.Equal("status", annotations.Instance.AccessibilityRole); Assert.False(annotations.Instance.Focusable); cut.SetParametersAndRender(); await annotation.Instance.DisposeAsync(); @@ -104,4 +104,4 @@ Assert.Equal("100", annotation.Y); Assert.NotNull(cut.Find("#container_Annotation_0")); } -} \ No newline at end of file +} diff --git a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Charts/Chart/Axis/ChartBasic.razor b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Charts/Chart/Axis/ChartBasic.razor index 60e7c05..bdc6aea 100644 --- a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Charts/Chart/Axis/ChartBasic.razor +++ b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Charts/Chart/Axis/ChartBasic.razor @@ -74,7 +74,7 @@ - +