Event for click outside a div or element to close it on Blazor
Asked Answered
C

13

16

In my Blazor server-side project, I need to close a pop-up menu by clicking outside the menu. I use a simple If statement to show/hide the pop-up by triggering the onClick event. but there is no such event to close the pop-up by clicking outside the pop-up menu. so the user should only close it by click on the element with the onClick event. so my question is how we can resolve this issue better without using JS?

Thank you in advance.

Cutlip answered 22/4, 2020 at 15:54 Comment(0)
Y
2

The @onmouseout event inside of my div tag does work, but it was a bit too touchy for what I needed because my div has several children so it kept closing when I didn't want it to. I found this solution to be more reliable.

I created an event handler class

[EventHandler("onmouseleave", typeof(EventArgs), true, true)]
public class EventHandlers
{
}

and then inside my razor page:

<div class="menu" @onmouseleave="MouseLeave"></div>

@code {
    private void MouseLeave(EventArgs args)
    {
        IsVisible = false;
    }
}
Yost answered 28/6, 2022 at 15:21 Comment(5)
Thanks hafootnd. I have tested this one and indeed it works as expected. I'll set this as the answer.Cutlip
To be clear, @AminM, this really doesn't solve/address your original question exactly, correct? Namely, you asked to be able to click outside of the popup menu to have the popup menu be closed. However, this answer relies on the mouse first entering the popup menu, then when the mouse leaves the popup menu, the menu will automatically be closed. No clicking involved.Zoba
For a detailed example of my above comment, if there is a link in the middle of the page that you click to open the popup menu and the popup menu renders below the link, but then the user moves their mouse up and thinks to try clicking on the document to close the menu, like you're asking, it won't happen. The user will have to move their mouse all the way back down into the popup menu and then out again in order for the menu to close. Isn't that what this solution is proposing? It would be nice to state that in this answer that it doesn't answer the original question as asked.Zoba
My understanding was that @Cutlip wanted a way to do this without needing to click the same element that opened the pop-up. I'm assuming the cursor is already in the pop-up as soon as it is displayed, so it's faster for the user to just leave the bounds of the pop-up to get it to close, which is a common method in JS websites. I think there was just some breakdown in communication because the question wasn't worded as well as it could've been because instead of the word "should" I think he meant "can"Yost
I don't understand how this can possibly be the accepted answer as it doesn't even solve the problem.Nucleo
S
20

Here is what I do with a div to fire the onfocusout event where the ShowSelectBox variable is used by your if-statement to show or hide something:

<div tabindex="0" @onfocusout="@(() => ShowSelectBox = false)">
...
</div>

I used this in a blazor webassambly application, but should be the same for server-side.

Swartz answered 20/6, 2020 at 4:12 Comment(5)
This doesn't seem to work unfortunately, as first you need to give the popup the focus. So if you click on the popup and then click outside, it will be closed. And AFAIK you can't set window to be focused without the use of JS, as also mentioned here: #60309688.Morrell
"and then click outside, it will be closed" - isn't that your intention?Swartz
The thing is, if you don't click on the popup it doesn't get focus, so clicking anywhere else will not close it. You HAVE TO click on the popup first to give it focus in order for a click outside of it to close it.Morrell
It's the correct solution, with no hacks and no unexpected behaviors.Monti
I can confirm it also works on server side blazorAftermost
D
10

Add a <div> element that is the size of the screen and is z-indexed 1 lower than the popup menu, but higher then the rest of the application. You will need to use position: fixed in the CSS class, and then width: 100%; height: 100%; top: 0%; left: 0%; so it will fill up the page. Initial CSS display value should be display: none;. When you open the popup, also change the display property of the div to display: flex to add it to the DOM, then add an @OnClick click handler on this floating div that will close the popup AND set the floating div display property back to display: none;. The div should be clear so you can still see the rest of the app behind it, but allows you to click outside the popup for the behavior you are looking for, as anywhere outside the popup will be on the Div covering the rest of the screen.

Note that on the popup, you will also need to set the floating Div display value back to 'none' when you close the popup from inside the popup, otherwise it will stick around and require an extra click to get it to go away.

You can also slightly shade the Div element with transparency to provide a highlighting effect for your popup when it opens.

Delative answered 22/4, 2020 at 16:25 Comment(1)
Thnx @NikProtsman for your suggestion, however, this is kind of hack which I prefer to not use it for a real-world project. in this situation, you could easily assign a function to the window using JS so it gets all your clicks and then you can filter it and use it for your certain goal. although, this is not still my taste :DCutlip
D
6

I came up with a solution without using JS, using the onmouseover event we can set a value to a bool var as true and with a onmouseout event we can set as false and so, verifying if the mouse clicked in or out a tag element

 <button @onclick="Show">Click</button>
<div  @onmouseover="MouseIn" @onmouseout="MouseOut">...</div>
    
@code{
bool IsInside; 
void Show()
{
   if(!IsInside)
   {
       ...
   }
}
void MouseIn()
{
   IsInside= true
}; 
void MouseOut()
{
   IsInside= False;
}
}
Devotee answered 8/7, 2020 at 1:52 Comment(2)
+1. I think this is the best way. I think this is the Blazor way of thinking. The JSInterop methods other people are using is not worth the trouble.Elbow
Thank you NetoTI for the answer, there is an issue with this approach. if you log these to functions, you will see that by any mouse movement, both functions get triggered. so you cannot distinguish when the mouse actually is in and when is out.Cutlip
I
5

I had this very same issue with a custom dropdown menu I made, and I felt that the standard behavior of anything that looks like a dropdown should be to close itself if you click outside of it.

I solved it with a custom component that puts a fullscreen div just below the dropdown (z-index wise), and added an onclick eventcallback to that div. It feels a bit unorthodox to do it like this, and it may not fit all cases, but hey, it works, and no JavaScript needed!

On my .razor page:

@if(showUserDD)
{
    <OutsideClickDetector MethodToCallOnClick="@ToggleUserDD" LowestInsideComponentZIndex="1000" />
    <div id="UserDDContent" style="z-index: 1000" class="list-group position-absolute shadow">
        ... some content ...
    </div>
}

@code {
    private async Task ToggleUserDD() => showUserDD = !showUserDD;
}

OutsideClickDetector.razor

<div @onclick="OnClick" class="vh-100 vw-100 position-fixed"
     style="top: 0; left: 0; z-index: @(LowestInsideComponentZIndex-1);
         background-color: @(TransparentOutside ? "none" : "rgba(0,0,0,0.5)");"
 />

@code
{
    [Parameter] public int LowestInsideComponentZIndex { get; set; }        
    [Parameter] public bool TransparentOutside { get; set; } = true;
    [Parameter] public EventCallback MethodToCallOnClick { get; set; }
    private async Task OnClick() => await MethodToCallOnClick.InvokeAsync(null);
}

For debugging purposes you can set TransparentOutside to false if you want to see the fullscreen div.

Integer answered 27/5, 2021 at 10:17 Comment(1)
This is exactly how I do it. A full screen div behind all modal divs and then handle click on those.Nucleo
W
3

I had the same situation and I ended up using css :hover state. Assuming you have a drop down menu popup, doing this way when you hover the div element the menu popup will get displayed and when you hover out the menu popup will be closed automatically.

or

You can do something like,

<div class="dropdown is-right is-hoverable @(PopupCollapsed ? "is-active" : null)"
             @onclick="e => this.PopupCollapsed = !this.PopupCollapsed"
             @onfocusout="e => this.PopupCollapsed = false">
</div>

@code {
public bool PopupCollapsed { get; set; }
}
Woehick answered 25/11, 2020 at 18:50 Comment(1)
I think the onfocusout is a very good idea, the problem is if the click that caused the focusout is on the popover and you dont want it to close onclickJaworski
Y
2

The @onmouseout event inside of my div tag does work, but it was a bit too touchy for what I needed because my div has several children so it kept closing when I didn't want it to. I found this solution to be more reliable.

I created an event handler class

[EventHandler("onmouseleave", typeof(EventArgs), true, true)]
public class EventHandlers
{
}

and then inside my razor page:

<div class="menu" @onmouseleave="MouseLeave"></div>

@code {
    private void MouseLeave(EventArgs args)
    {
        IsVisible = false;
    }
}
Yost answered 28/6, 2022 at 15:21 Comment(5)
Thanks hafootnd. I have tested this one and indeed it works as expected. I'll set this as the answer.Cutlip
To be clear, @AminM, this really doesn't solve/address your original question exactly, correct? Namely, you asked to be able to click outside of the popup menu to have the popup menu be closed. However, this answer relies on the mouse first entering the popup menu, then when the mouse leaves the popup menu, the menu will automatically be closed. No clicking involved.Zoba
For a detailed example of my above comment, if there is a link in the middle of the page that you click to open the popup menu and the popup menu renders below the link, but then the user moves their mouse up and thinks to try clicking on the document to close the menu, like you're asking, it won't happen. The user will have to move their mouse all the way back down into the popup menu and then out again in order for the menu to close. Isn't that what this solution is proposing? It would be nice to state that in this answer that it doesn't answer the original question as asked.Zoba
My understanding was that @Cutlip wanted a way to do this without needing to click the same element that opened the pop-up. I'm assuming the cursor is already in the pop-up as soon as it is displayed, so it's faster for the user to just leave the bounds of the pop-up to get it to close, which is a common method in JS websites. I think there was just some breakdown in communication because the question wasn't worded as well as it could've been because instead of the word "should" I think he meant "can"Yost
I don't understand how this can possibly be the accepted answer as it doesn't even solve the problem.Nucleo
V
1

My solution is use blazor only. All dropdown is from one component which contols click on it. if user click on closed dropdown to open it then actualy opened dropdown is closed. Or if user click on main div of body page or any object on this div (not dropdown), then actualy opened dropdown is closed too. Attribute stopPropagation is set to true on dropdown for block main div click event to block close dropdown actualy opening itself. Dropdown has its own click event because it also contains an input element for searching dropdown items and clicking on it would close the dropdown. It should also work for DatePisker, ColorPicker and other elements that actually open a div. I try this solution and it looks like it could work well

Create class for all dropdowns

public class DropDownState
{
    public string OpenedDropDownId { get; private set; }

    public event Action OnChange;

    public void SetOpened(string Id)
    {
        OpenedDropDownId = Id;
        NotifyStateChanged();
    }

    private void NotifyStateChanged() => OnChange?.Invoke();
}

To Program.cs add

builder.Services.AddScoped<Data.DropDownState>();

Dropdown component looks like this, stop propagation is important for this solution

@inject Data.DropDownState DropDownStateService
@implements IDisposable

<div class="form-group" @onclick="ObjectClick" 
    @onclick:stopPropagation="true">
    <div class="dropdown">
       ...

// each dropdown needs own unique id
[Parameter]
public string ComponentId { get; set; }

bool showResults = false;
protected override async Task OnInitializedAsync()
{
    DropDownStateService.OnChange += DropDownOdherOpened;
}
public void Dispose()
{
    DropDownStateService.OnChange -= DropDownOdherOpened;
}
async Task DropdownShow(bool _showResults)
{
    showResults = _showResults
    if (showResults)
    {
        // this informs other dropdowns that this dropdown is opened an other dropdown go to closed state
        DropDownStateService.SetOpened(ComponentId);
    }
}

public void DropDownOdherOpened()
{
    if (DropDownStateService.OpenedDropDownId != ComponentId && showResults)
    {
        // close dropdown
        DropdownShow(false);
        StateHasChanged();
    }
}

// this closes other dropdowns on click, this replaces main div click event functionality
public async Task ObjectClick()
{
    DropDownStateService.SetOpened(ComponentId);
}

And in MainLayout.razor add onclick

@inject Data.DropDownState DropDownStateService

<div class="container-fluid" @onclick="ObjectClick">
    @Body
</div>

public async Task ObjectClick()
{
    DropDownStateService.SetOpened("Body");
}
Vorlage answered 19/5, 2021 at 14:54 Comment(0)
C
1

I was running into a similar, if not the same, issue. In my case, I have a shared component from an RCL, that exists as a child component on either a page or other component. I'm creating a custom dropdown menu and I needed a global event that would close all of those components if I clicked anywhere else on the page.

Originally, I resorted to creating a global click event with Javascript. I decided to look back into it and came up with a solution that has zero Javascript and I wanted to share and it's actually pretty simple.

I have my component

<div class="dropdown" @onclick="toggleDropdown" @onclick:stopPropagation>
    @if(_item.Expanded)
    {
        <div class="dropdown-list">
            <ul>
                <li>Item</li>
            </ul>
        </div>
    }
</div>

@code{
    [CascadingParameter]
    protected MyService Service { get; set; }
    [Parameter]
    protected Item Item { get; set; }

    private Item _item;

    protected override void OnParametersSet()
    {
        _item = Item;

        if(!Service.Items.Contains(_item))
            Service.Items.Add(_item);

        base.OnParametersSet();
    }

    private void toggleDropdown() => _item.Expanded = !_item.Expanded;
}

Then I have my service

public class MyService : IMyService
{
    private List<Item> _items;
    public List<Item> Items
    {
        get => _items ?? new List<Item>();
        set => _items = value;
    }

    public void CloseDropdowns()
    {
        if(Items.Any(item => item.Expanded))
        {
            foreach(var i in Items)
                i.Expanded = false;
        }
    }
}

Finally, I have the MainLayout of my actual application

<CascadingValue Value="MyService">
    <div class="page" @onclick="MyService.CloseDropdowns">

        <div class="main">
            <div class="content px-4">
                @Body
            </div>
        </div>
    </div>
</CascadingValue>

Hope this helps.

Update

I failed to mention an issue that occurs when there are multiple of the same component on a page, for example, two or more dropdowns. The above code works as designed since the mainlayout click event is bubbled up, but we don't want this to close everything every time, we could have one dropdown open and all the others closed.

For this, we need to stop the event bubbling to the mainlayout and provide a callback to the dropdown's parent component that will update the child component.

In the service

public void ToggleDropdown(bool expanded, Item item)
{
    foreach(var i in _items)
        i.Expanded = false;

    item.Expanded = expanded;
}

In the component

[Parameter]
public EventCallback<(bool, DSCInputConfig)> ExpandCallback { get; set; }
private bool _shouldRender;
protected override bool ShouldRender() => _shouldRender;
private void toggleDropdown()
{
    _expanded = !_expanded;

    ExpandCallback.InvokeAsync((_expanded, _config));

    _shouldRender = true;
}

Finally, in the parent component/page

<Dropdown Options="@component" ExpandCallback="dropdownCallback" />
<Dropdown Options="@component" ExpandCallback="dropdownCallback" />

@code{
    private void dropdownCallback((bool expanded, Item config) item) => MyService.ToggleDropdown(item.expanded, item.config);
}
Caro answered 7/9, 2022 at 17:29 Comment(0)
B
0

It's a bit of a faff but managed it with some JS interop. I'm new to Blazor but managed to cadge this together from various other posts;

Add an id to your popup - called mine profile-popup

<div id="profile-popup">
        <RadzenProfileMenu @ref="profileMenu" Style="width: 200px;" >
            <Template>
                <div class="row">...

Create a JS file to attach a handler to the click event of the document - if the source of the click is in your popup ignore it, otherwise fire a helper method from your helper class

    window.attachHandlers = (dotnetHelper) => {
    document.addEventListener("click", (evt) => {
        const profileElement = document.getElementById("profile-popup");
        let targetElement = evt.target;

        do {
            if (targetElement == profileElement) {
                //return as not clicked outside
                return;
            }            
            targetElement = targetElement.parentNode;
        } while (targetElement);

        dotnetHelper.invokeMethod("InvokeAction");
    });
};

Create the helper class

public class ProfileOutsideClickInvokeHelper
{
    Action _action;

    public ProfileOutsideClickInvokeHelper(Action action)
    {
        _action = action;
    }

    [JSInvokable]
    public void InvokeAction()
    {
        _action.Invoke();
    }
}

Attach the handler in the OnAfterRender override. I have a component containing the popup. You need to dispose of the object reference

public partial class TopBanner : IDisposable
{        
    [Inject]
    IJSRuntime JSRuntime { get; set; }

    public void CloseProfileMenu()
    {
        profileMenu.Close();
    }

    DotNetObjectReference<ProfileOutsideClickInvokeHelper> _objRef;

    protected override void OnAfterRender(bool firstRender)
    {
        _objRef = DotNetObjectReference.Create(new ProfileOutsideClickInvokeHelper(CloseProfileMenu));
        JSRuntime.InvokeVoidAsync("attachHandlers", _objRef);
    }

    public void Dispose()
    {
        _objRef?.Dispose();
    }
}

Not sure if this is the best solution, or even a good one, but it seems to work ok.

Barr answered 29/6, 2020 at 15:26 Comment(1)
I'm trying to implement this solution, but I'm having an issue where the handler is being called right after the popup is displayed, dismissing it immediately after being made visible. How do you solve that? I was thinking of a nasty hack, to simply use a timer and not allow it to happen within like 500ms.Evangel
L
0

Here is my solution. This is Blazor/CSS only and will work with most popular exsisting pop-up/slider/offcanvas type css (eg: bootstrap).

The button to open the pop-up/slider etc:

<a @onclick="ToggleUserPanel">Open</a>

The pop-up/slider etc:

<div id="quick_user" class="offcanvas offcanvas-right p-10 @UserPanelClass">
<a href="javascript:;" class="btn btn-primary" id="quick_user_close">
                    <i @onclick="ToggleUserPanel"></i>
</a>
</div>

Important part, the Overlay. Most base css libraries will use an Overlay with a pop-up, the beauty of this is you don't need to worry about styling a div for the "non" pop-up part of the screen. If you aren't using a library it's easy enough to write your own styles for a z-indexed overlay.

@if (UserPanelClass != "")
{
    <div class="offcanvas-overlay" @onclick="ToggleUserPanel"></div>
}

BLAZOR STUFF

@code{
    private string UserPanelClass = "";
    //This will either show the User Panel along with it's overlay
    //or close the User Panel and remove the overlay.
    //It can be triggered by any buttons that call it or (the solution to this 
    //post) clicking on the "Non Pop-Up" part of the screen which is the 
    //overlay div
    private void ToggleUserPanel()
    {
        if (String.IsNullOrEmpty(UserPanelClass))
            UserPanelClass = "offcanvas-on";
        else
            UserPanelClass = "";

    }
}
            

And that's it.

Linnell answered 26/9, 2021 at 2:51 Comment(0)
I
0

Found a solution in a Github repository.

    <OutsideHandleContainer OnClickOutside=@OnClickOutside>
        ...
        <div>Sample element</div>
        ...
    </OutsideHandleContainer>
    
    @functions{
        void OnClickOutside()
        {
            // Do stuff
        }
    }
Impunity answered 17/12, 2021 at 13:17 Comment(1)
Th OnClickOutside is not in that component.Home
L
0

I spent a couple of hours on this today to come up with best solution, that doesn't have any side effects and doesn't rely on JSInterop:

Show an overlay element when you show your popup:

<div class="calendar-overlay @(FilterOpen ? "calendar-overlay--open" : "")" @onclick="@CloseFilter"></div>

Css:

.calendar-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0);
  z-index: 999;
  display: none;
}
.calendar-overlay--open {
  background-color: rgba(0, 0, 0, 0.5);
  display: block;
}

You can leave it transparent though.

Now, on the popup element:

<div class="vacancy-filter-slideout @(FilterOpen ? "vacancy-filter-slideout--open" : "")" onmouseover="@MouseIn" onmouseout="@MouseOut">

Make sure to give it this in CSS:

z-index: 1000;

and the code behind:

private bool FilterOpen { get; set; } = false;
private bool FilterMouseOver { get; set; } = false;

private void CloseFilter()
{
    if (!FilterMouseOver)
    {
        FilterOpen = false;
    }
    
}

private void MouseIn()
{
    FilterMouseOver = true;
}

private void MouseOut()
{
    FilterMouseOver = false;
}

In short - when your popup opens, it also shows visible or invisible overlay behind it with z-index:999. Popup has z-index:1000 so it's on top. It tracks the mouse, and flips a bool when mouse leaves the popup. Any click outside the popup will be a click on the overlay which flips another bool after checking if the click definitely wasn't inside the popup, and closes the window by adding/removing class.

This doesn't need any JSInterop and also makes sure that we don't have to give or check focus on the div. Also prevents popup from closing when we click anything inside of it (unless we add X button which can call another method to close the popup manually)

Leduc answered 14/3, 2023 at 16:43 Comment(0)
R
0

If you are using Blazor then it provides Blazor Bootstrap Components. We can use the below parameters for component from Blazor Bootstrap.

UseStaticBackdrop="true" CloseOnEscape="false"

This fixes issues like closing modal pup-up on outside click and also close pup-up using ESC key(keyboard button). Documentation Link here

<Modal @ref="modal" title="Modal title" UseStaticBackdrop="true" CloseOnEscape="false">
Ripon answered 25/1 at 7:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.