Skip to content
This repository was archived by the owner on Dec 26, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 52 additions & 15 deletions src/Blazored.Modal/BlazoredModal.razor
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
[Parameter] public bool? HideHeader { get; set; }
[Parameter] public bool? HideCloseButton { get; set; }
[Parameter] public bool? DisableBackgroundCancel { get; set; }
[Parameter] public bool? DisableEscapeKey { get; set; }
[Parameter] public string? OverlayCustomClass { get; set; }
[Parameter] public ModalPosition? Position { get; set; }
[Parameter] public string? PositionCustomClass { get; set; }
Expand All @@ -32,7 +33,8 @@

private readonly Collection<ModalReference> _modals = new();
private readonly ModalOptions _globalModalOptions = new();
private IJSObjectReference? _styleFunctions;
private IJSObjectReference? _jsModule;
private DotNetObjectReference<BlazoredModal>? _dotNetObjRef;
private bool _haveActiveModals;

internal event Action? OnModalClosed;
Expand All @@ -44,12 +46,13 @@
throw new InvalidOperationException($"{GetType()} requires a cascading parameter of type {nameof(IModalService)}.");
}

((ModalService) CascadedModalService).OnModalInstanceAdded += Update;
((ModalService) CascadedModalService).OnModalCloseRequested += CloseInstance;
((ModalService)CascadedModalService).OnModalInstanceAdded += Update;
((ModalService)CascadedModalService).OnModalCloseRequested += CloseInstance;
NavigationManager.LocationChanged += CancelModals;

_globalModalOptions.Class = Class;
_globalModalOptions.DisableBackgroundCancel = DisableBackgroundCancel;
_globalModalOptions.DisableEscapeKey = DisableEscapeKey;
_globalModalOptions.HideCloseButton = HideCloseButton;
_globalModalOptions.HideHeader = HideHeader;
_globalModalOptions.Position = Position;
Expand All @@ -67,7 +70,25 @@
{
if (firstRender)
{
_styleFunctions = await JsRuntime.InvokeAsync<IJSObjectReference>("import", "./_content/Blazored.Modal/BlazoredModal.razor.js");
_jsModule = await JsRuntime.InvokeAsync<IJSObjectReference>("import", "./_content/Blazored.Modal/BlazoredModal.razor.js");

_dotNetObjRef = DotNetObjectReference.Create(this);
if (DisableEscapeKey is not true && _dotNetObjRef is not null)
await _jsModule.InvokeVoidAsync("addEscapeKeyHandler", _dotNetObjRef);
}
}

[JSInvokable("HandleEscapeKey")]
public async Task HandleEscapeKeyAsync()
{
if (DisableEscapeKey is true || !_modals.Any())
return;

if (_modals.LastOrDefault() is { } lastModalReference)
{
if (lastModalReference.ModalInstanceRef?.Options.DisableEscapeKey is true)
return;
await CloseInstance(lastModalReference, ModalResult.Cancel());
}
}

Expand Down Expand Up @@ -129,12 +150,12 @@
if (!_haveActiveModals)
{
_haveActiveModals = true;
if (_styleFunctions is not null)
{
await _styleFunctions.InvokeVoidAsync("setBodyStyle");
}
if (_jsModule is null)
return;

await _jsModule.InvokeVoidAsync("setBodyStyle");
}

await InvokeAsync(StateHasChanged);
}

Expand All @@ -144,25 +165,41 @@
private async Task ClearBodyStyles()
{
_haveActiveModals = false;
if (_styleFunctions is not null)
if (_jsModule is not null)
{
await _styleFunctions.InvokeVoidAsync("removeBodyStyle");
await _jsModule.InvokeVoidAsync("removeBodyStyle");
}
}

private async Task RemoveEscapeKeyHandler()
{
if (_jsModule is null)
return;

try
{
await _jsModule.InvokeVoidAsync("removeEscapeKeyHandler");
}
catch (JSDisconnectedException)
{
// Ignored. Browser is not there any more.
}
}

async ValueTask IAsyncDisposable.DisposeAsync()
{
if (_styleFunctions is not null)
await RemoveEscapeKeyHandler();
_dotNetObjRef?.Dispose();
if (_jsModule is not null)
{
try
{
await _styleFunctions.DisposeAsync();
await _jsModule.DisposeAsync();
}
catch (JSDisconnectedException)
{
// If the browser is gone, we don't need it to clean up any browser-side state
}
}
}

}
}
35 changes: 35 additions & 0 deletions src/Blazored.Modal/BlazoredModal.razor.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
const el = document.body;
const computedBodyStyle = getComputedStyle(el);
const originalProps = { overflow: computedBodyStyle.overflow, paddingRight: computedBodyStyle.paddingRight };

let keyupHandler = null;
let dotNetRef = null;

const getScrollBarWidth = () => {
let el = document.createElement("div");
el.style.cssText = "overflow:scroll; visibility:hidden; position:absolute;";
Expand All @@ -18,6 +22,37 @@ const isScrollbarPresent = () => {
return beforeScrollbarHidden !== afterScrollbarHidden;
};

/**
* Adds event listener for the Escape key and invokes .NET method
* @param {object} dotNetObjectReference Reference to .NET object that handles the escape key
*/
export function addEscapeKeyHandler(dotNetObjectReference) {
// Clear state before adding the handler
removeEscapeKeyHandler()

dotNetRef = dotNetObjectReference;
keyupHandler = function (event) {
if(event.key === 'Escape') {
event.preventDefault()
event.stopPropagation()
dotNetRef.invokeMethodAsync('HandleEscapeKey')
}
}

document.addEventListener('keyup', keyupHandler, true)
}

/**
* Clears the event listener for the Escape key and resets state
*/
export function removeEscapeKeyHandler() {
if(keyupHandler) {
document.removeEventListener('keyup', keyupHandler, true)
keyupHandler = null
dotNetRef = null
}
}

export function setBodyStyle() {
if (isScrollbarPresent()) {
el.style.paddingRight = `${getScrollBarWidth()}px`;
Expand Down
13 changes: 12 additions & 1 deletion src/Blazored.Modal/BlazoredModalInstance.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public partial class BlazoredModalInstance : IDisposable
private bool HideHeader { get; set; }
private bool HideCloseButton { get; set; }
private bool DisableBackgroundCancel { get; set; }
private bool DisableEscapeKey { get; set; }
private string? OverlayAnimationClass { get; set; }
private string? OverlayCustomClass { get; set; }
private ModalAnimationType? AnimationType { get; set; }
Expand Down Expand Up @@ -133,6 +134,7 @@ private void ConfigureInstance()
HideHeader = SetHideHeader();
HideCloseButton = SetHideCloseButton();
DisableBackgroundCancel = SetDisableBackgroundCancel();
DisableEscapeKey = SetDisableEscapeKey();
UseCustomLayout = SetUseCustomLayout();
OverlayCustomClass = SetOverlayCustomClass();
ActivateFocusTrap = SetActivateFocusTrap();
Expand Down Expand Up @@ -207,7 +209,7 @@ private string SetPosition()
return "";
}
}

private string SetSize()
{
ModalSize size;
Expand Down Expand Up @@ -354,6 +356,15 @@ private async Task HandleBackgroundClick()
}
}

private bool SetDisableEscapeKey()
{
if (Options.DisableEscapeKey.HasValue)
return Options.DisableEscapeKey.Value;

return GlobalModalOptions.DisableEscapeKey.HasValue &&
GlobalModalOptions.DisableEscapeKey.Value;
}

private void ListenToBackgroundClick()
=> _listenToBackgroundClicks = true;

Expand Down
2 changes: 2 additions & 0 deletions src/Blazored.Modal/CascadingBlazoredModal.razor
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<BlazoredModal HideHeader="@HideHeader"
HideCloseButton="@HideCloseButton"
DisableBackgroundCancel="@DisableBackgroundCancel"
DisableEscapeKey="@DisableEscapeKey"
Position="@Position"
PositionCustomClass="@PositionCustomClass"
Class="@Class"
Expand All @@ -21,6 +22,7 @@
[Parameter] public bool? HideHeader { get; set; }
[Parameter] public bool? HideCloseButton { get; set; }
[Parameter] public bool? DisableBackgroundCancel { get; set; }
[Parameter] public bool? DisableEscapeKey { get; set; }
[Parameter] public ModalPosition? Position { get; set; }
[Parameter] public ModalSize? Size { get; set; }
[Parameter] public string? Class { get; set; }
Expand Down
1 change: 1 addition & 0 deletions src/Blazored.Modal/Configuration/ModalOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class ModalOptions
public string? OverlayCustomClass { get; set; }
public string? Class { get; set; }
public bool? DisableBackgroundCancel { get; set; }
public bool? DisableEscapeKey { get; set; }
public bool? HideHeader { get; set; }
public bool? HideCloseButton { get; set; }
public ModalAnimationType? AnimationType { get; set; }
Expand Down
34 changes: 34 additions & 0 deletions tests/Blazored.Modal.Tests/DisplayTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.AspNetCore.Components;
using Blazored.Modal.Tests.Assets;
using Blazored.Modal.Services;
using System.Threading.Tasks;
using static Bunit.ComponentParameterFactory;

namespace Blazored.Modal.Tests
Expand Down Expand Up @@ -124,5 +125,38 @@ public void ModalHidesWhenReferenceCloseCalled()
// Assert
Assert.Empty(cut.FindAll(".bm-container"));
}

[Fact]
public async Task ModalHidesWhenEscapeKeyPressed()
{
// Arrange
var modalService = Services.GetService<IModalService>();
var cut = RenderComponent<BlazoredModal>(CascadingValue(modalService));
modalService.Show<TestComponent>();

// Act
await cut.InvokeAsync( () => cut.Instance.HandleEscapeKeyAsync());

// Assert
Assert.Empty(cut.FindAll(".bm-container"));
}

[Fact]
public async Task TopMostModalHidesOnEscapeKeyPressedWhenMultipleAreVisible()
{
// Arrange
var modalService = Services.GetService<IModalService>();
var cut = RenderComponent<BlazoredModal>(CascadingValue(modalService));
modalService.Show<TestComponent>("First");
modalService.Show<TestComponent>("Last");

// Act
await cut.InvokeAsync( () => cut.Instance.HandleEscapeKeyAsync());

// Assert
var instances = cut.FindAll(".bm-container");
Assert.DoesNotContain("Last", cut.Find(".bm-title").InnerHtml);
Assert.Single(instances);
}
}
}
21 changes: 21 additions & 0 deletions tests/Blazored.Modal.Tests/ModalOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using System.Threading.Tasks;
using Xunit;
using static Bunit.ComponentParameterFactory;

Expand Down Expand Up @@ -266,5 +267,25 @@ public void ModalDisplaysCustomSizeClassWhenSizeIsCustom()
// Assert
Assert.NotNull(cut.Find(".my-custom-size"));
}

[Fact]
public async Task ModalDoesNotCloseWhenDisableEscapeKeySetToTrueInOptions()
{
// Arrange
var options = new ModalOptions
{
DisableEscapeKey = true
};
var modalService = Services.GetService<IModalService>();
var cut = RenderComponent<BlazoredModal>(CascadingValue(modalService));

modalService.Show<TestComponent>("", options);

// Act
await cut.InvokeAsync( () => cut.Instance.HandleEscapeKeyAsync());

// Assert
Assert.NotEmpty(cut.FindAll(".bm-container"));
}
}
}