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.