I'm aware this is a common question but none of the answers I've seen solve my problem, apologies if I have missed one and this can be removed/marked as duplicate obviously...
Markup
<div class="has-dropdown">
<button class="js-dropdown-trigger">
Dropdown
</button>
<div class="dropdown">
<div class="dropdown__item">
Some random text with a <a href="#" class="stop-propagation">link</a> in it.
</div>
<div class="dropdown__divider"></div>
<div class="dropdown__item">
<a href="#">Item One</a>
</div>
<div class="dropdown__item">
<a href="#">Item Two</a>
</div>
<div class="dropdown__item">
<a href="#">Item Three</a>
</div>
</div>
</div>
Script
$('.has-dropdown').off().on('click', '.js-dropdown-trigger', (event) => {
const $dropdown = $(event.currentTarget).next('.dropdown');
if (!$dropdown.hasClass('is-active')) {
$dropdown.addClass('is-active');
} else {
$dropdown.removeClass('is-active');
}
});
$('.has-dropdown').on('focusout', (event) => {
const $dropdown = $(event.currentTarget).children('.dropdown');
$dropdown.removeClass('is-active');
});
Styling
.has-dropdown {
display: inline-flex;
position: relative;
}
.dropdown {
background-color: #eee;
border: 1px solid #999;
display: none;
flex-direction: column;
position: absolute;
top: 100%;
left: 0;
width: 300px;
margin-top: 5px;
}
.dropdown.is-active {
display: flex;
}
.dropdown__item {
padding: 10px;
}
.dropdown__divider {
border-bottom: 1px solid #999;
}
Fiddle
http://jsfiddle.net/joemottershaw/3yzadmek/
It's unbelievably simple, clicking the js-dropdown-trigger
toggles the is-active
dropdown class fine, clicking outside the has-dropdown
container removes the is-active
dropdown class too.
Except, what I expected to happen is focusing on a descendant element (either click or tab) of the has-dropdown
element would mean that the focusout
event handler shouldn't be triggered as you are still focused on a descendant element of the has-dropdown
container.
The
focusout
event is sent to an element when it, or any element inside of it, loses focus. This is distinct from the blur event in that it supports detecting the loss of focus on descendant elements
I know I could remove the focusout
event handler and use something like:
$(document).on('click', (event) =>{
const $dropdownContainer = $('.has-dropdown');
if (!$dropdownContainer.is(event.target) && $dropdownContainer.has(event.target).length === 0) {
$dropdownContainer.find('.dropdown').removeClass('is-active');
}
});
This works, but if you were to click on the trigger and then tab through the links, when you tab past the last link, the dropdown will still be visible. Just struggling to find the best solution to keep the accessibility side of things.
I want to stick to the focusout
method if at all possible.
Updated based on darshanags answer
Although the updated script works for single elements, adding other elements to the body
causes focusout
not to work as intended anymore. I think this is because of the if
statement seems to be true even when focus is applied to any element after the has-dropdown
container, not just descendants? Cause if you are to update the HTML and add more focusable elements such as an input after the dropdown. When tabbing from the last focusable element from within the has-dropdown
container to the input, the dropdown stays active. It only works if the dropdown is the last element in the DOM and only triggers when focus is lost on the DOM entirely.
You're almost there with your code - but I believe that there needs to be a bit more clarity of how
focusout
works with natively non-focusable elements (i.e: div, p) and their descendants which could be focusable elements (inputs, anchors).When a container is bound to the
focusout
event which contains focusable elements, it'sfocusout
event is triggered each time any of it's focusable child elements loses focus - this might be by keyboard navigation or by clicking on another child element or on the container itself. I've setup a fiddle that demonstrates this: https://jsfiddle.net/darshanags/v5gk2cz8/ - console messages are sent out each time the container gains or loses focus.Q: So, why does the menu hide itself in the example given in the question?
A: The menu becomes visible when you click on the button, clicking on the button makes the button gain focus on itself. But once you click on the menu or an anchor inside the menu element, the button loses focus - this in turn triggers the 'focusout' event of the parent element and causes the menu to hide itself. This happens because the
focusout
event supports event bubbling.Q: How do we get around this?
A1.1: We make the parent element focusable by giving it a tabindex:
This addresses a couple of important things:
div
element a focus outline - this in turn enhances accessibility of the element.div
element - in this example, that wouldfocusout
. If thetabindex
was omitted we would need to add additional JavaScript to compensate for the lost functionality.A1.2: Within the
focusout
event handler, we check if the element which gains focus is a descendant of the parent element, if it is, we don't remove theis-active
class from the menu.Complete example code:
I've forked the original fiddle and modified it to show this in action. You may find the modified fiddle here: http://jsfiddle.net/darshanags/60jnusvk/.
Other helpful information:
I use
event.relatedTarget
to determine the element which gains focus every time thefocusout
event is triggered. More information onevent.relatedTarget
can be found here: https://api.jquery.com/event.relatedTarget/.Update
I've refactored some of my original code, it's much simpler now: http://jsfiddle.net/darshanags/60jnusvk/24/. I will leave this fiddle and the original for reference.