Making a drop-down menu keyboard and mouse browsable - javascript

I am working on a project for a client that requires a custom drop-down menu, accessible by clicking to open it, or using the keyboard to tab across to it.
Currently, my solution works almost perfectly, but there is one problem: the first time you click the drop-down menu, it flashes open and then closes again. I'm sure there's a simple glitch with my JavaScript and jQuery here!
The required functionality is this:
The user must be able to click to open a drop-down menu. (DONE, but with problems)
The user must be able to click another menu, and have the previous one close and the new one open. (DONE)
The user must be able to click anywhere else on the page and have the open menu close. (DONE)
The user must be able to tab along the menu bar, highlighting each link, and opening the menu when it is tabbed to. (DONE)
The tabbing must then tab down the entire menu, before leaving onto the next menu or link, closing the previous menu. (DONE)
As I say here, I have it almost entirely working, but the keyboard focus recognition also detects mouse focusing, meaning that when I click, it triggers a keyboard focus instantly after the mouse one, closing it again.
Or so I think. Please help!
JavaScript Code
$(document).ready(function() {
function toggle(select) {
closeAll(select);
if(select.hasClass("expanded")) {
select.removeClass("expanded");
} else {
select.addClass("expanded");
}
}
function closeAll(select) {
var close = $(document).find("#navigation li.select.expanded");
if(select != null) {
close = close.not(select);
}
close.removeClass("expanded")
}
$("#navigation > ul > li.select > a").click(function(event) {
event.preventDefault();
toggle($(this).parent());
});
$(document).mouseup(function(event) {
var select = $("#navigation > ul > li.select.expanded");
if(!select.is(event.target) && select.has(event.target).length === 0) {
closeAll();
}
});
$(document).on("focus", "#navigation > ul > li > a", function () {
closeAll();
if($(this).parent().hasClass("select")) {
toggle($(this).parent());
}
});
});
HTML Code
<div id="navigation">
<ul>
<li><i class="fa fa-home"></i> Home</li>
<li class="select">
<i class="fa fa-users"></i> Find a Society
<ul>
<li>Scotland</li>
<li>North West</li>
<li>North East</li>
<li>Midlands</li>
<li>Eastern Counties</li>
<li>Central Counties</li>
<li>Wales</li>
<li>South West Counties</li>
<li>Southern Counties</li>
<li>South East Counties</li>
<li>Greater London</li>
</ul>
</li>
</ul>
</div>
The current design

I recently had to create a similar dropdown with some complex features like typeahead list filtering and fixed this issue along the way.
The trick is to only have one entry point into your dropdown menu, every other method has to call this same event. In your case it's the tab focus, so you want to trigger this on click:
EDIT: Fix Open/Close as per OP comment
$("#navigation > ul > li.select > a").click(function(event) {
event.preventDefault();
if($(this).hasClass('expanded')){
closeAll()
}
else {
$(this).focus(); #Here the magic happens :)
}
});
Now the click event triggers a tab focus event which then toggles the visibility of your dropdown.
Closing the dropdown when clicking outside can be simplified with event delegation and the css :not() selector. This should also be slightly faster than your current implementation, though the length of the css selector probably means not by much.
$(document).on('mouseup',':not(#navigation > ul > li.select.expanded)',(function(event) {
closeAll();
});
By the way, your toggle function can be simplified using Jquery's toggleClass
function toggle(select) {
closeAll(select);
select.toggleClass('expanded');
}

Related

Close dropdown menu when clicking outside of it

I've got a problem with the navigation dropdown on my site that I've almost solved but can't quite fix. I'm worried I may have just made a mess out of my code.
When I introduced a "scroll to anchor tags" function with a preventDefault event, it broke my nav menu. The menu wouldn't close unless you clicked on the menu button again. I've finally got it to close after you click one of the links, but that's now the only way to close it. How do I have it close when clicking on the menu button or anywhere else on the site? I'm sure that bit of jQuery is the culprit, but don't know how to fix it or work around it.
HTML for the menu:
<div class="main-nav navbtn">
<div class="dropdown"><i onclick="myFunction()" class="dropbtn material-icons md-48"></i>
<div id="myDropdown" class="dropdown-content">
Home
About
Services
Work
Testimonials
Contact
Blog
</div>
</div>
</div>
And the related jQuery:
// When the user clicks on the button, toggle between hiding and showing the dropdown content
function myFunction() {
document.getElementById("myDropdown").classList.toggle("show");
}
//// Close the dropdown menu if the user clicks outside of it
window.onclick = function (event) {
if (!event.target.matches('.dropbtn')) {
dropdowns.forEach(function (openDropdown) {
dropdown.classList.contains('show') && dropdown.classList.remove('show');
});
}
};
////This is the section I made for it to close after clicking a link
jQuery(document).ready(function ($) {
$('.dropbtn').on('click', function () {
$(".dropdown-content").show();
});
$('.navlink').on('click', function () {
$(".dropdown-content").hide();
});
});
This is the potential problem that's screwing the other functions up.
//Scroll to anchor tags
$(document).on('click', 'a:not(.external)', function (event) {
event.preventDefault();
$('html, body').animate({
scrollTop: $($.attr(this, 'href')).offset().top
}, 500);
});
var $root = $('html, body');
$('a').click(function () {
$root.animate({
scrollTop: $($.attr(this, 'href')).offset().top
}, 500);
return false;
});
What in the world should I do to fix my menu?
You can take a look at the work-in-progress site at http://idpdx.kreigd.com
Update: I think I've got a lead on where things are getting confused. The function I'm using to add the dropdown functionality requires that the class "show" be added once the .dropbtn element is clicked, and removed when it is clicked again.
So what I really need to do is rework the code so that clicking .dropbtn opens the dropdown, and clicking on anything else, including the nav buttons and the .dropbtn element, will close it.
Update 2: Trying a different method. Ignore that first update.
Try this & let me know
$(document).click(function() {
$(".dropdown-content").hide();
// $(".dropdown-content").removeClass("show");
});
$(".dropdown-content").click(function(e) {
e.stopPropagation(); // if you click on the div itself it will cancel the click event.
});
you can try with something like this
$('body').not("#myDropdown").off('click').on('click',function(){$("#myDropdown").removeClass("show")});
I tried the code in your website but you have written some code on window.click which is causing issue.
#Nikhil's answer got me further but has it's downsides. I came up with the following solution:
$(document).click(function (e) {
var target = $(e.target);
// check the actual element being clicked is not the dropdown trigger itself
if (!target.hasClass('dropdown-trigger') && !target.closest('.dropdown-trigger').length) {
// use the framework to close the dropdown instead of just hiding it: hiding it will require you to click the trigger 2 times to reopen!
$('dropdown-trigger').dropdown('close');
}
});

jQuery toggle: Close all open li when another one is clicked

I've got an accordion menu which toggles on click.
This is the code :
$('ul.internal-nav-list li ').on('click', function () {
$(this).find('.internal-sub-list li ').toggle();
});
And the markup looks like this:
<div id="internal-nav">
<ul class="internal-nav-list">
<li><a>products</a>
<ul class="internal-sub-list">
<li>product1</li>
<li><a href="product2.aspx" >product2</a></li>
<li>product3</li>
</ul>
</li>
</ul>
</div>
Now I'm trying to enable that when an li element from the menu is open and the user clicks on another li, the open one will automatically close. Can anybody give me a suggestion on how to do this?
I'v I'm interpreting what you want correctly, try this:
var mainlis = $('.internal.nav.list > li'); // cache selector
mainlis.on('click', function(e) {
e.preventDefault();
var me = $(this);
mainlis.hide();
me.show();
});
The other existing answers come close, but it seems like what you want to do is hide the children of other menu items when the main menu items are clicked. If that is the case, the following will do:
$('.internal-nav-list > li > a').on('click', function () {
var $thisLi = $(this).parents('li');
$thisLi.siblings().find('.internal-sub-list').hide();
$thisLi.find('.internal-sub-list').show();
});
Note the first selector: this restricts the click handler to just the anchor, not the entire li. That means if they click on a child of the currently displayed main menu item, the function will not be called. That way you don't risk having a flicker as the click the submenu items...
In the handler itself, it traverses back to the parent li, finds its siblings and hides their children. Then is shows the submenu for the currently selected main menu.
Note that I took the liberty of hiding the entire ul of the non-selected menus; this should be faster than hiding each child. Perhaps not significantly, but I find it's best practice to perform these kinds of actions on the container rather than all children of the container.
The simplest solution is to close all lielements and open only the one clicked
$('ul.internal-nav-list > li').on('click', function () {
$(this).siblings('li').slideUp();
$(this).slideDown();
});
EDIT As Morfie pointed out, only the immediate children li of the internal-nav-list should be clickable, thus the > operator is used.
Thanks for the suggestions- I got it working this way in the end (in case it helps anyone)
$('ul.internal-nav-list li').on('click', function () {
$close = $(this).find('.internal-sub-list li ').toggle();
$('.internal-sub-list li').not($close).hide()
});

Bootstrap dropdown-menu doesn't get close when clicking an element

I'm firing the bootstrap dropdown menu by using the javascript function dropdown('toggle') as stated in their docs.
Usually dropdowns would hide whenever you click outside them or you select one of their options.
This doesn't happen when firing it through javascript.
In this reproduction you'll see two menus:
One which works as expected as its fired using the "components" trigger
And another one, using the right click, which doesn't work as expected. (it doesn't get closed on click outside or even on element click)
I was able to "manually" get rid of the dropdown menu when clicking outside it by using the following:
$('body').removeClass('open');
But I'm not quite sure why dropdown menus don't work in the same way as when you fire them by the normal procedure.
And having to manually hide them doesn't seem like the best solution...
I got an answer from boostrap issues forum in which they explained how to deal with it:
B. You're missing a <div class="dropdown"> around the <ul class="dropdown-menu">
C. You're missing an element with data-toggle="dropdown" (Not explicitly documented, but followed by all the examples and related to the warning in http://getbootstrap.com/javascript/#callout-dropdowns-data-required )
Here's a reproduction of the solution. (right anywhere click to see the dropdown menu)
HTML markup
<div class="wrapper">
<span data-toggle="dropdown"></span>
<ul class="dropdown-menu" id="menu">
<li>Download file</li>
<li>Upload file</li>
</ul>
</div>
Javascript
//context menu for orders table
$(document).on("contextmenu", "body", function (event) {
//we won't show the default context menu
event.preventDefault();
//showing it close to our cursor
$('#menu').dropdown('toggle').css({
top: (event.pageY) + "px",
left: (event.pageX) + "px"
});
});
Opening in Javascript does not work well with data-toggles. I once used this code to activate the dropdown from Javascript:
$(document).on("contextmenu", "body", function (event) {
//we won't show the default context menu
event.preventDefault();
//showing it close to our cursor
$('#menu').css({
top: (event.pageY) + "px",
left: (event.pageX) + "px"
}).show();
$(document).on('click.contextmenu', function () {
$('#menu').hide();
$(document).off('click.contextmenu');
});
});
I have added a listener on the mouse up to close it without closing it when you click inside, so it closes just when you want or when you click outside it: http://jsfiddle.net/q6po6jzh/1/
$(document).mousedown(function (e) {
var container = $("#menu");
if (!container.is(e.target) && container.has(e.target).length === 0 && container.parent().hasClass('open')) {
container.dropdown('toggle')
container.parent().removeClass('open');
}
});
But if you want to be closed as well when clicked in it I guess #wero answer is probably better.
This will solve your issue. It will close all opened dropdowns.
$('.in,.open').removeClass('in open');
Reference:
https://stackoverflow.com/a/22632931/6488619

Show Hidden Sub-Menu OnClick By Avoiding Link Of Parent Menu

I am working on a project where the most bad thing is that I can't edit HTML code of my project. I can only edit CSS/JavaScript. My project is that I have a list of menu using <ul><li>... having sub-list also in it. The full HTML code is giving below...
<ul class="main_list">
<li class="parent">
Menu-1
<ul class="children">
<li>SubMenu-1</li>
<li>SubMenu-2</li>
<li>SubMenu-3</li>
</ul>
</li>
<li>Menu-2</li>
<li>Menu-3</li>
<li>Menu-4</li>
<li class="parent">
Menu-5
<ul class="children">
<li>SubMenu-1</li>
<li>SubMenu-2</li>
<li>SubMenu-3</li>
</ul>
</li>
</ul>
This is the HTML code that I can not edit. I can only edit or add CSS/JavaScript. Here I want to hide all children menus and will show on click of there parent menu. But when we click on there parent menu then it goes to the external link that is on parent menu. So is there any way or solution for this...???
Update:
Keep in mind that I also want the parent menu link working too. I have an idea to add some text in front to parent menu like show/hide and make some JavaScript to open its children menu and in this case parent menu link will also work if we will click on it directly. Now can we add some text/icon in front of parent menu using JavaScript as I can't edit HTML?
Your click-function:
$('.main_list .parent > a').on('click', function(event){
event.preventDefault();
var href = $(this).attr('href'); // save the href for further use
$(this).siblings('.children').show(); //or whatever show-function you want to use
window.location.href = href; //if you want to user to be redirected
//OR
window.open(href,'_blank'); //for opening a new tab
});
For your second request, you can do somethin like that:
$(document).ready(function{
$('.main_list .parent').prepend('<span class="show">Show</span>');
});
then your selector in the click-handler above would be:
$('.show')
Demo
Cach your parent click event via JavaScript, then use event.preventDefault() to stop redirecting, then u can make some logic to show/hide menu items.
$(document).ready(function(){
$.each('.parent', function(){
$(this).prepend('<div class="clickableBox"></div>');
})
$(document).on('click', '.clickableBox', function(event){
//show/hide logic here
})
})
Based off of my comment above...
Append a drop down arrow next to the menu item for items with children - clicks on the parent item would navigate to it's link but clicks on the arrow would open up the submenu
JSfiddle DEMO
CSS:
ul.children {
display: none;
}
JQUERY:
$(function(){
$('li.parent').each(function(){
$(this).children('a').after('<img src="http://upload.wikimedia.org/wikipedia/commons/f/f7/Arrow-down-navmenu.png" />');
});
$('li.parent img').on("click",function(){
$(this).siblings('ul.children').toggle();
});
});
EDITED JQUERY (with arrow image toggle):
$('li.parent img').on("click",function(){
if($(this).hasClass('open')) {
$(this).removeClass('open');
$(this).attr('src','Arrow-down.png');
} else {
$(this).addClass('open');
$(this).attr('src','Arrow-up.png');
}
$(this).siblings('ul.children').toggle();
});
Updated DEMO
Since I can't see the full HTML, I'm acting like main_list is the only one.
// Get all anchors in menu
var anchors = document.getElementsByClassName('main_list')[0].getElementsByTagName('a');
// Change HREF to do nothing
for(a in anchors){
a.href = "javascript:void(0);
}
// Do other stuff
Make that code run at the end of the page after everything has loaded. Changing the href to javascript:void(0) will make there be no action.
The below works for me :)
jQuery(function( $ ){
$(document).ready(function(){
$('.main_list a').click(function (e) {
if ($(this).parent().children('ul').is(':visible') != true) {
$(this).parent().children('ul').show();
e.preventDefault();
return false;
}
})
});
});

How to temporarily disable a navigation link using jQuery

I'm still learning a bit on javascript/jquery and running into a bump. Every post I pull seems to over-complicate the process, unless it really does require all the extra code.
Here's what I'm doing:
Creating a vertical navigation menu with sliding menu's and static sub-menu's
Using HTML (5) layout with DL, DT and DD
Nav menu is using minor CSS for styling
Nav menu is using jQuery (1.8.3)
I have everything working the way I want, but because I'm picky, I want to temporarily disable the link after a menu is expanded. If I try to click the menu that is already expanded, it slides up and then back down. What I wanted to do is make it so it just doesn't react to a click if it's already expanded.
HTML of Nav Menu:
<dl class="nav2">
<dt>Thing1</dt>
<dd>
<ul>
<li>Test Def1</li>
<li>Test Def2<li>
<li>Test Def3</li>
</ul>
</dd>
<dt>Thing2</dt>
<dd>
<ul>
<li>Test Def4</li>
<li>Test Def5<li>
<li>Test Def6</li>
</ul>
</dd>
<dt>Thing3</dt>
<dd>
<ul>
<li>Test Def7</li>
<li>Test Def8<li>
<li>Test Def9</li>
</ul>
</dd>
</dl>
jQuery for Nav Menu:
$(document).ready(function() {
$("dd:not(:first)").hide();
$("dt a").click(function() {
/* was thinking the added code would go here, but I could be wrong */
$("dd:visible").slideUp("fast");
$(this).parent().next().slideDown("fast");
return false;
});
});
I've tried a few things with bind and one, but due to my confusion with writing js/jquery, I'm not finding my trick. Is there anyway possible to say something like:
$(this:active).unbind("click");
or
if ($(this).active(function() {
$(this).unbind("click");
} else {
$(this).bind("click");
)};
I know I'm probably way off, but I'm trying. Is there any way to change this into javascript/jQuery?
When the DT A is clicked -
make THIS DT / DT A not clickable;
When THIS DT / DT A is no longer expanded or visible -
make THIS DT / DT A clickable;
Thanks for the peak. Sorry if this was found somewhere. Each post I've ran into starts expanding this tiny change into several lines of code, whether attacking the CSS, longer then what seems to be needed javascript/jQuery or both. I just really want to try to keep it contained and simple (if at all possible).
You do not want to disable the click (by disabling the anchor, or unbinding the event). You want to not execute an action when the clicked element is currently selected. Do that like this:
$(document).ready(function() {
var active = $("dd:first");
$("dd").not(active).hide();
$("dt a").click(function() {
var dd = $(this).parent().next();
if (! dd.is(active)) {
active.slideUp("fast");
active = dd.slideDown("fast");
}
return false;
});
});
You could add a temporary class to a link which has been clicked, and then remove it once whatever action the event triggered is complete. Test for the class each time a link is click with hasClass, if it does, do nothing, if it doesn't, do something. I have answered a similar question here:
Suppress jQuery event handling temporarily
Here's how it would work with your code (I believe, though may need modification to suit your needs):
$(document).ready(function() {
$("dd:not(:first)").hide();
$("dt a").click(function(e) {
// Prevent link from doing default action
e.preventDefault();
if ($(this).hasClass('fired') == false) {
// Add 'fired' class to disable link
$(this).addClass('fired');
// Do some stuff
$("dd:visible").slideUp("fast");
$(this).parent().next().slideDown("fast");
// Remove 'fired' class to re-enable the link
// You may need to do this in a call back function
// If you are animating something
$(this).removeClass('fired');
}
// Use preventDefault instead of this
/*return false;*/
});
});

Categories