webcomponents - hide dropdown menu on window.onclick

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP


webcomponents - hide dropdown menu on window.onclick



dropdown menu is created in shadowDOM



almost perfect,
but the problem is how to hide the dropdown menu when click on any where else in window


class NavComponent extends HTMLElement {
constructor() {
super();

let template = document.currentScript.ownerDocument.querySelector('template');
let clone = document.importNode(template.content, true);
let root = this.attachShadow({ mode: 'open' });
root.appendChild(clone);
}


connectedCallback() {
let ddc = this.shadowRoot.querySelectorAll('.dropdowncontainer')
let dd = this.shadowRoot.querySelectorAll('.dropdown');
let ddc_length = ddc.length

for (let index = 0; index < ddc_length; index++) {
ddc[index].addEventListener('click', e => {
dd[index].classList.toggle('show');
});
}

/** have to update the code ............ */
window.onclick = function (event) {

} /** END - have to update the code ............ */

}
}

customElements.define('app-nav', NavComponent)



please refer this demo for complete code




2 Answers
2



An unrelated suggestion: you should probably separate .dropdown into its own <app-nav-dropdown> component and assign the 'click' event listeners in its 'constructor' or 'connectedCallback'.


.dropdown


<app-nav-dropdown>



The best idea for your problem is to


ddc[index].addEventListener('click', e => {
if(dd[index].classList.contains('show')) {
dd[index].classList.remove('show');
window.removeEventListener('click', handleDropdownUnfocus, true);
}
else {
dd[index].classList.add('show');
window.addEventListener('click', handleDropdownUnfocus, true);
}
});



Note: I use addEventListener with true, such that the event happens at capture, so that it's will not happen immediately after the .dropdown click handler.


true



And your handleDropdownUnfocus will look like


handleDropdownUnfocus


function handleDropdownUnfocus(e) {
Array.from(document.querySelectorAll('app-nav')).forEach(function(appNav) {
Array.from(appNav.shadowRoot.querySelectorAll('.dropdown')).forEach(function(dd) {
dd.classList.remove('show');
});
});
window.removeEventListener('click', handleDropdownUnfocus, true);
}



There's a problem with this solution though, that if you click on the menu item again after opening it, it will call both .dropdown and window handlers, and the net result will be that the dropdown will remain open. To fix that, you would normally add a check in handleDropdownUnfocus:


.dropdown


window


handleDropdownUnfocus


if(e.target.closest('.dropdown')) return;



However, it will not work. Even when you click on .dropdown, your e.target will be app-nav element, due to Shadow DOM. Which makes it more difficult to do the toggling. I don't know how you'd address this issue, maybe you can come up with something fancy, or stop using Shadow DOM.


.dropdown


e.target


app-nav



Also, your code has some red flags... For example, you use let keyword in your for-loop, which is fine. The support for let is very limited still, so you're more likely going to transpile. The transpiler will just change every let to var. However, if you use var in your loop, assigning the handlers in a loop like that will not work anymore, because index for every handler will refer to the last dropdown (because index will now be global within the function's context and not local for every loop instance).


let


let


let


var


var


index



The best solution, as @guitarino suggested is to define a dropdown menu custom element.



When the menu is clicked, call a (first) event handler that will show/hide the menu, and also add/remove a (second) dropdown event handler on window.


dropdown


window



At its turn, the (second) dropdown event handler will call the first event handler only if the action is outside the custom element itself.


dropdown


connectedCallback()
{
//mousedown anywhere
this.mouse_down = ev => !this.contains( ev.target ) && toggle_menu()

//toggle menu and window listener
var toggle_menu = () =>
{
if ( this.classList.toggle( 'show' ) )
window.addEventListener( 'mousedown', this.mouse_down )
else
window.removeEventListener( 'mousedown', this.mouse_down )
}

//click on menu
this.addEventListener( 'click', toggle_menu )
}



It works with or without Shadow DOM:




customElements.define( 'drop-menu', class extends HTMLElement
{
constructor ()
{
super()
this.attachShadow( { mode: 'open'} )
.innerHTML = '<slot></slot>'
}

connectedCallback()
{
//mousedown anywhere
this.mouse_down = ev => !this.contains( ev.target ) && toggle_menu()

//toggle menu and window listener
var toggle_menu = () =>
{
if ( this.classList.toggle( 'show' ) )
window.addEventListener( 'mousedown', this.mouse_down )
else
window.removeEventListener( 'mousedown', this.mouse_down )
}

//click on menu
this.addEventListener( 'click', toggle_menu )
}

disconnectedCallback ()
{
this.removeEventListener( 'mousedown', this.mouse_down )
}
} )


drop-menu {
position: relative ;
cursor: pointer ;
display: inline-block ;
}

drop-menu > output {
border: 1px solid #ccc ;
padding: 2px 5px ;
}

drop-menu > ul {
box-sizing: content-box ;
position: absolute ;
top: 2px ; left: 5px ;
width: 200px;
list-style: none ;
border: 1px solid #ccc ;
padding: 0 ;
opacity: 0 ;
transition: all 0.2s ease-in-out ;
background: white ;
visibility: hidden ;
z-index: 2 ;

}

drop-menu.show > ul {
opacity: 1 ;
visibility: visible ;
}

drop-menu > ul > li {
overflow: hidden ;
transition: font 0.2s ease-in-out ;
padding: 2px 5px ;
background-color: #e7e7e7;
}

drop-menu:hover {
cursor: pointer;
background-color: #f2f2f2;
}

drop-menu ul li:hover {
background-color: #e0e0e0;
}

drop-menu ul li span {
float: right;
color: #f9f9f9;
background-color: #f03861;
padding: 2px 5px;
border-radius: 3px;
text-align: center;
font-size: .8rem;
}

drop-menu ul li:hover span {
background-color: #ee204e;
}


<drop-menu><output>Services</output>
<ul>
<li>Graphic desing</li>
<li>web design</li>
<li>app design</li>
<li>theme</li>
</ul>
</drop-menu>
<drop-menu><output>tutorial</output>
<ul>
<li>css <span>12 available</span></li>
<li>php <span>10 available</span></li>
<li>javascript <span>40 available</span></li>
<li>html <span>20 available</span></li>
</ul>
</drop-menu>






By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Popular posts from this blog

Arduino Mega cannot recieve any sketches, stk500_recv() programmer is not responding

Visual Studio Code: How to configure includePath for better IntelliSense results

C++ virtual function: Base class function is called instead of derived