diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayBackgroundColor.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayBackgroundColor.razor
new file mode 100644
index 0000000000..050c6a99c0
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayBackgroundColor.razor
@@ -0,0 +1,18 @@
+
+ visible = !visible">Show Overlay
+
+
+
+
+
+@code {
+ private bool visible;
+
+ private void HandleOnClose()
+ {
+ Console.WriteLine("Custom background color overlay closed");
+ }
+}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayDefault.razor
new file mode 100644
index 0000000000..17043853aa
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayDefault.razor
@@ -0,0 +1,33 @@
+@using Microsoft.FluentUI.AspNetCore.Components.Extensions
+
+
+
+
+
+
+
+
+ visible = !visible">Show Overlay
+
+
+
+
+
+@code {
+ private bool visible;
+ private JustifyContent justification = JustifyContent.Center;
+ private HorizontalAlignment alignment = HorizontalAlignment.Center;
+
+ private void HandleOnClose()
+ {
+ Console.WriteLine("Overlay closed");
+ }
+}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayFullScreen.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayFullScreen.razor
new file mode 100644
index 0000000000..cb02b0911d
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayFullScreen.razor
@@ -0,0 +1,19 @@
+
+ visible = !visible">Show Overlay
+
+
+
+
+
+@code {
+ private bool visible;
+
+ private void HandleOnClose()
+ {
+ Console.WriteLine("Full screen overlay closed");
+ }
+}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayInteractive.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayInteractive.razor
new file mode 100644
index 0000000000..a11c1ac4ec
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayInteractive.razor
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+ visible = !visible">
+ Show Overlay
+
+
+
+ Increment
+ Counter: @counter
+
+
+
+ @if (interactive)
+ {
+
+
Non-interactive zone
+
+
+ }
+ else
+ {
+
+ }
+
+
+
+
+@code {
+ private bool visible;
+ private bool interactive = true;
+ private bool interactiveExceptId = true;
+ private bool fullScreen = true;
+ private int counter = 0;
+
+ private void HandleOnClose()
+ {
+ Console.WriteLine("Overlay closed");
+ }
+}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayTimed.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayTimed.razor
new file mode 100644
index 0000000000..36ecda22e6
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayTimed.razor
@@ -0,0 +1,33 @@
+@using Microsoft.FluentUI.AspNetCore.Components.Extensions
+
+
+
+
+
+
+
+Show Overlay
+
+
+
+
+
+@code {
+ private bool visible;
+ private JustifyContent justification = JustifyContent.Center;
+ private HorizontalAlignment alignment = HorizontalAlignment.Center;
+
+ private async Task HandleOnOpen()
+ {
+ visible = true;
+ Console.WriteLine("Overlay opened");
+ await Task.Delay(3000);
+ visible = false;
+ }
+}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayTransparent.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayTransparent.razor
new file mode 100644
index 0000000000..9a267145cd
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/Examples/OverlayTransparent.razor
@@ -0,0 +1,15 @@
+
+ visible = !visible">Show Overlay
+
+
+
+
+
+@code {
+ private bool visible;
+
+ private void HandleOnClose()
+ {
+ Console.WriteLine("Transparent overlay closed");
+ }
+}
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/FluentOverlay.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/FluentOverlay.md
new file mode 100644
index 0000000000..b4b4bd6450
--- /dev/null
+++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Overlay/FluentOverlay.md
@@ -0,0 +1,46 @@
+---
+title: Overlay
+route: /Overlay
+---
+
+# Overlay
+
+Overlays are used to temporarily overlay screen content to focus a dialog, progress or other information/interaction.
+
+## Default
+
+{{ OverlayDefault }}
+
+## Timed
+
+A timed overlay that hides after being shown for 3 seconds
+
+{{ OverlayTimed }}
+
+## Transparent overlay
+
+Overlay with a transparent background
+
+{{ OverlayTransparent }}
+
+## Background color
+
+Overlay with a custom background color
+
+{{ OverlayBackgroundColor }}
+
+## Full screen
+
+Overlay which takes up the whole screen.
+
+{{ OverlayFullScreen }}
+
+## Interactive
+By using the `Interactive` and `InteractiveExceptId` properties, only the targeted element will not close the FluentOverlay panel. The user can click anywhere else to close the FluentOverlay.
+In this example, the FluentOverlay will only close when the user clicks outside the white zone and the user can increment the counter before to close the Overlay.
+
+{{ OverlayInteractive }}
+
+## API FluentOverlay
+
+{{ API Type=FluentOverlay }}
diff --git a/src/Core/Components/Overlay/FluentOverlay.razor b/src/Core/Components/Overlay/FluentOverlay.razor
new file mode 100644
index 0000000000..045cc563a2
--- /dev/null
+++ b/src/Core/Components/Overlay/FluentOverlay.razor
@@ -0,0 +1,17 @@
+@namespace Microsoft.FluentUI.AspNetCore.Components
+@inherits FluentComponentBase
+
+@if (Visible)
+{
+
+}
diff --git a/src/Core/Components/Overlay/FluentOverlay.razor.cs b/src/Core/Components/Overlay/FluentOverlay.razor.cs
new file mode 100644
index 0000000000..5f6eb8b7ba
--- /dev/null
+++ b/src/Core/Components/Overlay/FluentOverlay.razor.cs
@@ -0,0 +1,229 @@
+// ------------------------------------------------------------------------
+// This file is licensed to you under the MIT License.
+// ------------------------------------------------------------------------
+
+using System.Globalization;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Web;
+using Microsoft.FluentUI.AspNetCore.Components.Extensions;
+using Microsoft.FluentUI.AspNetCore.Components.Utilities;
+using Microsoft.JSInterop;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+public partial class FluentOverlay : FluentComponentBase
+{
+ private const string JAVASCRIPT_FILE = FluentJSModule.JAVASCRIPT_ROOT + "Overlay/FluentOverlay.razor.js";
+ private DotNetObjectReference? _dotNetHelper;
+
+ ///
+ public FluentOverlay(LibraryConfiguration configuration) : base(configuration)
+ {
+ Id = Identifier.NewId();
+ }
+
+ ///
+ protected string? ClassValue => DefaultClassBuilder
+ .AddClass("fluent-overlay")
+ .AddClass("prevent-scroll", PreventScroll)
+ .Build();
+
+ ///
+ protected string? StyleValue => DefaultStyleBuilder
+ .AddStyle("cursor", "auto", () => Transparent)
+ .AddStyle("background-color", string.Create(CultureInfo.InvariantCulture, $"color-mix(in srgb, {BackgroundColor} {Opacity}%, transparent)"), () => !Transparent)
+ .AddStyle("cursor", "default", () => !Transparent)
+ .AddStyle("position", FullScreen ? "fixed" : "absolute")
+ .AddStyle("display", "flex")
+ .AddStyle("align-items", Alignment.ToAttributeValue())
+ .AddStyle("justify-content", Justification.ToAttributeValue())
+ .AddStyle("pointer-events", "none", () => Interactive)
+ .AddStyle("z-index", ZIndex.Overlay.ToString(CultureInfo.InvariantCulture))
+ .Build();
+
+ ///
+ protected string? StyleContentValue => new StyleBuilder()
+ .AddStyle("pointer-events", "auto", () => Interactive)
+ .Build();
+
+ ///
+ /// Gets or sets a value indicating whether the overlay is visible.
+ ///
+ [Parameter]
+ public bool Visible { get; set; }
+
+ ///
+ /// Callback for when overlay visibility changes.
+ ///
+ [Parameter]
+ public EventCallback VisibleChanged { get; set; }
+
+ ///
+ /// Callback for when the overlay is closed.
+ ///
+ [Parameter]
+ public EventCallback OnClose { get; set; }
+
+ ///
+ /// Gets or set if the overlay is transparent.
+ ///
+ [Parameter]
+ public bool Transparent { get; set; } = true;
+
+ ///
+ /// Gets or sets the opacity of the overlay.
+ /// Default is 40%.
+ ///
+ [Parameter]
+ public double? Opacity { get; set; } = 40;
+
+ ///
+ /// Gets or sets the alignment of the content to a value.
+ /// Defaults to Align.Center.
+ ///
+ [Parameter]
+ public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Center;
+
+ ///
+ /// Gets or sets the justification of the content to a value.
+ /// Defaults to JustifyContent.Center.
+ ///
+ [Parameter]
+ public JustifyContent Justification { get; set; } = JustifyContent.Center;
+
+ ///
+ /// Gets or sets a value indicating whether the overlay is shown full screen or bound to the containing element.
+ ///
+ [Parameter]
+ public bool FullScreen { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the overlay is interactive, except for the element with the specified .
+ /// In other words, the elements below the overlay remain usable (mouse-over, click) and the overlay will closed when clicked.
+ ///
+ [Parameter]
+ public bool Interactive { get; set; }
+
+ ///
+ /// Gets or sets the HTML identifier of the element that is not interactive when the overlay is shown.
+ /// This property is ignored if is false.
+ ///
+ [Parameter]
+ public string? InteractiveExceptId { get; set; }
+
+ ///
+ /// Gets of sets a value indicating if the overlay can be dismissed by clicking on it.
+ /// Default is true.
+ ///
+ [Parameter]
+ public bool Dismissable { get; set; } = true;
+
+ ///
+ /// Gets or sets the background color.
+ /// Default NeutralBaseColor token value (#808080).
+ ///
+ [Parameter]
+ public string BackgroundColor { get; set; } = "#808080";
+
+ ///
+ [Parameter]
+ public bool PreventScroll { get; set; }
+
+ ///
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+
+ ///
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (firstRender)
+ {
+ _dotNetHelper ??= DotNetObjectReference.Create(this);
+ await JSModule.ImportJavaScriptModuleAsync(JAVASCRIPT_FILE);
+ }
+ }
+
+ ///
+ protected override async Task OnParametersSetAsync()
+ {
+ if (Interactive && JSModule.Imported)
+ {
+ if (Visible)
+ {
+ // Add a document.addEventListener when Visible is true
+ await InvokeOverlayInitializeAsync();
+ }
+ else
+ {
+ // Remove a document.addEventListener when Visible is false
+ await InvokeOverlayDisposeAsync();
+ }
+ }
+ }
+
+ ///
+ [JSInvokable]
+ public async Task OnCloseInteractiveAsync(MouseEventArgs e)
+ {
+ if (!Dismissable || !Visible)
+ {
+ return;
+ }
+
+ // Remove the document.removeEventListener
+ await InvokeOverlayDisposeAsync();
+
+ // Close the overlay
+ await OnCloseInternalHandlerAsync(e);
+ }
+
+ ///
+ public async Task OnCloseHandlerAsync(MouseEventArgs e)
+ {
+ if (!Dismissable || !Visible || Interactive)
+ {
+ return;
+ }
+
+ // Close the overlay
+ await OnCloseInternalHandlerAsync(e);
+ }
+
+ private async Task OnCloseInternalHandlerAsync(MouseEventArgs e)
+ {
+ Visible = false;
+
+ if (VisibleChanged.HasDelegate)
+ {
+ await VisibleChanged.InvokeAsync(Visible);
+ }
+
+ if (OnClose.HasDelegate)
+ {
+ await OnClose.InvokeAsync(e);
+ }
+ }
+
+ ///
+ protected override async ValueTask DisposeAsync(IJSObjectReference jsModule)
+ {
+ await InvokeOverlayDisposeAsync();
+ }
+
+ ///
+ private async Task InvokeOverlayInitializeAsync()
+ {
+ var containerId = FullScreen ? null : Id;
+ await JSModule.ObjectReference.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Overlay.Initialize", _dotNetHelper, containerId, InteractiveExceptId);
+ }
+
+ ///
+ private async Task InvokeOverlayDisposeAsync()
+ {
+ if (JSModule.ObjectReference != null && Interactive)
+ {
+ await JSModule.ObjectReference.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Overlay.overlayDispose", InteractiveExceptId);
+ }
+ }
+}
diff --git a/src/Core/Components/Overlay/FluentOverlay.razor.css b/src/Core/Components/Overlay/FluentOverlay.razor.css
new file mode 100644
index 0000000000..61eea638fa
--- /dev/null
+++ b/src/Core/Components/Overlay/FluentOverlay.razor.css
@@ -0,0 +1,8 @@
+.fluent-overlay {
+ inset: 0px;
+ display: flex;
+ pointer-events: auto;
+ width: 100%;
+ height: 100%;
+ overflow: hidden auto;
+}
\ No newline at end of file
diff --git a/src/Core/Components/Overlay/FluentOverlay.razor.ts b/src/Core/Components/Overlay/FluentOverlay.razor.ts
new file mode 100644
index 0000000000..ffe5054241
--- /dev/null
+++ b/src/Core/Components/Overlay/FluentOverlay.razor.ts
@@ -0,0 +1,60 @@
+import { DotNet } from "../../../Core.Scripts/src/d-ts/Microsoft.JSInterop";
+
+export namespace Microsoft.FluentUI.Blazor.Overlay {
+ export function Initialize(dotNetHelper: DotNet.DotNetObject, containerId: string, id: string) {
+ const _document = document as any;
+
+ if (!_document.fluentOverlayData) {
+ _document.fluentOverlayData = {};
+ }
+
+ if (_document.fluentOverlayData[id]) {
+ return;
+ }
+
+ // Store the data
+ _document.fluentOverlayData[id] = {
+
+ // Click event handler
+ clickHandler: async function (event: MouseEvent) {
+ const containerElement = document.getElementById(containerId) as HTMLElement;
+ const isInsideContainer = isClickInsideContainer(event, containerElement);
+ const isInsideExcludedElement = !!containerElement && isClickInsideContainer(event, containerElement);
+
+ if (isInsideContainer && !isInsideExcludedElement) {
+ dotNetHelper.invokeMethodAsync('OnCloseInteractiveAsync', event);
+ }
+ }
+ };
+
+ // Let the user click on the container (containerId or the entire document)
+ document.addEventListener('click', _document.fluentOverlayData[id].clickHandler);
+ }
+ export function overlayDispose(id: string) {
+ const _document = document as any;
+ if (_document.fluentOverlayData[id]) {
+
+ // Remove the event listener
+ document.removeEventListener('click', _document.fluentOverlayData[id].clickHandler);
+
+ // Remove the data
+ _document.fluentOverlayData[id] = null;
+ delete _document.fluentOverlayData[id];
+ }
+ }
+ function isClickInsideContainer(event: MouseEvent, container: HTMLElement) {
+ if (!!container) {
+ const rect = container.getBoundingClientRect();
+
+ return (
+ event.clientX >= rect.left &&
+ event.clientX <= rect.right &&
+ event.clientY >= rect.top &&
+ event.clientY <= rect.bottom
+ );
+ }
+
+ // Default is true
+ return true;
+ }
+}
diff --git a/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj b/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj
index 0565cb189c..01a3d9630f 100644
--- a/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj
+++ b/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj
@@ -57,7 +57,6 @@
-
@@ -76,7 +75,7 @@
-
+
@@ -85,9 +84,9 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/src/Core/Utilities/ZIndex.cs b/src/Core/Utilities/ZIndex.cs
index ea6eb4830e..e9b11a1363 100644
--- a/src/Core/Utilities/ZIndex.cs
+++ b/src/Core/Utilities/ZIndex.cs
@@ -34,4 +34,9 @@ public static class ZIndex
/// ZIndex for the popup components in the .
///
public static int DataGridHeaderPopup { get; set; } = 5;
+
+ ///
+ /// ZIndex for the component.
+ ///
+ public static int Overlay { get; set; } = 99990;
}
diff --git a/tests/Core/Components/Base/ComponentBaseTests.cs b/tests/Core/Components/Base/ComponentBaseTests.cs
index c52cc60258..395c2892f1 100644
--- a/tests/Core/Components/Base/ComponentBaseTests.cs
+++ b/tests/Core/Components/Base/ComponentBaseTests.cs
@@ -52,6 +52,7 @@ public class ComponentBaseTests : Bunit.TestContext
{ typeof(FluentDragContainer<>), Loader.MakeGenericType(typeof(int))},
{ typeof(FluentDropZone<>), Loader.MakeGenericType(typeof(int))},
{ typeof(FluentTimePicker<>), Loader.MakeGenericType(typeof(DateTime))},
+ { typeof(FluentOverlay), Loader.Default.WithRequiredParameter("Visible", true)}
};
///
diff --git a/tests/Core/Components/Overlay/FluentOverlayTests.razor b/tests/Core/Components/Overlay/FluentOverlayTests.razor
new file mode 100644
index 0000000000..162989abe0
--- /dev/null
+++ b/tests/Core/Components/Overlay/FluentOverlayTests.razor
@@ -0,0 +1,33 @@
+@using Microsoft.FluentUI.AspNetCore.Components.Utilities
+@using Xunit;
+@using Microsoft.FluentUI.AspNetCore.Components.Tests.Samples;
+@inherits Bunit.TestContext
+@code
+{
+ public FluentOverlayTests()
+ {
+ JSInterop.Mode = JSRuntimeMode.Loose;
+ Services.AddFluentUIComponents();
+ }
+
+ [Fact]
+ public void FluentOverlay_Default()
+ {
+ // Arrange
+ bool onClosedRaised = false;
+
+ // Act
+ var cut = Render(@
+
+
+ );
+
+ cut.Find(".fluent-overlay").Click();
+
+ Assert.True(onClosedRaised);
+
+ // Assert
+ cut.Verify();
+ }
+}
+