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..f26509ea41 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/Chart/BitChart.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/Chart/BitChart.razor.cs @@ -27,9 +27,13 @@ public partial class BitChart : IAsyncDisposable [Parameter] public int? Height { get; set; } /// - /// This event is fired when the chart has been setup through interop and - /// the JavaScript chart object is available. Use this callback if you need to setup - /// custom JavaScript options or register plugins. + /// This event is fired once, on the component's first render, after the interop setup attempt has run. + /// It fires unconditionally - regardless of whether was set or the chart setup + /// actually succeeded - so treat it as a "first render completed" signal rather than a guarantee that the + /// JavaScript chart object is available. Note that the chart setup (BitChartJsSetupChart) has + /// already been attempted by the time this callback fires, so it is not a suitable place to register + /// plugins or custom JavaScript options that need to be present during setup. Any such registration must + /// happen before first render (i.e. before is assigned). /// [Parameter] public EventCallback SetupCompletedCallback { get; set; } @@ -147,13 +151,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..e868fecb69 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 new Error(`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..c591f6edb0 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,16 @@ public static ValueTask BitChartJsRemoveChart(this IJSRuntime jsRuntime, string? /// /// /// The config for the new chart. - /// - public static ValueTask BitChartJsSetupChart(this IJSRuntime jsRuntime, BitChartConfigBase chartConfig) + /// + /// when the chart was set up (or updated in place when one with the same id already existed). + /// The underlying BitBlazorUI.BitChart.setupChart throws on failure rather than returning ; + /// on the in-process (WASM) fast path that error can be swallowed, in which case the result is . + /// + 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 +47,16 @@ 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. The underlying BitBlazorUI.BitChart.updateChart throws + /// when no chart with the given id exists rather than returning ; on the in-process (WASM) + /// fast path that error can be swallowed, in which case the result is . + /// + 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..8e953c9c0d 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.ts @@ -44,7 +44,13 @@ namespace BitBlazorUI { colOptions.style.transform = `translateX(${applyOffset}px)`; } - colOptions.scrollIntoViewIfNeeded(); + if (typeof colOptions.scrollIntoViewIfNeeded === 'function') { + colOptions.scrollIntoViewIfNeeded(); + } else { + // Non-standard scrollIntoViewIfNeeded is unavailable in some browsers; fall back to the + // standard scrollIntoView with nearest alignment so the popup still stays in view. + colOptions.scrollIntoView({ block: 'nearest', inline: 'nearest' }); + } 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/BitInfiniteScrolling.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/InfiniteScrolling/BitInfiniteScrolling.ts index 417bb8541f..a3e940dc88 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/InfiniteScrolling/BitInfiniteScrolling.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/InfiniteScrolling/BitInfiniteScrolling.ts @@ -23,7 +23,13 @@ namespace BitBlazorUI { for (const entry of entries) { if (entry.isIntersecting) { observer.unobserve(lastElement); - await dotnetObj.invokeMethodAsync("Load"); + try { + await dotnetObj.invokeMethodAsync("Load"); + } catch (e) { + // Swallow the rejection so it doesn't surface as an unhandled promise rejection. + // The .NET object can already be disposed (component torn down) or the circuit gone. + console.error('BitBlazorUI.InfiniteScrolling.setup:', e); + } } } }, { 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/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 7f4cdf04ec..16408186b7 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/PhoneInput/BitPhoneInput.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/PhoneInput/BitPhoneInput.razor.cs @@ -367,7 +367,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 3129bb4e8c..37a0ee7d15 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProModal/BitProModal.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProModal/BitProModal.razor.cs @@ -440,11 +440,11 @@ private async Task ToggleScroll(bool isOpen) if (_scrollerElementOnOpen.HasValue) { - _offsetTop = await _js.BitUtilsToggleOverflow(_scrollerElementOnOpen.Value, isOpen); + _offsetTop = await _js.BitUtilsToggleOverflow(_scrollerElementOnOpen.Value, isOpen) ?? 0; } else { - _offsetTop = await _js.BitUtilsToggleOverflow(_scrollerSelectorOnOpen ?? "body", isOpen); + _offsetTop = await _js.BitUtilsToggleOverflow(_scrollerSelectorOnOpen ?? "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..7d14399878 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,299 @@ 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