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
focusoutevent 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.