diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/AppShell/BitAppShellJsRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/AppShell/BitAppShellJsRuntimeExtensions.cs index 6b9bf75064..e14d0a851e 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/AppShell/BitAppShellJsRuntimeExtensions.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/AppShell/BitAppShellJsRuntimeExtensions.cs @@ -6,21 +6,21 @@ internal static class BitAppShellJsRuntimeExtensions { internal static ValueTask BitAppShellInitScroll(this IJSRuntime jsRuntime, ElementReference container, string url) { - return jsRuntime.InvokeVoid("BitBlazorUI.AppShell.initScroll", container, url); + return jsRuntime.FastInvokeVoid("BitBlazorUI.AppShell.initScroll", container, url); } internal static ValueTask BitAppShellLocationChangedScroll(this IJSRuntime jsRuntime, string url) { - return jsRuntime.InvokeVoid("BitBlazorUI.AppShell.locationChangedScroll", url); + return jsRuntime.FastInvokeVoid("BitBlazorUI.AppShell.locationChangedScroll", url); } internal static ValueTask BitAppShellAfterRenderScroll(this IJSRuntime jsRuntime, string url) { - return jsRuntime.InvokeVoid("BitBlazorUI.AppShell.afterRenderScroll", url); + return jsRuntime.FastInvokeVoid("BitBlazorUI.AppShell.afterRenderScroll", url); } internal static ValueTask BitAppShellDisposeScroll(this IJSRuntime jsRuntime) { - return jsRuntime.InvokeVoid("BitBlazorUI.AppShell.disposeScroll"); + return jsRuntime.FastInvokeVoid("BitBlazorUI.AppShell.disposeScroll"); } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/Chart/BitChart.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/Chart/BitChart.razor.cs index d3d809f266..70de3e3154 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/Chart/BitChart.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/Chart/BitChart.razor.cs @@ -147,13 +147,21 @@ protected override async Task OnAfterRenderAsync(bool firstRender) await _js.BitChartJsSetupChart(Config); } + // Always signal completion on first render, matching the long-standing behavior: consumers may + // rely on SetupCompletedCallback firing once the component has rendered regardless of whether the + // chart setup itself succeeded (e.g. when Config is still null, or when interop was unavailable + // and the result was swallowed on the WebAssembly fast path). await SetupCompletedCallback.InvokeAsync(this); + return; } if (Config is not null) { - await _js.BitChartJsSetupChart(Config); + // Re-runs setup after a Config change. The readiness result is intentionally discarded here: + // SetupCompletedCallback is raised only once, on first render, so subsequent re-setups don't + // re-signal readiness. + _ = await _js.BitChartJsSetupChart(Config); } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/Chart/BitChart.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/Chart/BitChart.ts index f4eb9ac8e9..31096398ac 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/Chart/BitChart.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/Chart/BitChart.ts @@ -54,7 +54,7 @@ namespace BitBlazorUI { public static updateChart(config: BitChartConfiguration): boolean { if (!BitChart._bitCharts.has(config.canvasId)) - throw `Could not find a chart with the given id. ${config.canvasId}`; + throw `Could not find a chart with the given id: ${config.canvasId}`; let myChart = BitChart._bitCharts.get(config.canvasId); diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/Chart/JsInterop/BitChartJsInterop.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/Chart/JsInterop/BitChartJsInterop.cs index 391b21674f..98606e0fbc 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/Chart/JsInterop/BitChartJsInterop.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/Chart/JsInterop/BitChartJsInterop.cs @@ -22,7 +22,7 @@ internal static class BitChartJsInterop public static ValueTask BitChartJsRemoveChart(this IJSRuntime jsRuntime, string? canvasId) { - return jsRuntime.InvokeVoid("BitBlazorUI.BitChart.removeChart", canvasId); + return jsRuntime.FastInvokeVoid("BitBlazorUI.BitChart.removeChart", canvasId); } /// @@ -30,12 +30,15 @@ public static ValueTask BitChartJsRemoveChart(this IJSRuntime jsRuntime, string? /// /// /// The config for the new chart. - /// - public static ValueTask BitChartJsSetupChart(this IJSRuntime jsRuntime, BitChartConfigBase chartConfig) + /// + /// when setup succeeded, when the chart could not be updated in place, + /// or when interop could not run or an error was swallowed on the in-process (WASM) path. + /// + public static ValueTask BitChartJsSetupChart(this IJSRuntime jsRuntime, BitChartConfigBase chartConfig) { var dynParam = StripNulls(chartConfig); Dictionary param = ConvertExpandoObjectToDictionary(dynParam!); - return jsRuntime.Invoke("BitBlazorUI.BitChart.setupChart", param); + return jsRuntime.FastInvoke("BitBlazorUI.BitChart.setupChart", param); } /// @@ -43,12 +46,15 @@ public static ValueTask BitChartJsSetupChart(this IJSRuntime jsRuntime, Bi /// /// /// The updated config of the chart you want to update. - /// - public static ValueTask BitChartJsUpdateChart(this IJSRuntime jsRuntime, BitChartConfigBase chartConfig) + /// + /// when the chart was updated, when the chart instance was missing, + /// or when interop could not run or an error was swallowed on the in-process (WASM) path. + /// + public static ValueTask BitChartJsUpdateChart(this IJSRuntime jsRuntime, BitChartConfigBase chartConfig) { var dynParam = StripNulls(chartConfig); var param = ConvertExpandoObjectToDictionary(dynParam!); - return jsRuntime.Invoke("BitBlazorUI.BitChart.updateChart", param); + return jsRuntime.FastInvoke("BitBlazorUI.BitChart.updateChart", param); } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.ts index 1b8c80517a..01cdeb330d 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.ts @@ -44,7 +44,7 @@ namespace BitBlazorUI { colOptions.style.transform = `translateX(${applyOffset}px)`; } - colOptions.scrollIntoViewIfNeeded(); + colOptions.scrollIntoViewIfNeeded?.(); const autoFocusElem = colOptions.querySelector('[autofocus]'); if (autoFocusElem) { diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGridJsRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGridJsRuntimeExtensions.cs index 684418b1a8..a052ebf4b0 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGridJsRuntimeExtensions.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGridJsRuntimeExtensions.cs @@ -2,11 +2,22 @@ internal static class BitDataGridJsRuntimeExtensions { - public static async ValueTask BitDataGridInit(this IJSRuntime jsRuntime, ElementReference tableElement) + // FastInvoke returns default (null) when the runtime can't service interop or a JSON/JS interop + // error is swallowed on the in-process (WASM) path. Callers must null-check before using the + // reference; a null result means DataGrid JS hooks were not initialized. + public static async ValueTask BitDataGridInit(this IJSRuntime jsRuntime, ElementReference tableElement) { - return await jsRuntime.Invoke("BitBlazorUI.DataGrid.init", tableElement); + const string identifier = "BitBlazorUI.DataGrid.init"; + var result = await jsRuntime.FastInvoke(identifier, tableElement); + return jsRuntime.ReportIfUnexpectedNull(identifier, result); } + // This is a fire-and-forget call from OnAfterRenderAsync that runs DOM-heavy positioning logic + // (getBoundingClientRect, scrollIntoViewIfNeeded, focus). It deliberately uses the regular async + // invocation rather than FastInvokeVoid: on WebAssembly FastInvokeVoid runs synchronously and can + // alter Promise/ordering and error-propagation semantics, so we use the async Invoke pattern to keep + // any JS-side failure (e.g. scrollIntoViewIfNeeded being unsupported) contained within the returned + // task instead of letting it escape synchronously into the render loop. public static async ValueTask BitDataGridCheckColumnOptionsPosition(this IJSRuntime jsRuntime, ElementReference tableElement) { await jsRuntime.InvokeVoid("BitBlazorUI.DataGrid.checkColumnOptionsPosition", tableElement); diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/InfiniteScrolling/BitInfiniteScrollingJsRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/InfiniteScrolling/BitInfiniteScrollingJsRuntimeExtensions.cs index ecfcd131e9..0557a3c4fe 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/InfiniteScrolling/BitInfiniteScrollingJsRuntimeExtensions.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/InfiniteScrolling/BitInfiniteScrollingJsRuntimeExtensions.cs @@ -10,18 +10,18 @@ public static ValueTask BitInfiniteScrollingSetup(this IJSRuntime jsRuntime, decimal? threshold, DotNetObjectReference> dotnetObj) { - return jsRuntime.InvokeVoid("BitBlazorUI.InfiniteScrolling.setup", id, scrollerSelector, rootElement, lastElement, threshold, dotnetObj); + return jsRuntime.FastInvokeVoid("BitBlazorUI.InfiniteScrolling.setup", id, scrollerSelector, rootElement, lastElement, threshold, dotnetObj); } public static ValueTask BitInfiniteScrollingReobserve(this IJSRuntime jsRuntime, string id, ElementReference lastElement) { - return jsRuntime.InvokeVoid("BitBlazorUI.InfiniteScrolling.reobserve", id, lastElement); + return jsRuntime.FastInvokeVoid("BitBlazorUI.InfiniteScrolling.reobserve", id, lastElement); } public static ValueTask BitInfiniteScrollingDispose(this IJSRuntime jsRuntime, string id) { - return jsRuntime.InvokeVoid("BitBlazorUI.InfiniteScrolling.dispose", id); + return jsRuntime.FastInvokeVoid("BitBlazorUI.InfiniteScrolling.dispose", id); } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownViewer/BitMarkdownViewer.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownViewer/BitMarkdownViewer.ts index 1249277d6e..9ab7b4500f 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownViewer/BitMarkdownViewer.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownViewer/BitMarkdownViewer.ts @@ -5,6 +5,10 @@ namespace BitBlazorUI { } public static parse(md: string) { + // The `async: false` option MUST remain. This method is FastInvoked (FastInvoke) + // from the .NET side, which requires a synchronous string return. marked.parse returns a + // Promise when async is true, which would silently turn the FastInvoke call into a + // fire-and-forget with no test catching the regression. Use parseAsync for async needs. let html = marked.parse(md, { async: false }); return html; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownViewer/BitMarkdownViewerJsRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownViewer/BitMarkdownViewerJsRuntimeExtensions.cs index 1a6accf590..5bb13c6d94 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownViewer/BitMarkdownViewerJsRuntimeExtensions.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/MarkdownViewer/BitMarkdownViewerJsRuntimeExtensions.cs @@ -2,15 +2,19 @@ internal static class BitMarkdownViewerJsRuntimeExtensions { - public static ValueTask BitMarkdownViewerCheckScriptLoaded(this IJSRuntime jsRuntime, string script) + // FastInvoke returns null when the runtime can't service interop or a JSON/JS interop error is + // swallowed on the in-process (WASM) path. Nullable distinguishes that from a legitimate false. + public static ValueTask BitMarkdownViewerCheckScriptLoaded(this IJSRuntime jsRuntime, string script) { - return jsRuntime.FastInvoke("BitBlazorUI.MarkdownViewer.checkScriptLoaded", script); + return jsRuntime.FastInvoke("BitBlazorUI.MarkdownViewer.checkScriptLoaded", script); } - public static ValueTask BitMarkdownViewerParse(this IJSRuntime jsRuntime, string markdown, string? middleware) + // FastInvoke/Invoke return null when the runtime can't service interop or a JSON/JS interop error + // is swallowed on the in-process (WASM) path. Nullable surfaces that so call sites can coalesce. + public static ValueTask BitMarkdownViewerParse(this IJSRuntime jsRuntime, string markdown, string? middleware) { return OperatingSystem.IsBrowser() && middleware.HasNoValue() - ? jsRuntime.FastInvoke("BitBlazorUI.MarkdownViewer.parse", markdown) - : jsRuntime.Invoke("BitBlazorUI.MarkdownViewer.parseAsync", markdown, middleware); + ? jsRuntime.FastInvoke("BitBlazorUI.MarkdownViewer.parse", markdown) + : jsRuntime.Invoke("BitBlazorUI.MarkdownViewer.parseAsync", markdown, middleware); } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/PdfReader/BitPdfReaderJsRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/PdfReader/BitPdfReaderJsRuntimeExtensions.cs index f1e7c1eff2..d489b10eaa 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/PdfReader/BitPdfReaderJsRuntimeExtensions.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/PdfReader/BitPdfReaderJsRuntimeExtensions.cs @@ -9,16 +9,21 @@ public static ValueTask BitPdfReaderSetup(this IJSRuntime jsRuntime, BitPdf public static ValueTask BitPdfReaderRenderPage(this IJSRuntime jsRuntime, string id, int pageNumber) { + // The JS renderPage is async (awaits pdf.js page rendering). FastInvoke would use the + // synchronous in-process path in WASM, discarding the returned Promise (fire-and-forget), + // so callers would proceed/raise events before rendering completes and errors would be lost. return jsRuntime.InvokeVoid("BitBlazorUI.PdfReader.renderPage", id, pageNumber); } public static ValueTask BitPdfReaderRefreshPage(this IJSRuntime jsRuntime, BitPdfReaderConfig config, int pageNumber) { + // The JS refreshPage is async (awaits renderPage). See BitPdfReaderRenderPage for why + // the asynchronous invocation must be used instead of the synchronous fast-invoke. return jsRuntime.InvokeVoid("BitBlazorUI.PdfReader.refreshPage", config, pageNumber); } public static ValueTask BitPdfReaderDispose(this IJSRuntime jsRuntime, string id) { - return jsRuntime.InvokeVoid("BitBlazorUI.PdfReader.dispose", id); + return jsRuntime.FastInvokeVoid("BitBlazorUI.PdfReader.dispose", id); } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/PhoneInput/BitPhoneInput.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/PhoneInput/BitPhoneInput.razor.cs index baa5dc51a6..b1e2a710d8 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/PhoneInput/BitPhoneInput.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/PhoneInput/BitPhoneInput.razor.cs @@ -254,7 +254,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) if (_isOpen && _activeIndex >= 0 && _activeIndex != _lastScrolledIndex) { _lastScrolledIndex = _activeIndex; - await _js.BitExtrasScrollOptionIntoView(GetOptionId(_activeIndex)); + await _js.BitExtrasScrollElementIntoView(GetOptionId(_activeIndex)); } await base.OnAfterRenderAsync(firstRender); diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProModal/BitProModal.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProModal/BitProModal.razor.cs index 7995d58d1b..4c15ac6d63 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProModal/BitProModal.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProModal/BitProModal.razor.cs @@ -317,11 +317,11 @@ private async Task ToggleScroll(bool isOpen) if (ScrollerElement.HasValue) { - _offsetTop = await _js.BitUtilsToggleOverflow(ScrollerElement.Value, isOpen); + _offsetTop = await _js.BitUtilsToggleOverflow(ScrollerElement.Value, isOpen) ?? 0; } else { - _offsetTop = await _js.BitUtilsToggleOverflow(ScrollerSelector ?? "body", isOpen); + _offsetTop = await _js.BitUtilsToggleOverflow(ScrollerSelector ?? "body", isOpen) ?? 0; } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Extensions/JsInterop/ExtrasJsRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Extensions/JsInterop/ExtrasJsRuntimeExtensions.cs index 68a2da41d3..4f78091e41 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Extensions/JsInterop/ExtrasJsRuntimeExtensions.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Extensions/JsInterop/ExtrasJsRuntimeExtensions.cs @@ -4,17 +4,17 @@ internal static class ExtrasJsRuntimeExtensions { internal static ValueTask BitExtrasApplyRootClasses(this IJSRuntime jsRuntime, List cssClasses, Dictionary cssVariables) { - return jsRuntime.InvokeVoid("BitBlazorUI.Extras.applyRootClasses", cssClasses, cssVariables); + return jsRuntime.FastInvokeVoid("BitBlazorUI.Extras.applyRootClasses", cssClasses, cssVariables); } internal static ValueTask BitExtrasGoToTop(this IJSRuntime jsRuntime, ElementReference element, BitScrollBehavior? behavior = null) { - return jsRuntime.InvokeVoid("BitBlazorUI.Extras.goToTop", element, behavior?.ToString().ToLowerInvariant()); + return jsRuntime.FastInvokeVoid("BitBlazorUI.Extras.goToTop", element, behavior?.ToString().ToLowerInvariant()); } internal static ValueTask BitExtrasScrollBy(this IJSRuntime jsRuntime, ElementReference element, decimal x, decimal y) { - return jsRuntime.InvokeVoid("BitBlazorUI.Extras.scrollBy", element, x, y); + return jsRuntime.FastInvokeVoid("BitBlazorUI.Extras.scrollBy", element, x, y); } public static ValueTask BitExtrasInitScripts(this IJSRuntime jsRuntime, IEnumerable scripts, bool isModule = false) @@ -29,16 +29,16 @@ public static ValueTask BitExtrasInitStylesheets(this IJSRuntime jsRuntime, IEnu internal static ValueTask BitExtrasSetPreventKeys(this IJSRuntime jsRuntime, ElementReference element, string[] keys) { - return jsRuntime.InvokeVoid("BitBlazorUI.Extras.setPreventKeys", element, keys); + return jsRuntime.FastInvokeVoid("BitBlazorUI.Extras.setPreventKeys", element, keys); } internal static ValueTask BitExtrasDisposePreventKeys(this IJSRuntime jsRuntime, ElementReference element) { - return jsRuntime.InvokeVoid("BitBlazorUI.Extras.disposePreventKeys", element); + return jsRuntime.FastInvokeVoid("BitBlazorUI.Extras.disposePreventKeys", element); } - internal static ValueTask BitExtrasScrollOptionIntoView(this IJSRuntime jsRuntime, string optionId) + internal static ValueTask BitExtrasScrollElementIntoView(this IJSRuntime jsRuntime, string elementId) { - return jsRuntime.InvokeVoid("BitBlazorUI.Extras.scrollOptionIntoView", optionId); + return jsRuntime.FastInvokeVoid("BitBlazorUI.Extras.scrollElementIntoView", elementId); } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Scripts/Extras.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Scripts/Extras.ts index f5f215e628..5997f3f255 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Scripts/Extras.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Scripts/Extras.ts @@ -18,7 +18,7 @@ namespace BitBlazorUI { element.scrollBy(x, y); } - + // Attaches (or updates) a deterministic keydown listener that calls preventDefault // for the provided keys. Unlike Blazor's `@onkeydown:preventDefault` binding -- whose // value is evaluated at render time and therefore only applies to the *next* key event @@ -52,93 +52,240 @@ namespace BitBlazorUI { delete el.bitPreventKeys; } - // Scrolls the option element into the visible area of its scroll container using + // Scrolls the element into the visible area of its scroll container using // 'nearest' so keyboard navigation keeps the active item on screen with minimal movement. - public static scrollOptionIntoView(optionId: string) { - if (!optionId) return; + public static scrollElementIntoView(elementId: string) { + if (!elementId) return; - const element = document.getElementById(optionId); + const element = document.getElementById(elementId); if (!element) return; try { element.scrollIntoView({ block: 'nearest', inline: 'nearest' }); - } catch (e) { console.error('BitBlazorUI.Extras.scrollOptionIntoView:', e); } + } catch (e) { console.error('BitBlazorUI.Extras.scrollElementIntoView:', e); } } - - private static _initScriptsPromises: { [key: string]: Promise } = {}; + public static async initScripts(scripts: string[], isModule: boolean) { - const key = scripts.join('|'); - if (Extras._initScriptsPromises[key] !== undefined) { - return Extras._initScriptsPromises[key]; + // Resolve only when every script has actually executed. Loading is tracked per-url so that + // concurrent callers (e.g. several components, or a re-mount) await the same execution instead + // of a second caller seeing the