From 6e1b7858dfcca682eaff4c0b1f5cb0c2a59eafdc Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Fri, 30 Aug 2024 17:55:47 +0200 Subject: [PATCH] [Overlay] Add FullScreenInteractiveExceptElementId (#2580) --- ...crosoft.FluentUI.AspNetCore.Components.xml | 52 ++++++- .../Overlay/Examples/OverlayFullScreen.razor | 2 +- .../Overlay/Examples/OverlayInteractive.razor | 63 ++++++++ .../Shared/Pages/Overlay/OverlayPage.razor | 12 +- .../Demo/Shared/Pages/RawEventHandlers.razor | 2 +- .../DateTime/FluentDatePicker.razor | 3 +- .../DateTime/FluentDatePicker.razor.cs | 3 + .../Components/Overlay/FluentOverlay.razor | 9 +- .../Components/Overlay/FluentOverlay.razor.cs | 137 +++++++++++++++++- .../Components/Overlay/FluentOverlay.razor.js | 68 +++++++++ ...ete_Keyboard-ArrowDown.verified.razor.html | 14 +- ...plete_Keyboard-ArrowUp.verified.razor.html | 6 +- ...ete_Keyboard-Backspace.verified.razor.html | 6 +- ...e_MaxAutoHeight_Opened.verified.razor.html | 14 +- ...te_OnClear_ShowOverlay.verified.razor.html | 8 +- ...entAutocomplete_Opened.verified.razor.html | 8 +- ...mplete_SelectedOptions.verified.razor.html | 14 +- ...Options_OnDismissClick.verified.razor.html | 6 +- ...lectedOptions_Template.verified.razor.html | 6 +- ...Autocomplete_Templates.verified.razor.html | 8 +- ...plete_ValueText_Clears.verified.razor.html | 8 +- ...ProfileMenu_Customized.verified.razor.html | 6 +- ...entProfileMenu_Default.verified.razor.html | 6 +- .../Core/_ToDo/Overlay/FluentOverlayTests.cs | 8 + 24 files changed, 390 insertions(+), 79 deletions(-) create mode 100644 examples/Demo/Shared/Pages/Overlay/Examples/OverlayInteractive.razor create mode 100644 src/Core/Components/Overlay/FluentOverlay.razor.js diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml index 648d2f4e79..d2911222f3 100644 --- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml +++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml @@ -2765,6 +2765,9 @@ + + + Gets or sets the design of this input. @@ -7332,12 +7335,29 @@ + + + + + + + + + + + + + + + Gets or sets the unique identifier of the overlay. + + Gets or sets a value indicating whether the overlay is visible. @@ -7345,7 +7365,7 @@ - Callback for when overlay visisbility changes. + Callback for when overlay visibility changes. @@ -7380,6 +7400,24 @@ Gets or sets a value indicating whether the overlay is shown full screen or bound to the containing element. + + + 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. + + + + + 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. + + + + + Gets of sets a value indicating if the overlay can be dismissed by clicking on it. + Default is true. + + Gets or sets the background color. @@ -7387,6 +7425,18 @@ Default is '#ffffff'. + + + Disposes the overlay. + + + + + + + + + Pattern:
diff --git a/examples/Demo/Shared/Pages/Overlay/Examples/OverlayFullScreen.razor b/examples/Demo/Shared/Pages/Overlay/Examples/OverlayFullScreen.razor index b8553d4b11..01322bc4b2 100644 --- a/examples/Demo/Shared/Pages/Overlay/Examples/OverlayFullScreen.razor +++ b/examples/Demo/Shared/Pages/Overlay/Examples/OverlayFullScreen.razor @@ -2,7 +2,7 @@ Show Overlay + + + + + + + Show Overlay + + + + Increment + Counter: @counter + + + + @if (interactive) + { +
+

Non-interactive zone

+ +
+ } + else + { + + } +
+ + + +@code { + bool visible = false; + bool interactive = true; + bool interactiveExceptId = true; + bool fullScreen = true; + int counter = 0; + + protected void HandleOnClose() + { + DemoLogger.WriteLine("Overlay closed"); + } +} diff --git a/examples/Demo/Shared/Pages/Overlay/OverlayPage.razor b/examples/Demo/Shared/Pages/Overlay/OverlayPage.razor index 0e9d0f9512..1d27a6ba78 100644 --- a/examples/Demo/Shared/Pages/Overlay/OverlayPage.razor +++ b/examples/Demo/Shared/Pages/Overlay/OverlayPage.razor @@ -10,7 +10,7 @@

Examples

- + Overlay with a default white background @@ -27,7 +27,15 @@ - Overlay which takes up the whole screen + Overlay which takes up the whole screen. + + + + + 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.
+

Documentation

diff --git a/examples/Demo/Shared/Pages/RawEventHandlers.razor b/examples/Demo/Shared/Pages/RawEventHandlers.razor index 0003fd5805..a80e03999f 100644 --- a/examples/Demo/Shared/Pages/RawEventHandlers.razor +++ b/examples/Demo/Shared/Pages/RawEventHandlers.razor @@ -17,7 +17,7 @@

Menu component

- Menu item 1 + Menu item 1 @foreach (var item in _menuComponentItems) diff --git a/src/Core/Components/DateTime/FluentDatePicker.razor b/src/Core/Components/DateTime/FluentDatePicker.razor index b21fa51717..f04b1c41c6 100644 --- a/src/Core/Components/DateTime/FluentDatePicker.razor +++ b/src/Core/Components/DateTime/FluentDatePicker.razor @@ -23,8 +23,9 @@ @if (Opened) { - + + private string PopupId => $"{Id}-popup"; + /// /// Gets or sets the design of this input. /// diff --git a/src/Core/Components/Overlay/FluentOverlay.razor b/src/Core/Components/Overlay/FluentOverlay.razor index 838c5487fa..a13bbbc20b 100644 --- a/src/Core/Components/Overlay/FluentOverlay.razor +++ b/src/Core/Components/Overlay/FluentOverlay.razor @@ -4,10 +4,11 @@ {
-
+ id="@Id" + @onclick="@OnCloseHandlerAsync" + @oncontextmenu="@OnCloseHandlerAsync" + @oncontextmenu:preventDefault="true"> +
@ChildContent
diff --git a/src/Core/Components/Overlay/FluentOverlay.razor.cs b/src/Core/Components/Overlay/FluentOverlay.razor.cs index c38206f182..2d121aeeab 100644 --- a/src/Core/Components/Overlay/FluentOverlay.razor.cs +++ b/src/Core/Components/Overlay/FluentOverlay.razor.cs @@ -2,16 +2,33 @@ using System.Text.RegularExpressions; 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 +public partial class FluentOverlay : IAsyncDisposable { + private readonly string _defaultId = Identifier.NewId(); private string? _color = null; private int _r, _g, _b; + private const string JAVASCRIPT_FILE = "./_content/Microsoft.FluentUI.AspNetCore.Components/Components/Overlay/FluentOverlay.razor.js"; + private DotNetObjectReference? _dotNetHelper = null; + + /// + [Inject] + private LibraryConfiguration LibraryConfiguration { get; set; } = default!; + + /// + [Inject] + private IJSRuntime JSRuntime { get; set; } = default!; + + /// + private IJSObjectReference? _jsModule { get; set; } + /// protected string? ClassValue => new CssBuilder("fluent-overlay") .AddClass("prevent-scroll", PreventScroll) @@ -22,11 +39,25 @@ public partial class FluentOverlay .AddStyle("cursor", "auto", () => Transparent) .AddStyle("background-color", $"rgba({_r}, {_g}, {_b}, {Opacity.ToString()!.Replace(',', '.')})", () => !Transparent) .AddStyle("cursor", "default", () => !Transparent) - .AddStyle("position", "fixed", () => FullScreen) - .AddStyle("position", "absolute", () => !FullScreen) + .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}") .Build(); + /// + protected string? StyleContentValue => new StyleBuilder() + .AddStyle("pointer-events", "auto", () => Interactive) + .Build(); + + /// + /// Gets or sets the unique identifier of the overlay. + /// + [Parameter] + public string? Id { get; set; } + /// /// Gets or sets a value indicating whether the overlay is visible. /// @@ -34,7 +65,7 @@ public partial class FluentOverlay public bool Visible { get; set; } = false; /// - /// Callback for when overlay visisbility changes. + /// Callback for when overlay visibility changes. /// [Parameter] public EventCallback VisibleChanged { get; set; } @@ -77,6 +108,24 @@ public partial class FluentOverlay [Parameter] public bool FullScreen { get; set; } = false; + /// + /// 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; } = false; + + /// + /// 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; } = null; + + /// + /// 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; @@ -94,8 +143,28 @@ public partial class FluentOverlay [Parameter] public RenderFragment? ChildContent { get; set; } - protected override void OnParametersSet() + protected override async Task OnParametersSetAsync() { + if (Interactive) + { + if (string.IsNullOrEmpty(Id)) + { + Id = _defaultId; + } + + // Add a document.addEventListener when Visible is true + if (Visible) + { + await InvokeOverlayInitializeAsync(); + } + + // Remove a document.addEventListener when Visible is false + else + { + await InvokeOverlayDisposeAsync(); + } + } + if (!Transparent && Opacity == 0) { Opacity = 0.4; @@ -135,13 +204,34 @@ protected override void OnParametersSet() } } - protected async Task OnCloseHandlerAsync(MouseEventArgs e) + [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) + if (!Dismissable || !Visible || Interactive) { return; } + // Close the overlay + await OnCloseInternalHandlerAsync(e); + } + + private async Task OnCloseInternalHandlerAsync(MouseEventArgs e) + { Visible = false; if (VisibleChanged.HasDelegate) @@ -153,8 +243,39 @@ protected async Task OnCloseHandlerAsync(MouseEventArgs e) { await OnClose.InvokeAsync(e); } + } + + /// + /// Disposes the overlay. + /// + /// + public async ValueTask DisposeAsync() + { + await InvokeOverlayDisposeAsync(); + + if (_jsModule != null) + { + await _jsModule.DisposeAsync(); + } + } + + /// + private async Task InvokeOverlayInitializeAsync() + { + _dotNetHelper ??= DotNetObjectReference.Create(this); + _jsModule ??= await JSRuntime.InvokeAsync("import", JAVASCRIPT_FILE.FormatCollocatedUrl(LibraryConfiguration)); + + var containerId = FullScreen ? null : Id; + await _jsModule.InvokeVoidAsync("overlayInitialize", _dotNetHelper, containerId, InteractiveExceptId); + } - return; + /// + private async Task InvokeOverlayDisposeAsync() + { + if (_jsModule != null && Interactive) + { + await _jsModule.InvokeVoidAsync("overlayDispose", InteractiveExceptId); + } } #if NET7_0_OR_GREATER diff --git a/src/Core/Components/Overlay/FluentOverlay.razor.js b/src/Core/Components/Overlay/FluentOverlay.razor.js new file mode 100644 index 0000000000..a6813c5032 --- /dev/null +++ b/src/Core/Components/Overlay/FluentOverlay.razor.js @@ -0,0 +1,68 @@ +/** + * Initialize the global click event handler + * @param {any} dotNetHelper + * @param {any} id + */ +export function overlayInitialize(dotNetHelper, containerId, id) { + + if (!document.fluentOverlayData) { + document.fluentOverlayData = {}; + } + + if (document.fluentOverlayData[id]) { + return; + } + + // Store the data + document.fluentOverlayData[id] = { + + // Click event handler + clickHandler: async function (event) { + const excludeElement = document.getElementById(id); + const isExcludeElement = excludeElement && excludeElement.contains(event.target); + const isInsideContainer = isClickInsideContainer(event, containerId); + + if (isInsideContainer && !isExcludeElement) { + dotNetHelper.invokeMethodAsync('OnCloseInteractiveAsync', event); + } + } + }; + + // Let the user click on the container (containerId or the entire document) + document.addEventListener('click', document.fluentOverlayData[id].clickHandler); +} + +/** + * Dispose the global click event handler + */ +export function overlayDispose(id) { + 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]; + } +} + +/** + * Determines whether a mouse click event occurred inside a specific HTML element identified by its `id`. + */ +function isClickInsideContainer(event, id) { + if (id && document.getElementById(id)) { + const container = document.getElementById(id); + 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/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Keyboard-ArrowDown.verified.razor.html b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Keyboard-ArrowDown.verified.razor.html index 1db5ecf58b..16213f9659 100644 --- a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Keyboard-ArrowDown.verified.razor.html +++ b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Keyboard-ArrowDown.verified.razor.html @@ -5,18 +5,14 @@ - - 1-Denis Voituron - 1-Denis Voituron - - 2-Vincent Baaij - 2-Vincent Baaij @@ -28,8 +24,8 @@ -
-
+
+
@@ -40,4 +36,4 @@
-
+
\ No newline at end of file diff --git a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Keyboard-ArrowUp.verified.razor.html b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Keyboard-ArrowUp.verified.razor.html index e2b7c8a9f9..e75effd033 100644 --- a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Keyboard-ArrowUp.verified.razor.html +++ b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Keyboard-ArrowUp.verified.razor.html @@ -24,8 +24,8 @@ -
-
+
+
@@ -36,4 +36,4 @@
- + \ No newline at end of file diff --git a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Keyboard-Backspace.verified.razor.html b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Keyboard-Backspace.verified.razor.html index 94fd1b314a..a7072c97d2 100644 --- a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Keyboard-Backspace.verified.razor.html +++ b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Keyboard-Backspace.verified.razor.html @@ -24,8 +24,8 @@ -
-
+
+
@@ -36,4 +36,4 @@
- + \ No newline at end of file diff --git a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_MaxAutoHeight_Opened.verified.razor.html b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_MaxAutoHeight_Opened.verified.razor.html index 83b0343ae7..b1068d367b 100644 --- a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_MaxAutoHeight_Opened.verified.razor.html +++ b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_MaxAutoHeight_Opened.verified.razor.html @@ -3,18 +3,14 @@
- - 1-Denis Voituron - 1-Denis Voituron - - 2-Vincent Baaij - 2-Vincent Baaij @@ -26,12 +22,12 @@ -
-
+
+
-
+
\ No newline at end of file diff --git a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_OnClear_ShowOverlay.verified.razor.html b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_OnClear_ShowOverlay.verified.razor.html index 275010c70f..9eb9054ced 100644 --- a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_OnClear_ShowOverlay.verified.razor.html +++ b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_OnClear_ShowOverlay.verified.razor.html @@ -1,13 +1,13 @@
- -
-
+
+
diff --git a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Opened.verified.razor.html b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Opened.verified.razor.html index 1205d1362e..e45e63eebd 100644 --- a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Opened.verified.razor.html +++ b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Opened.verified.razor.html @@ -1,13 +1,13 @@
- -
-
+
+
diff --git a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_SelectedOptions.verified.razor.html b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_SelectedOptions.verified.razor.html index 59e1b62a80..7162f2ff4e 100644 --- a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_SelectedOptions.verified.razor.html +++ b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_SelectedOptions.verified.razor.html @@ -5,18 +5,14 @@ - - Denis Voituron - Denis Voituron - - Vincent Baaij - Vincent Baaij @@ -28,8 +24,8 @@ -
-
+
+
@@ -40,4 +36,4 @@
-
+
\ No newline at end of file diff --git a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_SelectedOptions_OnDismissClick.verified.razor.html b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_SelectedOptions_OnDismissClick.verified.razor.html index ac82bd2acf..0f8f0cc361 100644 --- a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_SelectedOptions_OnDismissClick.verified.razor.html +++ b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_SelectedOptions_OnDismissClick.verified.razor.html @@ -24,8 +24,8 @@
-
-
+
+
@@ -36,4 +36,4 @@
-
+
\ No newline at end of file diff --git a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_SelectedOptions_Template.verified.razor.html b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_SelectedOptions_Template.verified.razor.html index f8398be886..8f2c3bbd14 100644 --- a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_SelectedOptions_Template.verified.razor.html +++ b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_SelectedOptions_Template.verified.razor.html @@ -24,8 +24,8 @@ -
-
+
+
@@ -42,4 +42,4 @@
-
+
\ No newline at end of file diff --git a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Templates.verified.razor.html b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Templates.verified.razor.html index 323036155f..a7de027d1e 100644 --- a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Templates.verified.razor.html +++ b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_Templates.verified.razor.html @@ -1,13 +1,13 @@
- -
-
+
+
diff --git a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_ValueText_Clears.verified.razor.html b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_ValueText_Clears.verified.razor.html index 275010c70f..9eb9054ced 100644 --- a/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_ValueText_Clears.verified.razor.html +++ b/tests/Core/List/FluentAutocompleteTests.FluentAutocomplete_ValueText_Clears.verified.razor.html @@ -1,13 +1,13 @@
- -
-
+
+
diff --git a/tests/Core/ProfileMenu/FluentProfileMenuTests.FluentProfileMenu_Customized.verified.razor.html b/tests/Core/ProfileMenu/FluentProfileMenuTests.FluentProfileMenu_Customized.verified.razor.html index e632043c91..600168f124 100644 --- a/tests/Core/ProfileMenu/FluentProfileMenuTests.FluentProfileMenu_Customized.verified.razor.html +++ b/tests/Core/ProfileMenu/FluentProfileMenuTests.FluentProfileMenu_Customized.verified.razor.html @@ -8,8 +8,8 @@
-
-
+
+
@@ -21,4 +21,4 @@
-
+ \ No newline at end of file diff --git a/tests/Core/ProfileMenu/FluentProfileMenuTests.FluentProfileMenu_Default.verified.razor.html b/tests/Core/ProfileMenu/FluentProfileMenuTests.FluentProfileMenu_Default.verified.razor.html index cd7cd1890f..2ecf8d9235 100644 --- a/tests/Core/ProfileMenu/FluentProfileMenuTests.FluentProfileMenu_Default.verified.razor.html +++ b/tests/Core/ProfileMenu/FluentProfileMenuTests.FluentProfileMenu_Default.verified.razor.html @@ -15,8 +15,8 @@
Bill Gates
-
-
+
+
@@ -56,4 +56,4 @@