From 7e341f609147e8dd16b7dfb2da8818ff621e7075 Mon Sep 17 00:00:00 2001 From: YokeshSF4393 Date: Mon, 24 Aug 2026 20:45:03 +0530 Subject: [PATCH 1/5] 1047330: Fixed chart accessibility, b-unit, leak issues. --- .../Chart/Annotations/ChartAnnotations.cs | 17 +- .../Chart/ChartArea/ChartSubTitleStyle.cs | 16 +- .../Charts/Chart/ChartArea/ChartTitleStyle.cs | 16 +- .../Charts/Chart/Legend/LegendSettings.cs | 20 ++- .../AxisRenderer/ChartAxisRenderer.cs | 4 +- .../Charts/Chart/Series/ChartSeries.cs | 24 ++- .../Chart/Series/Trendline/ChartTrendline.cs | 20 ++- .../Charts/Chart/SfChart.razor.LifeCycle.cs | 12 +- .../Charts/Chart/SfChart.razor.Members.cs | 22 ++- src/Components/Charts/Chart/SfChart.razor.cs | 40 ++--- .../Charts/Common/ChartUtils/ChartHelper.cs | 163 ++++++++++++++++-- .../Charts/Common/SvgComponents/SvgPath.razor | 2 +- .../Common/SvgComponents/SvgPath.razor.cs | 20 +++ .../Charts/Common/SvgComponents/SvgRect.razor | 2 +- .../Common/SvgComponents/SvgRect.razor.cs | 21 ++- src/Components/Charts/Common/Utils/Helper.cs | 89 +++++++++- src/wwwroot/scripts/chart.js | 93 ++++++++++ .../Pages/Charts/Others/Annotation.razor | 2 +- .../Pages/Charts/Others/ChartBasics.razor | 2 +- .../Charts/Chart/Annotation/Annotation.razor | 4 +- .../Charts/Chart/Axis/ChartBasic.razor | 2 +- 21 files changed, 517 insertions(+), 74 deletions(-) diff --git a/src/Components/Charts/Chart/Annotations/ChartAnnotations.cs b/src/Components/Charts/Chart/Annotations/ChartAnnotations.cs index 3a53ae1..f482f23 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 . 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..22cf866 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,10 @@ internal class JsInteropState internal List _seriesBorders = []; internal List _axes = []; internal List _highLightPatternCollection = []; + // Instance-level font measurement caches (replaces process-wide static SizePerCharacter and ChartFontKeys) + // See: ChartHelper.cs remarks for why this was moved from static to instance + internal ConcurrentDictionary _fontSizeCache = new(); + internal List _requestedFontKeys = new(); internal ChartAnnotations _annotations = new(); internal DomRect _elementOffset = new(); /*To store the SVGElement's dimensions value.*/ @@ -1271,7 +1276,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 +1837,10 @@ internal async Task GetCharSizeListAsync(List fontKeys) List uniqueKeys = []; foreach (string fontKey in fontKeys) { - if (!ChartHelper.ChartFontKeys.Contains(fontKey)) + if (!_requestedFontKeys.Contains(fontKey)) { uniqueKeys.Add(fontKey); - ChartHelper.ChartFontKeys.Add(fontKey); + _requestedFontKeys.Add(fontKey); } } @@ -1855,7 +1860,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 +2595,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..62d23a8 100644 --- a/src/Components/Charts/Common/ChartUtils/ChartHelper.cs +++ b/src/Components/Charts/Common/ChartUtils/ChartHelper.cs @@ -1,13 +1,13 @@ +using System.Collections.Concurrent; +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 { @@ -33,27 +33,28 @@ public class ChartHelper /// /// /// - /// 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. + /// DEPRECATED: This static cache causes unbounded memory growth on long-lived Blazor Server hosts. + /// New code should use (per-chart instance) and the overloaded + /// method that accepts an SfChart parameter. /// /// - /// 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. + /// For backward compatibility, this cache is retained but not used by new SfChart rendering paths. + /// It will be removed in a future major version. /// /// + [System.Obsolete("Use SfChart._fontSizeCache and MeasureText(string, ChartFontOptions, object) overload instead. This static cache causes memory leaks on long-lived Blazor Server hosts.")] 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. + /// + /// DEPRECATED: Use instead. This static cache causes + /// unbounded memory growth on long-lived Blazor Server hosts and is not cleared per-circuit. + /// /// + /// [System.Obsolete("Use SfChart._requestedFontKeys instead. This static cache causes memory leaks on long-lived Blazor Server hosts.")] internal static List ChartFontKeys { get; } = new List(); /// @@ -118,6 +119,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. /// @@ -156,6 +184,51 @@ private static Size GetCharSize(char character, ChartFontOptions font) } } + /// + /// Retrieves the cached character size from a chart-instance cache, or computes and caches the value if missing. + /// + /// + /// This method uses instance-level caching (per SfChart) to avoid the process-wide memory leak of the static version. + /// The chart's own _fontSizeCache is used instead of the deprecated . + /// + /// 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); // Fallback to process-wide cache if chart not available + + var fontCache = sfChart._fontSizeCache; + string key = character + Constants.Underscore + font.FontWeight + Constants.Underscore + font.FontStyle + Constants.Underscore + font.FontFamily; + try + { + if (fontCache.TryGetValue(key, out Size? charSize)) + { + return charSize ?? null!; + } + double charWidth; + if (FontWidthLookup.TryGetValue(character, out charWidth)) + { + Size newSize = new Size { Width = charWidth * 6.25, Height = 130 }; + Size result = fontCache.GetOrAdd(key, newSize); + return result ?? null!; + } + else + { + Size defaultSize = new Size { Width = 50, Height = 130 }; + Size result = fontCache.GetOrAdd(key, defaultSize); + return result ?? null!; + } + } + catch + { + throw; + } + } + /// /// Converts a CSS size string (px, rem, em, pt, %) to a numeric pixel value. /// @@ -1186,6 +1259,51 @@ internal static Size MeasureText(string text, ChartFontOptions font) 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 instead of the deprecated + /// static , 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; + 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)); + } + } + + for (int i = 0; i < text.Length; i++) + { + charSize = GetCharSize(chart, 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); + } + /// /// Determines whether the given text contains Right-to-Left (RTL) characters. /// @@ -2391,13 +2509,20 @@ 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. + /// /// + /// DEPRECATED: This method is no longer called by SfChart. New code should rely on + /// which clears the instance-level + /// and . + /// + /// + /// Historically, this method could not be called during chart disposal because clearing a process-wide cache + /// from a single chart's teardown evicted entries still in use by every other live chart on the process, + /// forcing them to re-measure via JS interop. The static caches are now deprecated in favor of per-instance caches. + /// /// [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] [System.ComponentModel.Browsable(false)] + [System.Obsolete("No longer called. Use SfChart instance-level caches via DisposeAsyncCore instead.")] internal static void ClearStaticStorage() { SizePerCharacter.Clear(); diff --git a/src/Components/Charts/Common/SvgComponents/SvgPath.razor b/src/Components/Charts/Common/SvgComponents/SvgPath.razor index 5b04400..c30de3b 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 + \ 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..076cc00 100644 --- a/src/Components/Charts/Common/SvgComponents/SvgPath.razor.cs +++ b/src/Components/Charts/Common/SvgComponents/SvgPath.razor.cs @@ -125,6 +125,26 @@ 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 diff --git a/src/Components/Charts/Common/SvgComponents/SvgRect.razor b/src/Components/Charts/Common/SvgComponents/SvgRect.razor index d31ef11..f6400f3 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 + \ 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..31d2e76 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..7dd674d 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/src/wwwroot/scripts/chart.js b/src/wwwroot/scripts/chart.js index ec8f440..979d353 100644 --- a/src/wwwroot/scripts/chart.js +++ b/src/wwwroot/scripts/chart.js @@ -1849,6 +1849,12 @@ export function initialize(_dataId, element, dotnetRef, isZooming, isScrollbar, ejSVGTooltipElements.forEach((ejSVGTooltip) => { sfBlazorToolkit.base.setStyleAttribute(ejSVGTooltip, { position: 'absolute', zIndex: '1', pointerEvents: 'none' }); }); + // A11Y-004: mark every tooltip placeholder in this chart as a polite ARIA + // live region. This handles pages where the tooltip is never actually + // rendered (no hoverable data points) but the placeholder container still + // exists in the DOM. Render-time inserts are caught by the document-wide + // MutationObserver that applyTooltipLiveRegion installs. + applyTooltipLiveRegion(element.id); } instance.render(); } @@ -4532,6 +4538,85 @@ export function findColor(data, series) { return (data.point.i !== '' ? data.point.i : (series.marker.f !== '' ? series.marker.f : series.interior)); } +// A11Y-004: marks every tooltip element under the chart root as a polite ARIA live +// region so screen readers (NVDA, JAWS, VoiceOver) announce the tooltip text +// when it appears on hover/focus, satisfying WCAG 2.1 SC 4.1.3 (Status Messages). +// The attributes are idempotent — repeated calls are safe. +// +// Implementation: a single MutationObserver attached to `document.body` that +// catches any inserted `.e-tooltip` element anywhere in the document. This +// covers every chart on the page (single or multi-chart demos), template +// tooltips, and any future svgbase re-renders. Plus a one-shot tagged +// immediate run so the first tooltip is marked without waiting for the +// observer to fire. +const _tooltipLiveApplied = new WeakSet(); +let _tooltipLiveObserver = null; +function _markTooltipLiveRegion(node) { + if (!node || node.nodeType !== 1 || _tooltipLiveApplied.has(node)) { + return; + } + node.setAttribute('role', 'status'); + node.setAttribute('aria-live', 'polite'); + node.setAttribute('aria-atomic', 'true'); + _tooltipLiveApplied.add(node); +} +function _scanDocumentForTooltips(root) { + if (!root || !root.querySelectorAll) { + return; + } + // Two selectors: the fully rendered tooltip (`.e-tooltip` added by + // svgbase.Tooltip after appendTo) and the chart's own tooltip placeholder + // container (`.ejSVGTooltip` added when the chart initialises). + const found = root.querySelectorAll('.e-tooltip, .ejSVGTooltip'); + for (let i = 0; i < found.length; i++) { + _markTooltipLiveRegion(found[i]); + } +} +function _ensureTooltipLiveObserver() { + if (_tooltipLiveObserver) { + return; + } + _tooltipLiveObserver = new MutationObserver(function (mutations) { + for (let i = 0; i < mutations.length; i++) { + const added = mutations[i].addedNodes; + for (let j = 0; j < added.length; j++) { + const node = added[j]; + if (node.nodeType !== 1) { + continue; + } + if (node.classList && node.classList.contains('e-tooltip')) { + _markTooltipLiveRegion(node); + } + if (node.querySelectorAll) { + const descendants = node.querySelectorAll('.e-tooltip, .ejSVGTooltip'); + for (let k = 0; k < descendants.length; k++) { + _markTooltipLiveRegion(descendants[k]); + } + } + } + } + }); + _tooltipLiveObserver.observe(document.body, { childList: true, subtree: true }); +} +function applyTooltipLiveRegion(tooltipHostId) { + // Run both an immediate scan (in case the tooltip is already in the DOM) + // and set up the document-wide observer to catch future insertions. + setTimeout(function () { + const tooltipHost = document.getElementById(tooltipHostId); + if (tooltipHost) { + if (tooltipHost.classList && tooltipHost.classList.contains('e-tooltip')) { + _markTooltipLiveRegion(tooltipHost); + } + const chartContainer = tooltipHost.parentElement; + if (chartContainer) { + _scanDocumentForTooltips(chartContainer); + } + } + _scanDocumentForTooltips(document.body); + _ensureTooltipLiveObserver(); + }, 0); +} + export function renderTooltip(tooltipOptions, elementId, chart) { const svgElement = document.getElementById(elementId + '_svg'); const firstRender = svgElement && parseInt(svgElement.getAttribute('opacity'), 10) > 0 ? false : true; @@ -4541,6 +4626,9 @@ export function renderTooltip(tooltipOptions, elementId, chart) { currentInstance.tooltip = new svgbase.Tooltip(options); currentInstance.tooltip.enableRTL = options.enableRTL; currentInstance.tooltip.appendTo('#' + elementId); + // A11Y-004: mark the tooltip as a polite live region so screen readers + // announce its content when it appears (WCAG 4.1.3 Status Messages). + applyTooltipLiveRegion(elementId); } else if (!sfBlazorToolkit.base.isNullOrUndefined(currentInstance.tooltip)) { currentInstance.tooltip.location = new svgbase.TooltipLocation(options.location.x, options.location.y); @@ -4561,6 +4649,9 @@ export function renderTooltip(tooltipOptions, elementId, chart) { currentInstance.tooltip.clipBounds = new svgbase.TooltipLocation(options.clipBounds.x, options.clipBounds.y); currentInstance.tooltip.arrowPadding = options.arrowPadding; currentInstance.tooltip.dataBind(); + // A11Y-004: re-apply the live-region attributes after dataBind() in case + // svgbase rebuilt the inner tooltip DOM during the update. + applyTooltipLiveRegion(elementId); } } @@ -8107,6 +8198,8 @@ export function renderStriplineTooltip(tooltipOptions, showHeaderLine, elementId currentInstance.striplineTooltip.enableRTL = tooltipOptions.enableRTL; currentInstance.striplineTooltip.showHeaderLine = showHeaderLine; currentInstance.striplineTooltip.appendTo('#' + elementId); + // A11Y-004: mark the stripline tooltip as a polite live region. + applyTooltipLiveRegion(elementId); return true; } 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..1141e93 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
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..31d0b08 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(); 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 @@ - + From 6074d478f1e800eb8485ff3cce43cdb35ba42ede Mon Sep 17 00:00:00 2001 From: YokeshSF4393 Date: Tue, 25 Aug 2026 17:12:48 +0530 Subject: [PATCH 2/5] 1047330: Updated review changes. --- .../Reflection/FastReflectionExtension.cs | 2 +- .../BaseRenderers/MultiColoredBase.cs | 18 +- src/Components/Charts/Chart/SfChart.razor.cs | 15 +- .../Charts/Common/ChartUtils/ChartHelper.cs | 246 ++++-------------- src/Components/Charts/Common/Utils/Helper.cs | 2 +- src/wwwroot/scripts/chart.js | 33 ++- 6 files changed, 78 insertions(+), 238 deletions(-) diff --git a/src/Base/Reflection/FastReflectionExtension.cs b/src/Base/Reflection/FastReflectionExtension.cs index fb7415e..9812441 100644 --- a/src/Base/Reflection/FastReflectionExtension.cs +++ b/src/Base/Reflection/FastReflectionExtension.cs @@ -31,8 +31,8 @@ public static IPropertyAccessor CreateAccessor(PropertyInfo propertyInfo) /// An that can read the property value from an object. /// /// If is null or empty, a no-op accessor is returned. + /// This method returns a non-functional accessor of type PropertyAccessor<object, object> whose GetValue returns null. /// - /// this method returns a non-functional accessor of type PropertyAccessor<object, object> whose `GetValue` returns null. public static IPropertyAccessor CreateAccessor(Type objectType, string propertyName) { PropertyInfo? propertyInfo = null; diff --git a/src/Components/Charts/Chart/Renderer/SeriesRenderers/BaseRenderers/MultiColoredBase.cs b/src/Components/Charts/Chart/Renderer/SeriesRenderers/BaseRenderers/MultiColoredBase.cs index 9e95a6c..e0df7cd 100644 --- a/src/Components/Charts/Chart/Renderer/SeriesRenderers/BaseRenderers/MultiColoredBase.cs +++ b/src/Components/Charts/Chart/Renderer/SeriesRenderers/BaseRenderers/MultiColoredBase.cs @@ -26,6 +26,7 @@ internal class MultiColoredBaseSeriesRenderer : AreaBaseSeriesRenderer /// End axis value for clipping. /// Segment index (used for clip id). /// Whether the segment axis is X. + /// Series index used for the clip id. /// CSS url() clip-path reference string or null when not applicable. private string CreateClipRect(RenderTreeBuilder builder, double startValue, double endValue, int index, bool isX, int seriesIndex) { @@ -209,15 +210,6 @@ internal List SortSegments(ChartSeries series, IList return segments; } - /// - /// Sets the fill/stroke color for the current point using the segment list or point color mapping. - /// - /// Point being colored. - /// Previous point for comparison when using point color mapping. - /// Series metadata. - /// Whether segmentation occurs on the X axis. - /// List of configured segments. - /// True when color changed compared to the previous point (used for boundary detection). /// /// Sets the fill/stroke color for the current point using the segment list or point color mapping. /// @@ -229,8 +221,14 @@ public override string SetPointColor(Point point, string color) return point is not null && !string.IsNullOrEmpty(point.Interior) ? point.Interior : color; } - + /// + /// Resolves the point color using point color mapping and configured segments. /// + /// Point being colored. + /// Previous point for comparison when using point color mapping. + /// Series metadata. + /// Whether segmentation occurs on the X axis. + /// List of configured segments. internal bool SetPointColor(Point currentPoint, Point previous, ChartSeries series, bool isXSegment, List segments) { if (string.IsNullOrEmpty(series.PointColorMapping)) diff --git a/src/Components/Charts/Chart/SfChart.razor.cs b/src/Components/Charts/Chart/SfChart.razor.cs index 22cf866..c9856b4 100644 --- a/src/Components/Charts/Chart/SfChart.razor.cs +++ b/src/Components/Charts/Chart/SfChart.razor.cs @@ -179,10 +179,10 @@ internal class JsInteropState internal List _seriesBorders = []; internal List _axes = []; internal List _highLightPatternCollection = []; - // Instance-level font measurement caches (replaces process-wide static SizePerCharacter and ChartFontKeys) + // Instance-level font measurement caches // See: ChartHelper.cs remarks for why this was moved from static to instance internal ConcurrentDictionary _fontSizeCache = new(); - internal List _requestedFontKeys = new(); + internal ConcurrentDictionary _requestedFontKeys = []; internal ChartAnnotations _annotations = new(); internal DomRect _elementOffset = new(); /*To store the SVGElement's dimensions value.*/ @@ -1249,9 +1249,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); } /// @@ -1276,7 +1276,9 @@ private async Task LoadCharacterDictionaryAsync(List distinctKeys) Dictionary charSizeList = JsonSerializer.Deserialize>(result) ?? null!; foreach (KeyValuePair charSize in charSizeList) { - _ = _fontSizeCache.TryAdd(charSize.Key, new Size { Width = charSize.Value.X, Height = charSize.Value.Y }); + Size size = new() { Width = charSize.Value.X, Height = charSize.Value.Y }; + _ = _fontSizeCache.TryAdd(charSize.Key, size); + ChartHelper.CacheSharedFontSize(charSize.Key, size); } } @@ -1837,10 +1839,9 @@ internal async Task GetCharSizeListAsync(List fontKeys) List uniqueKeys = []; foreach (string fontKey in fontKeys) { - if (!_requestedFontKeys.Contains(fontKey)) + if (_requestedFontKeys.TryAdd(fontKey, 0)) { uniqueKeys.Add(fontKey); - _requestedFontKeys.Add(fontKey); } } diff --git a/src/Components/Charts/Common/ChartUtils/ChartHelper.cs b/src/Components/Charts/Common/ChartUtils/ChartHelper.cs index 62d23a8..2793728 100644 --- a/src/Components/Charts/Common/ChartUtils/ChartHelper.cs +++ b/src/Components/Charts/Common/ChartUtils/ChartHelper.cs @@ -22,41 +22,15 @@ public class ChartHelper { #region Constants private const int RGB_HEX_CODE = 6; + private const int MAX_SHARED_FONT_CACHE_ENTRIES = 4096; private const string DEFAULT_COLOR = "white"; private const string SPACE = " "; + private static readonly ConcurrentDictionary SharedFontSizeCache = new(); + private static readonly ConcurrentQueue SharedFontSizeKeys = new(); #endregion #region Properties - /// - /// Gets a process-wide, thread-safe cache of measured character sizes indexed by font characteristics. - /// - /// - /// - /// DEPRECATED: This static cache causes unbounded memory growth on long-lived Blazor Server hosts. - /// New code should use (per-chart instance) and the overloaded - /// method that accepts an SfChart parameter. - /// - /// - /// For backward compatibility, this cache is retained but not used by new SfChart rendering paths. - /// It will be removed in a future major version. - /// - /// - [System.Obsolete("Use SfChart._fontSizeCache and MeasureText(string, ChartFontOptions, object) overload instead. This static cache causes memory leaks on long-lived Blazor Server hosts.")] - internal static ConcurrentDictionary SizePerCharacter { get; } = new ConcurrentDictionary(); - - /// - /// Gets the set of font keys for which character sizes have already been requested from JavaScript. - /// - /// - /// - /// DEPRECATED: Use instead. This static cache causes - /// unbounded memory growth on long-lived Blazor Server hosts and is not cleared per-circuit. - /// - /// - /// [System.Obsolete("Use SfChart._requestedFontKeys instead. This static cache causes memory leaks on long-lived Blazor Server hosts.")] - internal static List ChartFontKeys { get; } = new List(); - /// /// Gets a read-only lookup table mapping characters to their approximate pixel widths. /// @@ -99,7 +73,7 @@ private static void UpdateExistingElement(DynamicTextAnimationOptions element, d /// The text that may contain line breaks. /// The font settings used during measurement. /// The measured size encompassing all lines. - private static Size MeasureBreakText(string originalText, ChartFontOptions font) + private static Size MeasureBreakText(string originalText, ChartFontOptions font, object? chart = null) { originalText = originalText.Replace("
", "
", StringComparison.InvariantCulture); List textCollection = originalText.Split("
").ToList(); @@ -108,7 +82,7 @@ private static Size MeasureBreakText(string originalText, ChartFontOptions font) foreach (string text in textCollection) { - Size size = MeasureText(text, font); + Size size = MeasureText(text, font, chart); if (size is not null) { width = Math.Max(width, size.Width); @@ -120,112 +94,60 @@ private static Size MeasureBreakText(string originalText, ChartFontOptions font) } /// - /// Measures multi-line text (split by <br>) using a chart instance's cache to avoid process-wide memory leak. + /// Retrieves or computes a character size, using the chart cache when an owning chart is available. /// - /// The text that may contain line breaks. + /// The character to measure. /// 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) + /// The optional chart instance owning the cache. + /// The measured character size. + private static Size GetCharSize(char character, ChartFontOptions font, object? chart = null) { - originalText = originalText.Replace("
", "
", StringComparison.InvariantCulture); - List textCollection = originalText.Split("
").ToList(); - double width = 0; - double height = 0; + string key = character + Constants.Underscore + font.FontWeight + Constants.Underscore + font.FontStyle + Constants.Underscore + font.FontFamily; - foreach (string text in textCollection) + if (chart is Charts.SfChart sfChart) { - Size size = MeasureText(text, font, chart); - if (size is not null) - { - width = Math.Max(width, size.Width); - height += size.Height; - } + return sfChart._fontSizeCache.GetOrAdd(key, _ => GetSharedCharSize(key, character)); } - return new Size(width, height); + return GetSharedCharSize(key, character); } - /// - /// Retrieves the cached character size for a font and caches the value if missing. - /// - /// The character to measure. - /// The font settings used during measurement. - /// The measured character size. - private static Size GetCharSize(char character, ChartFontOptions font) + private static Size GetSharedCharSize(string key, char character) { - string key = character + Constants.Underscore + font.FontWeight + Constants.Underscore + font.FontStyle + Constants.Underscore + font.FontFamily; - try + if (SharedFontSizeCache.TryGetValue(key, out Size? size)) { - if (SizePerCharacter.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); - 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); - return result ?? null!; - } + return size; } - catch + + size = FontWidthLookup.TryGetValue(character, out double charWidth) + ? new Size { Width = charWidth * 6.25, Height = 130 } + : new Size { Width = 50, Height = 130 }; + + if (SharedFontSizeCache.TryAdd(key, size)) { - throw; + SharedFontSizeKeys.Enqueue(key); + TrimSharedFontSizeCache(); } + + return size; } - /// - /// Retrieves the cached character size from a chart-instance cache, or computes and caches the value if missing. - /// - /// - /// This method uses instance-level caching (per SfChart) to avoid the process-wide memory leak of the static version. - /// The chart's own _fontSizeCache is used instead of the deprecated . - /// - /// 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) + internal static void CacheSharedFontSize(string key, Size size) { - var sfChart = chart as Charts.SfChart; - if (sfChart is null) - return GetCharSize(character, font); // Fallback to process-wide cache if chart not available - - var fontCache = sfChart._fontSizeCache; - string key = character + Constants.Underscore + font.FontWeight + Constants.Underscore + font.FontStyle + Constants.Underscore + font.FontFamily; - try + bool isNewKey = !SharedFontSizeCache.ContainsKey(key); + SharedFontSizeCache[key] = size; + if (isNewKey) { - if (fontCache.TryGetValue(key, out Size? charSize)) - { - return charSize ?? null!; - } - double charWidth; - if (FontWidthLookup.TryGetValue(character, out charWidth)) - { - Size newSize = new Size { Width = charWidth * 6.25, Height = 130 }; - Size result = fontCache.GetOrAdd(key, newSize); - return result ?? null!; - } - else - { - Size defaultSize = new Size { Width = 50, Height = 130 }; - Size result = fontCache.GetOrAdd(key, defaultSize); - return result ?? null!; - } + SharedFontSizeKeys.Enqueue(key); + TrimSharedFontSizeCache(); } - catch + } + + private static void TrimSharedFontSizeCache() + { + while (SharedFontSizeCache.Count > MAX_SHARED_FONT_CACHE_ENTRIES && SharedFontSizeKeys.TryDequeue(out string? key)) { - throw; + _ = SharedFontSizeCache.TryRemove(key, out _); } } @@ -1226,51 +1148,7 @@ internal static Rect AppendRectElements(SfChart chart, string id, Rect rect) /// The text to measure. /// The font options for text measurement. /// The calculated size of the text. - internal static Size MeasureText(string text, ChartFontOptions font) - { - if (text.Contains("
", StringComparison.InvariantCulture) || text.Contains("
", StringComparison.InvariantCulture)) - { - return MeasureBreakText(text, font); - } - - 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)) - { - charSize = value; - return new Size(charSize.Width * (fontSize / 100), charSize.Height * (fontSize / 100)); - } - } - - 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 instead of the deprecated - /// static , 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) + internal static Size MeasureText(string text, ChartFontOptions font, object? chart = null) { if (text.Contains("
", StringComparison.InvariantCulture) || text.Contains("
", StringComparison.InvariantCulture)) { @@ -1280,20 +1158,9 @@ internal static Size MeasureText(string text, ChartFontOptions font, object char 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; - 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)); - } - } - for (int i = 0; i < text.Length; i++) { - charSize = GetCharSize(chart, text[i], font); + charSize = GetCharSize(text[i], font, chart); if (charSize is not null) { width += charSize.Width > 0 ? charSize.Width : 100; @@ -2476,7 +2343,7 @@ 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) + internal static void GetDistinctCharacter(string text, ChartFontOptions font, List distinctKeys, Charts.SfChart chart) { if (!string.IsNullOrEmpty(text)) { @@ -2484,20 +2351,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); } } } @@ -2505,29 +2370,6 @@ internal static void GetDistinctCharacter(string text, ChartFontOptions font, Li } } - /// - /// Clears static font measurement caches. - /// - /// - /// /// - /// DEPRECATED: This method is no longer called by SfChart. New code should rely on - /// which clears the instance-level - /// and . - /// - /// - /// Historically, this method could not be called during chart disposal because clearing a process-wide cache - /// from a single chart's teardown evicted entries still in use by every other live chart on the process, - /// forcing them to re-measure via JS interop. The static caches are now deprecated in favor of per-instance caches. - /// - /// - [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] - [System.ComponentModel.Browsable(false)] - [System.Obsolete("No longer called. Use SfChart instance-level caches via DisposeAsyncCore instead.")] - internal static void ClearStaticStorage() - { - SizePerCharacter.Clear(); - ChartFontKeys.Clear(); - } #endregion } diff --git a/src/Components/Charts/Common/Utils/Helper.cs b/src/Components/Charts/Common/Utils/Helper.cs index 7dd674d..4961dbf 100644 --- a/src/Components/Charts/Common/Utils/Helper.cs +++ b/src/Components/Charts/Common/Utils/Helper.cs @@ -81,7 +81,7 @@ public static double StringToNumber(string size, double containerSize) #region Nested Validator /// - /// Validates a string value against the WAI-ARIA 1.2 abstract role list. + /// Validates a string value against the WAI-ARIA 1.2 abstract role list, ignoring case. /// /// /// diff --git a/src/wwwroot/scripts/chart.js b/src/wwwroot/scripts/chart.js index 979d353..05f9cf9 100644 --- a/src/wwwroot/scripts/chart.js +++ b/src/wwwroot/scripts/chart.js @@ -4543,7 +4543,8 @@ export function findColor(data, series) { // when it appears on hover/focus, satisfying WCAG 2.1 SC 4.1.3 (Status Messages). // The attributes are idempotent — repeated calls are safe. // -// Implementation: a single MutationObserver attached to `document.body` that +// Implementation: one module-level singleton MutationObserver attached to +// `document.body` that // catches any inserted `.e-tooltip` element anywhere in the document. This // covers every chart on the page (single or multi-chart demos), template // tooltips, and any future svgbase re-renders. Plus a one-shot tagged @@ -4599,22 +4600,20 @@ function _ensureTooltipLiveObserver() { _tooltipLiveObserver.observe(document.body, { childList: true, subtree: true }); } function applyTooltipLiveRegion(tooltipHostId) { - // Run both an immediate scan (in case the tooltip is already in the DOM) - // and set up the document-wide observer to catch future insertions. - setTimeout(function () { - const tooltipHost = document.getElementById(tooltipHostId); - if (tooltipHost) { - if (tooltipHost.classList && tooltipHost.classList.contains('e-tooltip')) { - _markTooltipLiveRegion(tooltipHost); - } - const chartContainer = tooltipHost.parentElement; - if (chartContainer) { - _scanDocumentForTooltips(chartContainer); - } - } - _scanDocumentForTooltips(document.body); - _ensureTooltipLiveObserver(); - }, 0); + // Install the observer before scanning so a tooltip inserted during this + // call is covered, and mark existing nodes before returning to the caller. + _ensureTooltipLiveObserver(); + const tooltipHost = document.getElementById(tooltipHostId); + if (tooltipHost) { + if (tooltipHost.classList && tooltipHost.classList.contains('e-tooltip')) { + _markTooltipLiveRegion(tooltipHost); + } + const chartContainer = tooltipHost.parentElement; + if (chartContainer) { + _scanDocumentForTooltips(chartContainer); + } + } + _scanDocumentForTooltips(document.body); } export function renderTooltip(tooltipOptions, elementId, chart) { From aa6e76b70c784c3c24bda4a42963beee24a8d5ff Mon Sep 17 00:00:00 2001 From: YokeshSF4393 Date: Tue, 25 Aug 2026 18:04:07 +0530 Subject: [PATCH 3/5] Revert "1047330: Updated review changes." This reverts commit 6074d478f1e800eb8485ff3cce43cdb35ba42ede. --- .../Reflection/FastReflectionExtension.cs | 2 +- .../BaseRenderers/MultiColoredBase.cs | 18 +- src/Components/Charts/Chart/SfChart.razor.cs | 15 +- .../Charts/Common/ChartUtils/ChartHelper.cs | 246 ++++++++++++++---- src/Components/Charts/Common/Utils/Helper.cs | 2 +- src/wwwroot/scripts/chart.js | 33 +-- 6 files changed, 238 insertions(+), 78 deletions(-) diff --git a/src/Base/Reflection/FastReflectionExtension.cs b/src/Base/Reflection/FastReflectionExtension.cs index 9812441..fb7415e 100644 --- a/src/Base/Reflection/FastReflectionExtension.cs +++ b/src/Base/Reflection/FastReflectionExtension.cs @@ -31,8 +31,8 @@ public static IPropertyAccessor CreateAccessor(PropertyInfo propertyInfo) /// An that can read the property value from an object. /// /// If is null or empty, a no-op accessor is returned. - /// This method returns a non-functional accessor of type PropertyAccessor<object, object> whose GetValue returns null. /// + /// this method returns a non-functional accessor of type PropertyAccessor<object, object> whose `GetValue` returns null. public static IPropertyAccessor CreateAccessor(Type objectType, string propertyName) { PropertyInfo? propertyInfo = null; diff --git a/src/Components/Charts/Chart/Renderer/SeriesRenderers/BaseRenderers/MultiColoredBase.cs b/src/Components/Charts/Chart/Renderer/SeriesRenderers/BaseRenderers/MultiColoredBase.cs index e0df7cd..9e95a6c 100644 --- a/src/Components/Charts/Chart/Renderer/SeriesRenderers/BaseRenderers/MultiColoredBase.cs +++ b/src/Components/Charts/Chart/Renderer/SeriesRenderers/BaseRenderers/MultiColoredBase.cs @@ -26,7 +26,6 @@ internal class MultiColoredBaseSeriesRenderer : AreaBaseSeriesRenderer /// End axis value for clipping. /// Segment index (used for clip id). /// Whether the segment axis is X. - /// Series index used for the clip id. /// CSS url() clip-path reference string or null when not applicable. private string CreateClipRect(RenderTreeBuilder builder, double startValue, double endValue, int index, bool isX, int seriesIndex) { @@ -210,6 +209,15 @@ internal List SortSegments(ChartSeries series, IList return segments; } + /// + /// Sets the fill/stroke color for the current point using the segment list or point color mapping. + /// + /// Point being colored. + /// Previous point for comparison when using point color mapping. + /// Series metadata. + /// Whether segmentation occurs on the X axis. + /// List of configured segments. + /// True when color changed compared to the previous point (used for boundary detection). /// /// Sets the fill/stroke color for the current point using the segment list or point color mapping. /// @@ -221,14 +229,8 @@ public override string SetPointColor(Point point, string color) return point is not null && !string.IsNullOrEmpty(point.Interior) ? point.Interior : color; } - /// - /// Resolves the point color using point color mapping and configured segments. + /// - /// Point being colored. - /// Previous point for comparison when using point color mapping. - /// Series metadata. - /// Whether segmentation occurs on the X axis. - /// List of configured segments. internal bool SetPointColor(Point currentPoint, Point previous, ChartSeries series, bool isXSegment, List segments) { if (string.IsNullOrEmpty(series.PointColorMapping)) diff --git a/src/Components/Charts/Chart/SfChart.razor.cs b/src/Components/Charts/Chart/SfChart.razor.cs index c9856b4..22cf866 100644 --- a/src/Components/Charts/Chart/SfChart.razor.cs +++ b/src/Components/Charts/Chart/SfChart.razor.cs @@ -179,10 +179,10 @@ internal class JsInteropState internal List _seriesBorders = []; internal List _axes = []; internal List _highLightPatternCollection = []; - // Instance-level font measurement caches + // Instance-level font measurement caches (replaces process-wide static SizePerCharacter and ChartFontKeys) // See: ChartHelper.cs remarks for why this was moved from static to instance internal ConcurrentDictionary _fontSizeCache = new(); - internal ConcurrentDictionary _requestedFontKeys = []; + internal List _requestedFontKeys = new(); internal ChartAnnotations _annotations = new(); internal DomRect _elementOffset = new(); /*To store the SVGElement's dimensions value.*/ @@ -1249,9 +1249,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 void GetDistinctCharacter(string text, ChartFontOptions font, List distinctKeys) + private static void GetDistinctCharacter(string text, ChartFontOptions font, List distinctKeys) { - ChartHelper.GetDistinctCharacter(text, font, distinctKeys, this); + ChartHelper.GetDistinctCharacter(text, font, distinctKeys); } /// @@ -1276,9 +1276,7 @@ private async Task LoadCharacterDictionaryAsync(List distinctKeys) Dictionary charSizeList = JsonSerializer.Deserialize>(result) ?? null!; foreach (KeyValuePair charSize in charSizeList) { - Size size = new() { Width = charSize.Value.X, Height = charSize.Value.Y }; - _ = _fontSizeCache.TryAdd(charSize.Key, size); - ChartHelper.CacheSharedFontSize(charSize.Key, size); + _ = _fontSizeCache.TryAdd(charSize.Key, new Size { Width = charSize.Value.X, Height = charSize.Value.Y }); } } @@ -1839,9 +1837,10 @@ internal async Task GetCharSizeListAsync(List fontKeys) List uniqueKeys = []; foreach (string fontKey in fontKeys) { - if (_requestedFontKeys.TryAdd(fontKey, 0)) + if (!_requestedFontKeys.Contains(fontKey)) { uniqueKeys.Add(fontKey); + _requestedFontKeys.Add(fontKey); } } diff --git a/src/Components/Charts/Common/ChartUtils/ChartHelper.cs b/src/Components/Charts/Common/ChartUtils/ChartHelper.cs index 2793728..62d23a8 100644 --- a/src/Components/Charts/Common/ChartUtils/ChartHelper.cs +++ b/src/Components/Charts/Common/ChartUtils/ChartHelper.cs @@ -22,15 +22,41 @@ public class ChartHelper { #region Constants private const int RGB_HEX_CODE = 6; - private const int MAX_SHARED_FONT_CACHE_ENTRIES = 4096; private const string DEFAULT_COLOR = "white"; private const string SPACE = " "; - private static readonly ConcurrentDictionary SharedFontSizeCache = new(); - private static readonly ConcurrentQueue SharedFontSizeKeys = new(); #endregion #region Properties + /// + /// Gets a process-wide, thread-safe cache of measured character sizes indexed by font characteristics. + /// + /// + /// + /// DEPRECATED: This static cache causes unbounded memory growth on long-lived Blazor Server hosts. + /// New code should use (per-chart instance) and the overloaded + /// method that accepts an SfChart parameter. + /// + /// + /// For backward compatibility, this cache is retained but not used by new SfChart rendering paths. + /// It will be removed in a future major version. + /// + /// + [System.Obsolete("Use SfChart._fontSizeCache and MeasureText(string, ChartFontOptions, object) overload instead. This static cache causes memory leaks on long-lived Blazor Server hosts.")] + internal static ConcurrentDictionary SizePerCharacter { get; } = new ConcurrentDictionary(); + + /// + /// Gets the set of font keys for which character sizes have already been requested from JavaScript. + /// + /// + /// + /// DEPRECATED: Use instead. This static cache causes + /// unbounded memory growth on long-lived Blazor Server hosts and is not cleared per-circuit. + /// + /// + /// [System.Obsolete("Use SfChart._requestedFontKeys instead. This static cache causes memory leaks on long-lived Blazor Server hosts.")] + internal static List ChartFontKeys { get; } = new List(); + /// /// Gets a read-only lookup table mapping characters to their approximate pixel widths. /// @@ -73,7 +99,7 @@ private static void UpdateExistingElement(DynamicTextAnimationOptions element, d /// The text that may contain line breaks. /// The font settings used during measurement. /// The measured size encompassing all lines. - private static Size MeasureBreakText(string originalText, ChartFontOptions font, object? chart = null) + private static Size MeasureBreakText(string originalText, ChartFontOptions font) { originalText = originalText.Replace("
", "
", StringComparison.InvariantCulture); List textCollection = originalText.Split("
").ToList(); @@ -82,7 +108,7 @@ private static Size MeasureBreakText(string originalText, ChartFontOptions font, foreach (string text in textCollection) { - Size size = MeasureText(text, font, chart); + Size size = MeasureText(text, font); if (size is not null) { width = Math.Max(width, size.Width); @@ -94,60 +120,112 @@ private static Size MeasureBreakText(string originalText, ChartFontOptions font, } /// - /// Retrieves or computes a character size, using the chart cache when an owning chart is available. + /// Measures multi-line text (split by <br>) using a chart instance's cache to avoid process-wide memory leak. /// - /// The character to measure. + /// The text that may contain line breaks. /// The font settings used during measurement. - /// The optional chart instance owning the cache. - /// The measured character size. - private static Size GetCharSize(char character, ChartFontOptions font, object? chart = null) + /// The chart instance owning the cache. + /// The measured size encompassing all lines. + private static Size MeasureBreakText(string originalText, ChartFontOptions font, object chart) { - string key = character + Constants.Underscore + font.FontWeight + Constants.Underscore + font.FontStyle + Constants.Underscore + font.FontFamily; + originalText = originalText.Replace("
", "
", StringComparison.InvariantCulture); + List textCollection = originalText.Split("
").ToList(); + double width = 0; + double height = 0; - if (chart is Charts.SfChart sfChart) + foreach (string text in textCollection) { - return sfChart._fontSizeCache.GetOrAdd(key, _ => GetSharedCharSize(key, character)); + Size size = MeasureText(text, font, chart); + if (size is not null) + { + width = Math.Max(width, size.Width); + height += size.Height; + } } - return GetSharedCharSize(key, character); + return new Size(width, height); } - private static Size GetSharedCharSize(string key, char character) + /// + /// Retrieves the cached character size for a font and caches the value if missing. + /// + /// The character to measure. + /// The font settings used during measurement. + /// The measured character size. + private static Size GetCharSize(char character, ChartFontOptions font) { - if (SharedFontSizeCache.TryGetValue(key, out Size? size)) + string key = character + Constants.Underscore + font.FontWeight + Constants.Underscore + font.FontStyle + Constants.Underscore + font.FontFamily; + try { - return size; + if (SizePerCharacter.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); + 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); + return result ?? null!; + } } - - size = FontWidthLookup.TryGetValue(character, out double charWidth) - ? new Size { Width = charWidth * 6.25, Height = 130 } - : new Size { Width = 50, Height = 130 }; - - if (SharedFontSizeCache.TryAdd(key, size)) + catch { - SharedFontSizeKeys.Enqueue(key); - TrimSharedFontSizeCache(); + throw; } - - return size; } - internal static void CacheSharedFontSize(string key, Size size) + /// + /// Retrieves the cached character size from a chart-instance cache, or computes and caches the value if missing. + /// + /// + /// This method uses instance-level caching (per SfChart) to avoid the process-wide memory leak of the static version. + /// The chart's own _fontSizeCache is used instead of the deprecated . + /// + /// 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) { - bool isNewKey = !SharedFontSizeCache.ContainsKey(key); - SharedFontSizeCache[key] = size; - if (isNewKey) + var sfChart = chart as Charts.SfChart; + if (sfChart is null) + return GetCharSize(character, font); // Fallback to process-wide cache if chart not available + + var fontCache = sfChart._fontSizeCache; + string key = character + Constants.Underscore + font.FontWeight + Constants.Underscore + font.FontStyle + Constants.Underscore + font.FontFamily; + try { - SharedFontSizeKeys.Enqueue(key); - TrimSharedFontSizeCache(); + if (fontCache.TryGetValue(key, out Size? charSize)) + { + return charSize ?? null!; + } + double charWidth; + if (FontWidthLookup.TryGetValue(character, out charWidth)) + { + Size newSize = new Size { Width = charWidth * 6.25, Height = 130 }; + Size result = fontCache.GetOrAdd(key, newSize); + return result ?? null!; + } + else + { + Size defaultSize = new Size { Width = 50, Height = 130 }; + Size result = fontCache.GetOrAdd(key, defaultSize); + return result ?? null!; + } } - } - - private static void TrimSharedFontSizeCache() - { - while (SharedFontSizeCache.Count > MAX_SHARED_FONT_CACHE_ENTRIES && SharedFontSizeKeys.TryDequeue(out string? key)) + catch { - _ = SharedFontSizeCache.TryRemove(key, out _); + throw; } } @@ -1148,7 +1226,51 @@ internal static Rect AppendRectElements(SfChart chart, string id, Rect rect) /// The text to measure. /// The font options for text measurement. /// The calculated size of the text. - internal static Size MeasureText(string text, ChartFontOptions font, object? chart = null) + internal static Size MeasureText(string text, ChartFontOptions font) + { + if (text.Contains("
", StringComparison.InvariantCulture) || text.Contains("
", StringComparison.InvariantCulture)) + { + return MeasureBreakText(text, font); + } + + 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)) + { + charSize = value; + return new Size(charSize.Width * (fontSize / 100), charSize.Height * (fontSize / 100)); + } + } + + 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 instead of the deprecated + /// static , 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)) { @@ -1158,9 +1280,20 @@ internal static Size MeasureText(string text, ChartFontOptions font, object? cha 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; + 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)); + } + } + for (int i = 0; i < text.Length; i++) { - charSize = GetCharSize(text[i], font, chart); + charSize = GetCharSize(chart, text[i], font); if (charSize is not null) { width += charSize.Width > 0 ? charSize.Width : 100; @@ -2343,7 +2476,7 @@ 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, Charts.SfChart chart) + internal static void GetDistinctCharacter(string text, ChartFontOptions font, List distinctKeys) { if (!string.IsNullOrEmpty(text)) { @@ -2351,18 +2484,20 @@ 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 (!chart._fontSizeCache.ContainsKey(key) && chart._requestedFontKeys.TryAdd(key, 0)) + if (!SizePerCharacter.ContainsKey(key) && !ChartFontKeys.Contains(key)) { 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 (!chart._fontSizeCache.ContainsKey(key) && chart._requestedFontKeys.TryAdd(key, 0)) + if (!SizePerCharacter.ContainsKey(key) && !ChartFontKeys.Contains(key)) { distinctKeys.Add(key); + ChartFontKeys.Add(key); } } } @@ -2370,6 +2505,29 @@ internal static void GetDistinctCharacter(string text, ChartFontOptions font, Li } } + /// + /// Clears static font measurement caches. + /// + /// + /// /// + /// DEPRECATED: This method is no longer called by SfChart. New code should rely on + /// which clears the instance-level + /// and . + /// + /// + /// Historically, this method could not be called during chart disposal because clearing a process-wide cache + /// from a single chart's teardown evicted entries still in use by every other live chart on the process, + /// forcing them to re-measure via JS interop. The static caches are now deprecated in favor of per-instance caches. + /// + /// + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + [System.ComponentModel.Browsable(false)] + [System.Obsolete("No longer called. Use SfChart instance-level caches via DisposeAsyncCore instead.")] + internal static void ClearStaticStorage() + { + SizePerCharacter.Clear(); + ChartFontKeys.Clear(); + } #endregion } diff --git a/src/Components/Charts/Common/Utils/Helper.cs b/src/Components/Charts/Common/Utils/Helper.cs index 4961dbf..7dd674d 100644 --- a/src/Components/Charts/Common/Utils/Helper.cs +++ b/src/Components/Charts/Common/Utils/Helper.cs @@ -81,7 +81,7 @@ public static double StringToNumber(string size, double containerSize) #region Nested Validator /// - /// Validates a string value against the WAI-ARIA 1.2 abstract role list, ignoring case. + /// Validates a string value against the WAI-ARIA 1.2 abstract role list. /// /// /// diff --git a/src/wwwroot/scripts/chart.js b/src/wwwroot/scripts/chart.js index 05f9cf9..979d353 100644 --- a/src/wwwroot/scripts/chart.js +++ b/src/wwwroot/scripts/chart.js @@ -4543,8 +4543,7 @@ export function findColor(data, series) { // when it appears on hover/focus, satisfying WCAG 2.1 SC 4.1.3 (Status Messages). // The attributes are idempotent — repeated calls are safe. // -// Implementation: one module-level singleton MutationObserver attached to -// `document.body` that +// Implementation: a single MutationObserver attached to `document.body` that // catches any inserted `.e-tooltip` element anywhere in the document. This // covers every chart on the page (single or multi-chart demos), template // tooltips, and any future svgbase re-renders. Plus a one-shot tagged @@ -4600,20 +4599,22 @@ function _ensureTooltipLiveObserver() { _tooltipLiveObserver.observe(document.body, { childList: true, subtree: true }); } function applyTooltipLiveRegion(tooltipHostId) { - // Install the observer before scanning so a tooltip inserted during this - // call is covered, and mark existing nodes before returning to the caller. - _ensureTooltipLiveObserver(); - const tooltipHost = document.getElementById(tooltipHostId); - if (tooltipHost) { - if (tooltipHost.classList && tooltipHost.classList.contains('e-tooltip')) { - _markTooltipLiveRegion(tooltipHost); - } - const chartContainer = tooltipHost.parentElement; - if (chartContainer) { - _scanDocumentForTooltips(chartContainer); - } - } - _scanDocumentForTooltips(document.body); + // Run both an immediate scan (in case the tooltip is already in the DOM) + // and set up the document-wide observer to catch future insertions. + setTimeout(function () { + const tooltipHost = document.getElementById(tooltipHostId); + if (tooltipHost) { + if (tooltipHost.classList && tooltipHost.classList.contains('e-tooltip')) { + _markTooltipLiveRegion(tooltipHost); + } + const chartContainer = tooltipHost.parentElement; + if (chartContainer) { + _scanDocumentForTooltips(chartContainer); + } + } + _scanDocumentForTooltips(document.body); + _ensureTooltipLiveObserver(); + }, 0); } export function renderTooltip(tooltipOptions, elementId, chart) { From 9a9d49a1bf9b9c19e3e0f2521caf17e2b1601b61 Mon Sep 17 00:00:00 2001 From: YokeshSF4393 Date: Tue, 25 Aug 2026 19:37:15 +0530 Subject: [PATCH 4/5] 1047330: Updated review changes. --- src/Components/Charts/Chart/SfChart.razor.cs | 12 +- .../Charts/Common/ChartUtils/ChartHelper.cs | 119 +++--------------- src/wwwroot/scripts/chart.js | 93 -------------- 3 files changed, 23 insertions(+), 201 deletions(-) diff --git a/src/Components/Charts/Chart/SfChart.razor.cs b/src/Components/Charts/Chart/SfChart.razor.cs index 22cf866..e085022 100644 --- a/src/Components/Charts/Chart/SfChart.razor.cs +++ b/src/Components/Charts/Chart/SfChart.razor.cs @@ -179,10 +179,9 @@ internal class JsInteropState internal List _seriesBorders = []; internal List _axes = []; internal List _highLightPatternCollection = []; - // Instance-level font measurement caches (replaces process-wide static SizePerCharacter and ChartFontKeys) - // See: ChartHelper.cs remarks for why this was moved from static to instance + // Instance-level font measurement caches internal ConcurrentDictionary _fontSizeCache = new(); - internal List _requestedFontKeys = new(); + internal ConcurrentDictionary _requestedFontKeys = new(); internal ChartAnnotations _annotations = new(); internal DomRect _elementOffset = new(); /*To store the SVGElement's dimensions value.*/ @@ -1249,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); } /// @@ -1837,10 +1836,9 @@ internal async Task GetCharSizeListAsync(List fontKeys) List uniqueKeys = []; foreach (string fontKey in fontKeys) { - if (!_requestedFontKeys.Contains(fontKey)) + if (_requestedFontKeys.TryAdd(fontKey, 0)) { uniqueKeys.Add(fontKey); - _requestedFontKeys.Add(fontKey); } } diff --git a/src/Components/Charts/Common/ChartUtils/ChartHelper.cs b/src/Components/Charts/Common/ChartUtils/ChartHelper.cs index 62d23a8..fd27661 100644 --- a/src/Components/Charts/Common/ChartUtils/ChartHelper.cs +++ b/src/Components/Charts/Common/ChartUtils/ChartHelper.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using System.Collections.ObjectModel; using System.Drawing; using System.Dynamic; @@ -28,35 +27,6 @@ public class ChartHelper #region Properties - /// - /// Gets a process-wide, thread-safe cache of measured character sizes indexed by font characteristics. - /// - /// - /// - /// DEPRECATED: This static cache causes unbounded memory growth on long-lived Blazor Server hosts. - /// New code should use (per-chart instance) and the overloaded - /// method that accepts an SfChart parameter. - /// - /// - /// For backward compatibility, this cache is retained but not used by new SfChart rendering paths. - /// It will be removed in a future major version. - /// - /// - [System.Obsolete("Use SfChart._fontSizeCache and MeasureText(string, ChartFontOptions, object) overload instead. This static cache causes memory leaks on long-lived Blazor Server hosts.")] - internal static ConcurrentDictionary SizePerCharacter { get; } = new ConcurrentDictionary(); - - /// - /// Gets the set of font keys for which character sizes have already been requested from JavaScript. - /// - /// - /// - /// DEPRECATED: Use instead. This static cache causes - /// unbounded memory growth on long-lived Blazor Server hosts and is not cleared per-circuit. - /// - /// - /// [System.Obsolete("Use SfChart._requestedFontKeys instead. This static cache causes memory leaks on long-lived Blazor Server hosts.")] - internal static List ChartFontKeys { get; } = new List(); - /// /// Gets a read-only lookup table mapping characters to their approximate pixel widths. /// @@ -154,42 +124,25 @@ private static Size MeasureBreakText(string originalText, ChartFontOptions font, /// The measured character size. private static Size GetCharSize(char character, ChartFontOptions font) { - string key = character + Constants.Underscore + font.FontWeight + Constants.Underscore + font.FontStyle + Constants.Underscore + font.FontFamily; - try + if (FontWidthLookup.TryGetValue(character, out double charWidth)) { - if (SizePerCharacter.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); - 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); - return result ?? null!; - } - } - catch - { - throw; + return new Size { Width = charWidth * 6.25, Height = 130 }; } + + return new Size { Width = 50, Height = 130 }; } /// - /// Retrieves the cached character size from a chart-instance cache, or computes and caches the value if missing. + /// 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 avoid the process-wide memory leak of the static version. - /// The chart's own _fontSizeCache is used instead of the deprecated . + /// 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. @@ -199,7 +152,7 @@ private static Size GetCharSize(object chart, char character, ChartFontOptions f { var sfChart = chart as Charts.SfChart; if (sfChart is null) - return GetCharSize(character, font); // Fallback to process-wide cache if chart not available + return GetCharSize(character, font); var fontCache = sfChart._fontSizeCache; string key = character + Constants.Underscore + font.FontWeight + Constants.Underscore + font.FontStyle + Constants.Underscore + font.FontFamily; @@ -1236,16 +1189,6 @@ internal static Size MeasureText(string text, ChartFontOptions font) 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)) - { - charSize = value; - return new Size(charSize.Width * (fontSize / 100), charSize.Height * (fontSize / 100)); - } - } - for (int i = 0; i < text.Length; i++) { charSize = GetCharSize(text[i], font); @@ -1263,8 +1206,7 @@ internal static Size MeasureText(string text, ChartFontOptions font) /// Measures text using a chart instance's font cache to avoid process-wide memory accumulation. /// /// - /// This overload uses the chart's instance-level _fontSizeCache instead of the deprecated - /// static , ensuring per-circuit/per-chart memory isolation. + /// 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. @@ -2476,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)) { @@ -2484,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); } } } @@ -2505,30 +2446,6 @@ internal static void GetDistinctCharacter(string text, ChartFontOptions font, Li } } - /// - /// Clears static font measurement caches. - /// - /// - /// /// - /// DEPRECATED: This method is no longer called by SfChart. New code should rely on - /// which clears the instance-level - /// and . - /// - /// - /// Historically, this method could not be called during chart disposal because clearing a process-wide cache - /// from a single chart's teardown evicted entries still in use by every other live chart on the process, - /// forcing them to re-measure via JS interop. The static caches are now deprecated in favor of per-instance caches. - /// - /// - [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] - [System.ComponentModel.Browsable(false)] - [System.Obsolete("No longer called. Use SfChart instance-level caches via DisposeAsyncCore instead.")] - internal static void ClearStaticStorage() - { - SizePerCharacter.Clear(); - ChartFontKeys.Clear(); - } - #endregion } } diff --git a/src/wwwroot/scripts/chart.js b/src/wwwroot/scripts/chart.js index 979d353..ec8f440 100644 --- a/src/wwwroot/scripts/chart.js +++ b/src/wwwroot/scripts/chart.js @@ -1849,12 +1849,6 @@ export function initialize(_dataId, element, dotnetRef, isZooming, isScrollbar, ejSVGTooltipElements.forEach((ejSVGTooltip) => { sfBlazorToolkit.base.setStyleAttribute(ejSVGTooltip, { position: 'absolute', zIndex: '1', pointerEvents: 'none' }); }); - // A11Y-004: mark every tooltip placeholder in this chart as a polite ARIA - // live region. This handles pages where the tooltip is never actually - // rendered (no hoverable data points) but the placeholder container still - // exists in the DOM. Render-time inserts are caught by the document-wide - // MutationObserver that applyTooltipLiveRegion installs. - applyTooltipLiveRegion(element.id); } instance.render(); } @@ -4538,85 +4532,6 @@ export function findColor(data, series) { return (data.point.i !== '' ? data.point.i : (series.marker.f !== '' ? series.marker.f : series.interior)); } -// A11Y-004: marks every tooltip element under the chart root as a polite ARIA live -// region so screen readers (NVDA, JAWS, VoiceOver) announce the tooltip text -// when it appears on hover/focus, satisfying WCAG 2.1 SC 4.1.3 (Status Messages). -// The attributes are idempotent — repeated calls are safe. -// -// Implementation: a single MutationObserver attached to `document.body` that -// catches any inserted `.e-tooltip` element anywhere in the document. This -// covers every chart on the page (single or multi-chart demos), template -// tooltips, and any future svgbase re-renders. Plus a one-shot tagged -// immediate run so the first tooltip is marked without waiting for the -// observer to fire. -const _tooltipLiveApplied = new WeakSet(); -let _tooltipLiveObserver = null; -function _markTooltipLiveRegion(node) { - if (!node || node.nodeType !== 1 || _tooltipLiveApplied.has(node)) { - return; - } - node.setAttribute('role', 'status'); - node.setAttribute('aria-live', 'polite'); - node.setAttribute('aria-atomic', 'true'); - _tooltipLiveApplied.add(node); -} -function _scanDocumentForTooltips(root) { - if (!root || !root.querySelectorAll) { - return; - } - // Two selectors: the fully rendered tooltip (`.e-tooltip` added by - // svgbase.Tooltip after appendTo) and the chart's own tooltip placeholder - // container (`.ejSVGTooltip` added when the chart initialises). - const found = root.querySelectorAll('.e-tooltip, .ejSVGTooltip'); - for (let i = 0; i < found.length; i++) { - _markTooltipLiveRegion(found[i]); - } -} -function _ensureTooltipLiveObserver() { - if (_tooltipLiveObserver) { - return; - } - _tooltipLiveObserver = new MutationObserver(function (mutations) { - for (let i = 0; i < mutations.length; i++) { - const added = mutations[i].addedNodes; - for (let j = 0; j < added.length; j++) { - const node = added[j]; - if (node.nodeType !== 1) { - continue; - } - if (node.classList && node.classList.contains('e-tooltip')) { - _markTooltipLiveRegion(node); - } - if (node.querySelectorAll) { - const descendants = node.querySelectorAll('.e-tooltip, .ejSVGTooltip'); - for (let k = 0; k < descendants.length; k++) { - _markTooltipLiveRegion(descendants[k]); - } - } - } - } - }); - _tooltipLiveObserver.observe(document.body, { childList: true, subtree: true }); -} -function applyTooltipLiveRegion(tooltipHostId) { - // Run both an immediate scan (in case the tooltip is already in the DOM) - // and set up the document-wide observer to catch future insertions. - setTimeout(function () { - const tooltipHost = document.getElementById(tooltipHostId); - if (tooltipHost) { - if (tooltipHost.classList && tooltipHost.classList.contains('e-tooltip')) { - _markTooltipLiveRegion(tooltipHost); - } - const chartContainer = tooltipHost.parentElement; - if (chartContainer) { - _scanDocumentForTooltips(chartContainer); - } - } - _scanDocumentForTooltips(document.body); - _ensureTooltipLiveObserver(); - }, 0); -} - export function renderTooltip(tooltipOptions, elementId, chart) { const svgElement = document.getElementById(elementId + '_svg'); const firstRender = svgElement && parseInt(svgElement.getAttribute('opacity'), 10) > 0 ? false : true; @@ -4626,9 +4541,6 @@ export function renderTooltip(tooltipOptions, elementId, chart) { currentInstance.tooltip = new svgbase.Tooltip(options); currentInstance.tooltip.enableRTL = options.enableRTL; currentInstance.tooltip.appendTo('#' + elementId); - // A11Y-004: mark the tooltip as a polite live region so screen readers - // announce its content when it appears (WCAG 4.1.3 Status Messages). - applyTooltipLiveRegion(elementId); } else if (!sfBlazorToolkit.base.isNullOrUndefined(currentInstance.tooltip)) { currentInstance.tooltip.location = new svgbase.TooltipLocation(options.location.x, options.location.y); @@ -4649,9 +4561,6 @@ export function renderTooltip(tooltipOptions, elementId, chart) { currentInstance.tooltip.clipBounds = new svgbase.TooltipLocation(options.clipBounds.x, options.clipBounds.y); currentInstance.tooltip.arrowPadding = options.arrowPadding; currentInstance.tooltip.dataBind(); - // A11Y-004: re-apply the live-region attributes after dataBind() in case - // svgbase rebuilt the inner tooltip DOM during the update. - applyTooltipLiveRegion(elementId); } } @@ -8198,8 +8107,6 @@ export function renderStriplineTooltip(tooltipOptions, showHeaderLine, elementId currentInstance.striplineTooltip.enableRTL = tooltipOptions.enableRTL; currentInstance.striplineTooltip.showHeaderLine = showHeaderLine; currentInstance.striplineTooltip.appendTo('#' + elementId); - // A11Y-004: mark the stripline tooltip as a polite live region. - applyTooltipLiveRegion(elementId); return true; } From 6460b9e06c7afa8b5539d0e8a7a816cab30973af Mon Sep 17 00:00:00 2001 From: YokeshSF4393 Date: Wed, 26 Aug 2026 13:57:36 +0530 Subject: [PATCH 5/5] 1047330: Addressed review changes. --- src/Components/Charts/Chart/Annotations/ChartAnnotations.cs | 2 +- src/Components/Charts/Common/SvgComponents/SvgPath.razor | 2 +- src/Components/Charts/Common/SvgComponents/SvgPath.razor.cs | 2 +- src/Components/Charts/Common/SvgComponents/SvgRect.razor | 2 +- src/Components/Charts/Common/SvgComponents/SvgRect.razor.cs | 2 +- src/Components/Charts/Common/Utils/Helper.cs | 2 +- .../Components/Pages/Charts/Others/Annotation.razor | 2 +- .../Charts/Chart/Annotation/Annotation.razor | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Components/Charts/Chart/Annotations/ChartAnnotations.cs b/src/Components/Charts/Chart/Annotations/ChartAnnotations.cs index f482f23..4edb408 100644 --- a/src/Components/Charts/Chart/Annotations/ChartAnnotations.cs +++ b/src/Components/Charts/Chart/Annotations/ChartAnnotations.cs @@ -268,4 +268,4 @@ public override void AddRenderer(IChartElementRenderer renderer) } #endregion } -} \ No newline at end of file +} diff --git a/src/Components/Charts/Common/SvgComponents/SvgPath.razor b/src/Components/Charts/Common/SvgComponents/SvgPath.razor index c30de3b..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 076cc00..6cf8979 100644 --- a/src/Components/Charts/Common/SvgComponents/SvgPath.razor.cs +++ b/src/Components/Charts/Common/SvgComponents/SvgPath.razor.cs @@ -151,4 +151,4 @@ public partial class SvgPath 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 f6400f3..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 31d2e76..86e343d 100644 --- a/src/Components/Charts/Common/SvgComponents/SvgRect.razor.cs +++ b/src/Components/Charts/Common/SvgComponents/SvgRect.razor.cs @@ -174,4 +174,4 @@ public partial class SvgRect 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 7dd674d..1d1469a 100644 --- a/src/Components/Charts/Common/Utils/Helper.cs +++ b/src/Components/Charts/Common/Utils/Helper.cs @@ -163,4 +163,4 @@ internal static void EnsureValidRole(string? value, string 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 1141e93..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 @@ -71,4 +71,4 @@ font-size: 18px; font-weight: 600; } - \ No newline at end of file + 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 31d0b08..f2f3e04 100644 --- a/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Charts/Chart/Annotation/Annotation.razor +++ b/tests/Syncfusion.Blazor.Toolkit.BUnitTest/Charts/Chart/Annotation/Annotation.razor @@ -104,4 +104,4 @@ Assert.Equal("100", annotation.Y); Assert.NotNull(cut.Find("#container_Annotation_0")); } -} \ No newline at end of file +}