From 932c1d518e7c4b02a19bb26d19f48c57c28baa5c Mon Sep 17 00:00:00 2001 From: Denis VOITURON Date: Thu, 6 Aug 2026 16:00:34 +0200 Subject: [PATCH 1/4] Implement Fluent UI Web Components override for Blazor integration --- .../src/FluentUIWebComponentsOverride.ts | 37 +++++++++++++++++++ src/Core.Scripts/src/Startup.ts | 5 +++ src/Core.Scripts/src/d-ts/Blazor.d.ts | 4 ++ 3 files changed, 46 insertions(+) create mode 100644 src/Core.Scripts/src/FluentUIWebComponentsOverride.ts diff --git a/src/Core.Scripts/src/FluentUIWebComponentsOverride.ts b/src/Core.Scripts/src/FluentUIWebComponentsOverride.ts new file mode 100644 index 0000000000..516559f0e4 --- /dev/null +++ b/src/Core.Scripts/src/FluentUIWebComponentsOverride.ts @@ -0,0 +1,37 @@ + +/// + +export namespace Microsoft.FluentUI.Override { + + /** + * Override the default behavior of the Fluent UI Web Components to use Blazor's features. + */ + export function overrideComponents() { + + /* Original code: https://github.com/microsoft/fluentui/blob/master/packages/web-components/src/anchor-button/anchor-button.base.ts + + private handleNavigation(newTab: boolean): void { + newTab ? window.open(this.href, '_blank') : this.internalProxyAnchor.click(); + } + */ + + customElements.whenDefined('fluent-anchor-button').then(() => { + const proto = customElements.get('fluent-anchor-button').prototype; + proto.handleNavigation = function (newTab: boolean): void { + if (newTab) { + window.open(this.href, '_blank'); + return; + } + + const url = new URL(this.href, document.baseURI); + if (url.origin === location.origin) { + Blazor.navigateTo(url.pathname + url.search + url.hash, false); + } + else { + this.internalProxyAnchor.click() + //location.href = this.href; + } + }; + }); + } +} \ No newline at end of file diff --git a/src/Core.Scripts/src/Startup.ts b/src/Core.Scripts/src/Startup.ts index 0c301eeace..3568fc0bde 100644 --- a/src/Core.Scripts/src/Startup.ts +++ b/src/Core.Scripts/src/Startup.ts @@ -1,6 +1,7 @@ import { Microsoft as LoggerFile } from './Utilities/Logger'; import { Microsoft as ThemeFile } from './Utilities/Theme'; import { Microsoft as FluentUIComponentsFile } from './FluentUIWebComponents'; +import { Microsoft as FluentUIWebComponentsOverrideFile } from './FluentUIWebComponentsOverride'; import { Microsoft as FluentPageScriptFile } from './Components/PageScript/FluentPageScript'; import { Microsoft as FluentPopoverFile } from './Components/Popover/FluentPopover'; import { Microsoft as FluentOverlayFile } from './Components/Overlay/FluentOverlay'; @@ -16,6 +17,7 @@ export namespace Microsoft.FluentUI.Blazor.Startup { // Alias import Logger = LoggerFile.FluentUI.Blazor.Utilities.Logger; import FluentUIComponents = FluentUIComponentsFile.FluentUI.Blazor.FluentUIWebComponents; + import FluentUIOverride = FluentUIWebComponentsOverrideFile.FluentUI.Override; import FluentPageScript = FluentPageScriptFile.FluentUI.Blazor.Components.PageScript; import FluentPopover = FluentPopoverFile.FluentUI.Blazor.Components.Popover; import FluentOverlay = FluentOverlayFile.FluentUI.Blazor.Components.Overlay; @@ -42,6 +44,9 @@ export namespace Microsoft.FluentUI.Blazor.Startup { // Define Fluent UI components FluentUIComponents.defineComponents(); + // Override Fluent UI components to use Blazor's features + FluentUIOverride.overrideComponents(); + // Finishing beforeStartCalled = true; } diff --git a/src/Core.Scripts/src/d-ts/Blazor.d.ts b/src/Core.Scripts/src/d-ts/Blazor.d.ts index 82a2650f6a..339adead19 100644 --- a/src/Core.Scripts/src/d-ts/Blazor.d.ts +++ b/src/Core.Scripts/src/d-ts/Blazor.d.ts @@ -12,6 +12,8 @@ interface ThemeSettings { interface Blazor { addEventListener?: (name: string, callback: (event: any) => void) => void; + navigateTo: (uri: string, options: NavigationOptions | boolean) => void; + registerCustomEventType(eventName: string, options: EventTypeOptions): void; theme: { @@ -41,3 +43,5 @@ interface Blazor { switchDirection(): void, } } + +declare const Blazor: Blazor; From 504f75695f8eaf68e553275cdb5c7417adc437a8 Mon Sep 17 00:00:00 2001 From: Denis VOITURON Date: Thu, 6 Aug 2026 17:06:18 +0200 Subject: [PATCH 2/4] Refactor handleNavigation in fluent-anchor-button to integrate Blazor navigation --- .../src/FluentUIWebComponentsOverride.ts | 50 ++++++++++++++----- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/src/Core.Scripts/src/FluentUIWebComponentsOverride.ts b/src/Core.Scripts/src/FluentUIWebComponentsOverride.ts index 516559f0e4..a2ed5e5606 100644 --- a/src/Core.Scripts/src/FluentUIWebComponentsOverride.ts +++ b/src/Core.Scripts/src/FluentUIWebComponentsOverride.ts @@ -2,21 +2,25 @@ /// export namespace Microsoft.FluentUI.Override { - /** * Override the default behavior of the Fluent UI Web Components to use Blazor's features. */ export function overrideComponents() { - /* Original code: https://github.com/microsoft/fluentui/blob/master/packages/web-components/src/anchor-button/anchor-button.base.ts + /** + * fluent-anchor-button + */ + waitForCustomElements().then(async registry => { + const anchorButton = await registry.whenDefined('fluent-anchor-button'); + const proto = anchorButton.prototype; - private handleNavigation(newTab: boolean): void { - newTab ? window.open(this.href, '_blank') : this.internalProxyAnchor.click(); - } - */ + /* + Original code: https://github.com/microsoft/fluentui/blob/master/packages/web-components/src/anchor-button/anchor-button.base.ts - customElements.whenDefined('fluent-anchor-button').then(() => { - const proto = customElements.get('fluent-anchor-button').prototype; + private handleNavigation(newTab: boolean): void { + newTab ? window.open(this.href, '_blank') : this.internalProxyAnchor.click(); + } + */ proto.handleNavigation = function (newTab: boolean): void { if (newTab) { window.open(this.href, '_blank'); @@ -25,13 +29,35 @@ export namespace Microsoft.FluentUI.Override { const url = new URL(this.href, document.baseURI); if (url.origin === location.origin) { - Blazor.navigateTo(url.pathname + url.search + url.hash, false); + const forceLoad = this.getAttribute('force-load') === 'true'; + console.log(`fluent-anchor-button handleNavigation: navigating to ${url.pathname + url.search + url.hash} using Blazor.navigateTo with forceLoad=${forceLoad}`); + Blazor.navigateTo(url.pathname + url.search + url.hash, forceLoad); } - else { - this.internalProxyAnchor.click() - //location.href = this.href; + else { + console.log(`fluent-anchor-button handleNavigation: navigating to ${this.href} using internalProxyAnchor.click()`); + this.internalProxyAnchor.click(); } }; }); } + + /** + * Wait for the customElements registry to be available before overriding the components. + * This is necessary because the Fluent UI Web Components are defined asynchronously. + * @returns A promise that resolves with the customElements registry. + */ + function waitForCustomElements(): Promise { + return new Promise(resolve => { + const checkCustomElements = () => { + if (typeof customElements !== 'undefined') { + resolve(customElements); + return; + } + + setTimeout(checkCustomElements, 10); + }; + + checkCustomElements(); + }); + } } \ No newline at end of file From 596967b96d44c136f900b027855691ed91152154 Mon Sep 17 00:00:00 2001 From: Denis VOITURON Date: Thu, 6 Aug 2026 17:29:46 +0200 Subject: [PATCH 3/4] Add ForceLoad parameter to FluentAnchorButton and FluentLink for full page navigation --- .../src/FluentUIWebComponentsOverride.ts | 66 +++++++++++-------- .../Button/FluentAnchorButton.razor | 1 + .../Button/FluentAnchorButton.razor.cs | 7 ++ src/Core/Components/Link/FluentLink.razor | 1 + src/Core/Components/Link/FluentLink.razor.cs | 7 ++ 5 files changed, 54 insertions(+), 28 deletions(-) diff --git a/src/Core.Scripts/src/FluentUIWebComponentsOverride.ts b/src/Core.Scripts/src/FluentUIWebComponentsOverride.ts index a2ed5e5606..a81180b399 100644 --- a/src/Core.Scripts/src/FluentUIWebComponentsOverride.ts +++ b/src/Core.Scripts/src/FluentUIWebComponentsOverride.ts @@ -2,45 +2,55 @@ /// export namespace Microsoft.FluentUI.Override { + interface NavigationElement extends HTMLElement { + href: string; + internalProxyAnchor: { click(): void }; + handleNavigation(newTab: boolean): void; + } + /** * Override the default behavior of the Fluent UI Web Components to use Blazor's features. */ export function overrideComponents() { - - /** - * fluent-anchor-button - */ waitForCustomElements().then(async registry => { - const anchorButton = await registry.whenDefined('fluent-anchor-button'); - const proto = anchorButton.prototype; - - /* - Original code: https://github.com/microsoft/fluentui/blob/master/packages/web-components/src/anchor-button/anchor-button.base.ts - private handleNavigation(newTab: boolean): void { - newTab ? window.open(this.href, '_blank') : this.internalProxyAnchor.click(); - } + /** + * fluent-anchor-button */ - proto.handleNavigation = function (newTab: boolean): void { - if (newTab) { - window.open(this.href, '_blank'); - return; - } + const anchorButton = await registry.whenDefined('fluent-anchor-button'); + anchorButton.prototype.handleNavigation = handleNavigation; - const url = new URL(this.href, document.baseURI); - if (url.origin === location.origin) { - const forceLoad = this.getAttribute('force-load') === 'true'; - console.log(`fluent-anchor-button handleNavigation: navigating to ${url.pathname + url.search + url.hash} using Blazor.navigateTo with forceLoad=${forceLoad}`); - Blazor.navigateTo(url.pathname + url.search + url.hash, forceLoad); - } - else { - console.log(`fluent-anchor-button handleNavigation: navigating to ${this.href} using internalProxyAnchor.click()`); - this.internalProxyAnchor.click(); - } - }; + /** + * fluent-link + */ + const link = await registry.whenDefined('fluent-link'); + link.prototype.handleNavigation = handleNavigation; }); } + /* + Original code: https://github.com/microsoft/fluentui/blob/master/packages/web-components/src/anchor-button/anchor-button.base.ts + + private handleNavigation(newTab: boolean): void { + newTab ? window.open(this.href, '_blank') : this.internalProxyAnchor.click(); + } + */ + function handleNavigation(this: NavigationElement, newTab: boolean): void { + if (newTab) { + window.open(this.href, '_blank'); + return; + } + + const url = new URL(this.href, document.baseURI); + if (url.origin === location.origin) { + const forceLoad = this.getAttribute('force-load') === 'true'; + Blazor.navigateTo(url.pathname + url.search + url.hash, forceLoad); + } + else { + this.internalProxyAnchor.click(); + } + } + /** * Wait for the customElements registry to be available before overriding the components. * This is necessary because the Fluent UI Web Components are defined asynchronously. diff --git a/src/Core/Components/Button/FluentAnchorButton.razor b/src/Core/Components/Button/FluentAnchorButton.razor index c6a403e8e1..3cd12fe1b6 100644 --- a/src/Core/Components/Button/FluentAnchorButton.razor +++ b/src/Core/Components/Button/FluentAnchorButton.razor @@ -19,6 +19,7 @@ name="@Name" role="@(string.IsNullOrEmpty(Title) ? null : "link")" aria-label="@Title" + force-load="@(ForceLoad ? "true" : null)" @attributes="@AdditionalAttributes"> @if (IconStart != null) { diff --git a/src/Core/Components/Button/FluentAnchorButton.razor.cs b/src/Core/Components/Button/FluentAnchorButton.razor.cs index 57d5441111..aa76468d31 100644 --- a/src/Core/Components/Button/FluentAnchorButton.razor.cs +++ b/src/Core/Components/Button/FluentAnchorButton.razor.cs @@ -148,6 +148,13 @@ public FluentAnchorButton(LibraryConfiguration configuration) : base(configurati [Parameter] public string? Tooltip { get; set; } + /// + /// Gets or sets a value indicating whether the navigation should force a full page load instead of using Blazor's built-in navigation. + /// Default is , which uses Blazor's navigation. + /// + [Parameter] + public bool ForceLoad { get; set; } + /// protected override async Task OnInitializedAsync() { diff --git a/src/Core/Components/Link/FluentLink.razor b/src/Core/Components/Link/FluentLink.razor index da0abf1617..b6d177afa6 100644 --- a/src/Core/Components/Link/FluentLink.razor +++ b/src/Core/Components/Link/FluentLink.razor @@ -14,6 +14,7 @@ class="@ClassValue" style="@StyleValue" clickable="@OnClick.HasDelegate" + force-load="@(ForceLoad ? "true" : null)" @onclick="@OnClickHandlerAsync" @attributes="AdditionalAttributes"> @if (IconStart is not null) diff --git a/src/Core/Components/Link/FluentLink.razor.cs b/src/Core/Components/Link/FluentLink.razor.cs index a06a3ac7c7..3fc3c1ced7 100644 --- a/src/Core/Components/Link/FluentLink.razor.cs +++ b/src/Core/Components/Link/FluentLink.razor.cs @@ -102,6 +102,13 @@ public FluentLink(LibraryConfiguration configuration) : base(configuration) { } [Parameter] public string? Tooltip { get; set; } + /// + /// Gets or sets a value indicating whether the navigation should force a full page load instead of using Blazor's built-in navigation. + /// Default is , which uses Blazor's navigation. + /// + [Parameter] + public bool ForceLoad { get; set; } + /// protected override async Task OnInitializedAsync() { From 2d84c38f2bbe64eac5892b4bd0c89885e1ea44de Mon Sep 17 00:00:00 2001 From: Denis VOITURON Date: Thu, 6 Aug 2026 17:35:00 +0200 Subject: [PATCH 4/4] Add ForceLoad attribute tests for FluentAnchorButton and FluentLink components --- .../Components/Button/FluentAnchorButtonTests.razor | 13 +++++++++++++ tests/Core/Components/Link/FluentLinkTests.razor | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/tests/Core/Components/Button/FluentAnchorButtonTests.razor b/tests/Core/Components/Button/FluentAnchorButtonTests.razor index 60b1a1595b..d0ed9fdc69 100644 --- a/tests/Core/Components/Button/FluentAnchorButtonTests.razor +++ b/tests/Core/Components/Button/FluentAnchorButtonTests.razor @@ -33,6 +33,19 @@ cut.Verify(); } + [Theory] + [InlineData(false, null)] + [InlineData(true, "true")] + public void FluentAnchorButton_ForceLoadAttribute(bool forceLoad, string? expectedValue) + { + // Arrange && Act + var cut = Render(@fluent-anchor-button); + + // Assert + var attribute = cut.Find("fluent-anchor-button").GetAttribute("force-load"); + Assert.Equal(expectedValue, attribute); + } + [Theory] [InlineData(LinkTarget.Blank, "_blank")] [InlineData(LinkTarget.Parent, "_parent")] diff --git a/tests/Core/Components/Link/FluentLinkTests.razor b/tests/Core/Components/Link/FluentLinkTests.razor index b19bfa7d27..841563a69d 100644 --- a/tests/Core/Components/Link/FluentLinkTests.razor +++ b/tests/Core/Components/Link/FluentLinkTests.razor @@ -45,6 +45,19 @@ cut.Verify(); } + [Theory] + [InlineData(false, null)] + [InlineData(true, "true")] + public void FluentLink_ForceLoadAttribute(bool forceLoad, string? expectedValue) + { + // Arrange && Act + var cut = Render(@fluent-link); + + // Assert + var attribute = cut.Find("fluent-link").GetAttribute("force-load"); + Assert.Equal(expectedValue, attribute); + } + [Fact] public void FluentLink_hreflang() {