Become a MacRumors Supporter for $50/year with no ads, ability to filter front page stories, and private forums.

bbiddiscombe

macrumors newbie
Original poster
Apr 7, 2015
4
0
Upfront, I'm not a developer. I have low level html/css skills and almost nothing when it comes to JavaScript. I'm currently in the position of having to build a new website for my organisation and an issue with the JS has presented itself since upgrading to Chrome 50. I'm hoping somebody will be able to offer some advice, these forums have always been helpful in the past (I'm a network engineer by day!).

I'm fairly happy with the site, it looks good, easy to navigate, is fully mobile responsive and the CMS is solid. This issue I am having is with accessibility, specifically with keyboard navigation on a JavaScript dropdown menu (dropotron), after hitting enter on a heading, hitting tab now moves focus along to the next heading rather than the options in the list that appears.

The solution I have been using worked fine across all browsers/OSs. However since updating to the most recent version of Chrome and Firefox (Win and OSX) it has stopped working (IE is still fine however!).

If anyone could give me any pointers I would really appreciate it, I've included the HTML of the menu and the modified dropotron code below. Thanks for reading!

HTML:
<nav id="nav">
<li><a href="#">Curriculum</a>
<ul>
        <li><a href="URLTOPAGE">Art</a></li>   
        <li><a href="URLTOPAGE">GCSE Media Studies</a></li>   
        <li><a href="URLTOPAGE">Humanities &amp; Social Sciences</a></li>     
</ul>
</li>
<ul>
<li><a href="URLTOPAGE">Information</a>
<ul>
        <li><a href="URLTOPAGE">Information Home</a></li>
        <li><a href="URLTOPAGE">Admissions</a></li>
        <li><a href="URLTOPAGE">Behaviour For Learning</a></li>
</ul>
</li>

Code:
/* jquery.dropotron.js v1.4.3 | (c) n33 | n33.co | MIT licensed */

(function($) {

    // Disables selection.
        $.fn.disableSelection_dropotron = function() { return $(this).css('user-select', 'none').css('-khtml-user-select', 'none').css('-moz-user-select', 'none').css('-o-user-select', 'none').css('-webkit-user-select', 'none'); }

    // Dropotron prototype method.
        $.fn.dropotron = function(options) {

            if (this.length == 0)
                return $(this);

            if (this.length > 1)
                for (var i=0; i < this.length; i++)
                    $(this[i]).dropotron(options);

            return $.dropotron($.extend({ selectorParent: $(this) }, options));

        }

    // Dropotron method.
        $.dropotron = function(options) {

            // Settings.
                var settings = $.extend({

                    // Parent jQuery object.
                        selectorParent: null,

                    // Base Z-Index.
                        baseZIndex: 1000,

                    // Menu class (assigned to every <ul>).
                        menuClass: 'dropotron',

                    // Expansion mode ("hover" or "click").
                        expandMode: 'hover',

                    // Hover delay (in ms).
                        hoverDelay: 150,

                    // Hide delay (in ms; 0 disables).
                        hideDelay: 250,

                    // Opener class.
                        openerClass: 'opener',

                    // Active opener class.
                        openerActiveClass: 'active',

                    // Submenu class prefix.
                        submenuClassPrefix: 'level-',

                    // Menu mode ("instant", "fade", "slide", "zoom").
                        mode: 'fade',

                    // Menu speed ("fast", "slow", or ms).
                        speed: 'fast',

                    // Easing mode ("swing", "linear").
                        easing: 'swing',

                    // Alignment ("left", "center", "right").
                        alignment: 'left',

                    // Submenu offset X.
                        offsetX: 0,

                    // Submenu offset Y.
                        offsetY: 0,

                    // Global offset Y.
                        globalOffsetY: 0,

                    // IE Offset X.
                        IEOffsetX: 0,

                    // IE Offset Y.
                        IEOffsetY: 0,

                    // If true and mode = "fade", prevents top-level opener fade.
                        noOpenerFade: true,

                    // Detach second level menus (prevents parent style bleed).
                        detach: true,

                    // If true and detach = true, leave original menu intact.
                        cloneOnDetach: true     

                }, options);

            // Vars.
                var $top = settings.selectorParent,
                    $menus = $top.find('ul'),
                    $body = $('body'),
                    $bodyhtml = $('body,html'),
                    $window = $(window);

                var isLocked = false,
                    hoverTimeoutId = null,
                    hideTimeoutId = null;

            // Main.

                // Top.
                    $top
                        .on('doCollapseAll', function() {
                            $menus.trigger('doCollapse');
                        });

                // Top level menus.
                    $menus.each(function() {

                        var $menu = $(this), $opener = $menu.parent();

                        // If a hideDelay is set, set up the event.
                            if (settings.hideDelay > 0)
                                $menu.add($opener)
                                    .on('mouseleave', function(e) {
                                        window.clearTimeout(hideTimeoutId);
                                        hideTimeoutId = window.setTimeout(function() {
                                            $menu.trigger('doCollapse');
                                        }, settings.hideDelay);
                                    });

                        // Menu.
                            $menu
                                .disableSelection_dropotron()
                                .hide()
                                .addClass(settings.menuClass)
                                .css('position', 'absolute')
                                .on('mouseenter', function(e) {
                                    window.clearTimeout(hideTimeoutId);
                                })
                                .on('doExpand', function() {

                                    // Already visible? Bail out.
                                        if ($menu.is(':visible'))
                                            return false;

                                    // Reset our "hide" timeout.
                                        window.clearTimeout(hideTimeoutId);

                                    // Collapse everything but this menu.
                                        $menus.each(function() {

                                            var $this = $(this);

                                            if (!$.contains($this.get(0), $opener.get(0)))
                                                $this.trigger('doCollapse');

                                        });

                                    // Vars.
                                        var oo = $opener.offset(),
                                            op = $opener.position(),
                                            opp = $opener.parent().position(),
                                            ow = $opener.outerWidth(),
                                            mw = $menu.outerWidth(),
                                            isTL = ($menu.css('z-index') == settings.baseZIndex);

                                        var x, c, left, top;

                                    // If this is a top level menu ...
                                        if (isTL) {

                                            // If detach is enabled, use .position()
                                                if (!settings.detach)
                                                    x = op;
                                            // Otherwise, use .offset()
                                                else
                                                    x = oo;

                                            top = x.top + $opener.outerHeight() + settings.globalOffsetY;
                                            c = settings.alignment;

                                            // Remove alignment classes.
                                                $menu
                                                    .removeClass('left')
                                                    .removeClass('right')
                                                    .removeClass('center');

                                            // Figure out alignment and position.
                                                switch (settings.alignment) {

                                                    case 'right':
                                                        left = x.left - mw + ow;

                                                        if (left < 0) {

                                                            left = x.left;
                                                            c = 'left';

                                                        }

                                                        break;

                                                    case 'center':
                                                        left = x.left - Math.floor((mw - ow) / 2);

                                                        if (left < 0) {

                                                            left = x.left;
                                                            c = 'left';

                                                        }
                                                        else if (left + mw > $window.width()) {

                                                            left = x.left - mw + ow;
                                                            c = 'right';

                                                        }

                                                        break;

                                                    case 'left':
                                                    default:
                                                        left = x.left;

                                                        if (left + mw > $window.width()) {

                                                            left = x.left - mw + ow;
                                                            c = 'right';

                                                        }

                                                        break;

                                                }

                                            // Add alignment class.
                                                $opener.find( 'a' ).blur(); // Take focus off the opening link

$menu
  .addClass(c)
  .find( 'li' )
  .first()
  .find( 'a' )
  .focus(); // After adding classes, give focus to the first anchor tag in the submenu

                                        }

                                    // Otherwise, we're dealing with a submenu.
                                        else {

                                            // If the opener position isn't static ...
                                                if ($opener.css('position') == 'relative'
                                                ||  $opener.css('position') == 'absolute') {

                                                    top = settings.offsetY;
                                                    left = (-1 * op.left);

                                                }
                                                else {

                                                    top = op.top + settings.offsetY;
                                                    left = 0;

                                                }

                                            // Figure out position (based on alignment).
                                                switch (settings.alignment) {

                                                    case 'right':
                                                        left += (-1 * $opener.parent().outerWidth()) + settings.offsetX;

                                                        break;

                                                    case 'center':
                                                    case 'left':
                                                    default:
                                                        left += $opener.parent().outerWidth() + settings.offsetX;

                                                        break;

                                                }

                                        }

                                    // Legacy IE? Apply IE-specific offsets.
                                        if (navigator.userAgent.match(/MSIE ([0-9]+)\./) && RegExp.$1 < 8) {

                                            left += settings.IEOffsetX;
                                            top += settings.IEOffsetY;

                                        }

                                    // Position and show the menu.
                                        $menu
                                            .css('left', left + 'px')
                                            .css('top', top + 'px')
                                            .css('opacity', '0.01')
                                            .show();

                                    // Kludge!
                                        var tmp = false;

                                        // Non-static position fix.
                                            if ($opener.css('position') == 'relative'
                                            ||  $opener.css('position') == 'absolute')
                                                left = (-1 * op.left);
                                            else
                                                left = 0;

                                        if ($menu.offset().left < 0) {

                                            left += $opener.parent().outerWidth() - settings.offsetX;
                                            tmp = true;

                                        }
                                        else if ($menu.offset().left + mw > $window.width()) {

                                            left += (-1 * $opener.parent().outerWidth()) - settings.offsetX;
                                            tmp = true;

                                        }

                                        if (tmp)
                                            $menu
                                                .css('left', left + 'px');

                                        $menu
                                            .hide()
                                            .css('opacity', '1');

                                    // Figure out animation mode.
                                        switch (settings.mode) {

                                            case 'zoom':

                                                isLocked = true;

                                                $opener.addClass(settings.openerActiveClass);
                                                $menu.animate({
                                                    width: 'toggle',
                                                    height: 'toggle'
                                                }, settings.speed, settings.easing, function() {
                                                    isLocked = false;
                                                });

                                                break;

                                            case 'slide':

                                                isLocked = true;

                                                $opener.addClass(settings.openerActiveClass);
                                                $menu.animate({ height: 'toggle' }, settings.speed, settings.easing, function() {
                                                    isLocked = false;
                                                });

                                                break;

                                            case 'fade':

                                                isLocked = true;

                                                if (isTL && !settings.noOpenerFade) {

                                                    var tmp;

                                                    if (settings.speed == 'slow')
                                                        tmp = 80;
                                                    else if (settings.speed == 'fast')
                                                        tmp = 40;
                                                    else
                                                        tmp = Math.floor(settings.speed / 2);

                                                    $opener.fadeTo(tmp, 0.01, function() {
                                                        $opener.addClass(settings.openerActiveClass);
                                                        $opener.fadeTo(settings.speed, 1);
                                                        $menu.fadeIn(settings.speed, function() {
                                                            isLocked = false;
                                                        });
                                                    });

                                                }
                                                else {

                                                    $opener.addClass(settings.openerActiveClass);
                                                    $opener.fadeTo(settings.speed, 1);
                                                    $menu.fadeIn(settings.speed, function() {
                                                        isLocked = false;
                                                    });

                                                }

                                                break;

                                            case 'instant':
                                            default:

                                                $opener.addClass(settings.openerActiveClass);
                                                $menu.show();

                                                break;

                                        }

                                    return false;
                                })
                                .on('doCollapse', function() {

                                    // Not visible? Bail out.
                                        if (!$menu.is(':visible'))
                                            return false;

                                    // Collapse the menu.
                                        $menu.hide();
                                        $opener.removeClass(settings.openerActiveClass);
                                        $menu.find('.' + settings.openerActiveClass).removeClass(settings.openerActiveClass);
                                        $menu.find('ul').hide();

                                    return false;

                                })
                                .on('doToggle', function(e) {

                                    if ($menu.is(':visible'))
                                        $menu.trigger('doCollapse');
                                    else
                                        $menu.trigger('doExpand');

                                    return false;

                                });

                        // Menu's opener.
                            $opener
                                .disableSelection_dropotron()
                                .addClass('opener')
                                .css('cursor', 'pointer')
                                .on('click touchend', function(e) {

                                    // Locked? Bail.
                                        if (isLocked)
                                            return;

                                    // Toggle menu.
                                        e.preventDefault();
                                        e.stopPropagation();
                                        $menu.trigger('doToggle');

                                });

                        // If expandMode is "hover", set up the event.
                            if (settings.expandMode == 'hover')
                                $opener.hover(function(e) {

                                    if (isLocked)   
                                        return;

                                    hoverTimeoutId = window.setTimeout(function() {
                                        $menu.trigger('doExpand');
                                    }, settings.hoverDelay);

                                },
                                function (e) {

                                    window.clearTimeout(hoverTimeoutId);

                                });

                    });

                // All links.
                    $menus.find('a')
                        .css('display', 'block')
                        .on('click touchend', function(e) {

                            // Locked? Bail.
                                if (isLocked)
                                    return;

                            // No href? Prevent default.
                                if ($(this).attr('href').length < 1)
                                    e.preventDefault();

                        });

                // All list items.
                    $top.find('li')
                        .css('white-space', 'nowrap')
                        .each(function() {

                            var $this = $(this), $a = $this.children('a'), $ul = $this.children('ul'),
                                href = $a.attr('href');

                            // If href is blank ("") or a hash (#), prevent the link from doing anything.
                                $a.on('click touchend', function(e) {

                                    if (href.length == 0
                                    ||  href == '#')
                                        e.preventDefault();
                                    else
                                        e.stopPropagation();

                                });

                            // If there is a link but no unordered list ...
                                if ($a.length > 0 && $ul.length == 0)
                                    $this.on('click touchend', function(e) {

                                        if (isLocked)
                                            return;

                                        $top.trigger('doCollapseAll');

                                        e.stopPropagation();

                                    });

                        });

                // Top level list items.
                    $top.children('li').each(function() {

                        var $opener = $(this), $menu = $opener.children('ul'),
                            c;

                        if ($menu.length > 0) {

                            // If we're using detach ...
                                if (settings.detach) {

                                    // If we're cloning on detach ...
                                        if (settings.cloneOnDetach) {

                                            // Make a copy of the menu.
                                                c = $menu.clone();

                                            // Leave it behind
                                                c
                                                    .attr('class', '')
                                                    .hide()
                                                    .appendTo($menu.parent());

                                        }

                                    // Detach the menu and move it to the end of the <body> element.
                                        $menu
                                            .detach()
                                            .appendTo($body);

                                }

                            // Step through menus, assigning z-indexes and submenu class prefixes as necessary.
                                for(var z = settings.baseZIndex, i = 1, y = $menu; y.length > 0; i++) {

                                    y.css('z-index', z++);

                                    if (settings.submenuClassPrefix)
                                        y.addClass(settings.submenuClassPrefix + (z - 1 - settings.baseZIndex));

                                    y = y.find('> li > ul');

                                }

                        }

                    });

                // Window.
                    $window
                        .on('', function() {
                            $top.trigger('doCollapseAll');
                        })
                        .on('keypress', function(e) {

                            // Collapse all menus if the user hits escape.
                                if (!isLocked && e.keyCode == 27) {

                                    e.preventDefault();
                                    $top.trigger('doCollapseAll');

                                }

                        });

                // Body.
                    $bodyhtml
                        .on('click touchend', function() {

                            if (!isLocked)
                                $top.trigger('doCollapseAll');

                        });

        };

})(jQuery);
 
Hi there,

I can't say with certainty why your solution would stop working in Chrome 50 and the latest Firefox, but I'd recommend looking into the tabindex HTML attribute to help with your situation. As MDN says,

"The tabindex global attribute is an integer indicating if the element can take input focus (is focusable), if it should participate to sequential keyboard navigation, and if so, at what position."​

Basically, with the tabindex attribute, you can control the order in which the tab key will jump through focusable items - in your case, links.

I put together a JSFiddle for you to see how tabindex can be used. I just included the original dropotron code, instead of the code in your post. I tested it in Chrome and FF and it looks to be working how you want it to.

One other thing to note is your HTML in your post was missing some beginning and closing tags, which might also be a contributing factor in your code not working. The JSFiddle has a corrected version of your HTML.

Hope this helps in some way! Good luck. :)
 
  • Like
Reactions: bbiddiscombe
Register on MacRumors! This sidebar will go away, and you'll see fewer ads.