18

I want to be able to use a ul list as an select form element, for styling reasons.

I'm able to populate an hidden input with my code (not included in this jsfiddle), and so far so good.But now I'm trying to let my ul behave like the select input when the keyboard is pressed, or the mouse is used.

In my previous question i had some problems with keyboard controls. They are now fixed. See: Autoscroll on keyboard arrow up/down

The problem that remains is that the mouse is not ignored when the keyboard buttons are pressed. This is causing the "hover effect" to listen to the keyboard input first, but than immediately going to the mouse and select this li item as being selected.

This can be seen in my jsfiddle example: http://jsfiddle.net/JVDXT/3/

My javascript code:

// scrollTo plugin 
  $.fn.scrollTo = function( target, options, callback ){
  if(typeof options == 'function' && arguments.length == 2){ callback = options; options = target; }
  var settings = $.extend({
    scrollTarget  : target,
    offsetTop     : 100,
    duration      : 0,
    easing        : 'linear'
  }, options);
  return this.each(function(){
    var scrollPane = $(this);
    var scrollTarget = (typeof settings.scrollTarget == "number") ? settings.scrollTarget : $(settings.scrollTarget);
    var scrollY = (typeof scrollTarget == "number") ? scrollTarget : scrollTarget.offset().top + scrollPane.scrollTop() - parseInt(settings.offsetTop);
    scrollPane.animate({scrollTop : scrollY }, parseInt(settings.duration), settings.easing, function(){
      if (typeof callback == 'function') { callback.call(this); }
    });
  });
}


//My code
//The function that is listing the the mouse
jQuery(".btn-group .dropdown-menu li").mouseover(function() {
        console.log('mousie')
        jQuery(".btn-group .dropdown-menu li").removeClass('selected');
        jQuery(this).addClass('selected');
})  

//What to do when the keyboard is pressed
jQuery(".btn-group").keydown(function(e) {
    if (e.keyCode == 38) { // up
        console.log('keyup pressed');
        var selected = jQuery('.selected');
        jQuery(".btn-group .dropdown-menu li").removeClass('selected');
        if (selected.prev().length == 0) {
            selected.siblings().last().addClass('selected');
        } else {
            selected.prev().addClass('selected');
            jQuery('.btn-group .dropdown-menu').scrollTo('.selected');
        }
    }
    if (e.keyCode == 40) { // down
        console.log('keydown');
        var selected = jQuery('.selected');
        jQuery(".btn-group .dropdown-menu li").removeClass('selected');
        if (selected.next().length == 0) {
            selected.siblings().first().addClass('selected');
        } else {
            selected.next().addClass('selected');
            jQuery('.btn-group .dropdown-menu').scrollTo('.selected');
        }
    }
});

So could anyone teach me how to igonore the mouse when the keyboard buttons are pressed, but listing to the mouse when it's touched again by the user. Like the default select input form field.

Update

Here's a new jsfiddle.

Community
  • 1
  • 1
Menno van leeuwen
  • 420
  • 1
  • 4
  • 8
  • Do you know about plugins such as jqTransform? http://www.dfc-e.com/metiers/multimedia/opensource/jqtransform/ – Lee Gunn Mar 29 '13 at 10:12
  • Thanks Lee for the suggestion, I'm aware of such plugins. But i think i really need an ul, because of the unique "click" button. I've updated my jsfiddle: http://jsfiddle.net/JVDXT/4/, as you can see i want to span the selected value inside the button. Is this possible with one of the plugins? – Menno van leeuwen Mar 29 '13 at 10:43
  • Just to let you know(in case that you did not know), chosen.js and select2, really good and flexible alternatives for DropDowns – Daniel Aranda May 11 '13 at 14:55
  • Select inputs vary from browser to browser – Qvcool May 12 '13 at 16:23
  • Thanks for all the reactions, but i still prefer the idea of a "carousel" type of selection. Where the highlighted field stays in the center as much as possible =) – Menno van leeuwen May 13 '13 at 14:42
  • funny, a bounty of +250, while he only has 153 :p – DiederikEEn May 13 '13 at 15:24
  • The bounty was subtracted from Ivo van Beek reputation, before this he had 403... The real funny thing is people offering bounties and abandoning their questions, I don't get it, the bounty won't be refunded. – coma May 13 '13 at 17:04
  • This is bad for accessibility. Even with ARIA labels, it's gonna be harder to use for some users. – Mohsen May 13 '13 at 17:32

7 Answers7

13

Check this out:

http://jsfiddle.net/coma/9KvhL/25/

(function($, undefined) {

    $.fn.dropdown = function() {

        var widget = $(this);
        var label = widget.find('span.valueOfButton');
        var list = widget.children('ul');
        var selected;
        var highlighted;

        var select = function(i) {

            selected = $(i);
            label.text(selected.text());

        };

        var highlight = function(i) {

            highlighted = $(i);

            highlighted
            .addClass('selected')
            .siblings('.selected')
            .removeClass('selected');
        };

        var scroll = function(event) {

            list.scrollTo('.selected');

        };

        var hover = function(event) {

            highlight(this);

        };

        var rebind = function(event) {

            bind();

        };

        var bind = function() {

            list.on('mouseover', 'li', hover);
            widget.off('mousemove', rebind);

        };

        var unbind = function() {

            list.off('mouseover', 'li', hover);
            widget.on('mousemove', rebind);

        };

        list.on('click', 'li', function(event) {

            select(this);

        });

        widget.keydown(function(event) {

            unbind();

            switch(event.keyCode) {

                case 38:
                    highlight((highlighted && highlighted.prev().length > 0) ? highlighted.prev() : list.children().last());

                    scroll();
                    break;

                case 40:
                    highlight((highlighted && highlighted.next().length > 0) ? highlighted.next() : list.children().first());

                    scroll();
                    break;

                case 13:
                    if(highlighted) {

                        select(highlighted);

                    }
                    break;

            }

        });

        bind();

    };

    $.fn.scrollTo = function(target, options, callback) {

        if(typeof options === 'function' && arguments.length === 2) {

            callback = options;
            options = target;
        }

        var settings = $.extend({
            scrollTarget  : target,
            offsetTop     : 185,
            duration      : 0,
            easing        : 'linear'
        }, options);

        return this.each(function(i) {

            var scrollPane = $(this);
            var scrollTarget = (typeof settings.scrollTarget === 'number') ? settings.scrollTarget : $(settings.scrollTarget);
            var scrollY = (typeof scrollTarget === 'number') ? scrollTarget : scrollTarget.offset().top + scrollPane.scrollTop() - parseInt(settings.offsetTop, 10);

            scrollPane.animate({scrollTop: scrollY}, parseInt(settings.duration, 10), settings.easing, function() {

                if (typeof callback === 'function') {

                    callback.call(this);
                }

            });

        });

    };

})(jQuery);

$('div.btn-group').dropdown();

The key is to unbind the mouseover and rebind when mouse moves.

I refactored it a little by using a closure function, adding the logic to a jQuery method called dropdown so you can reuse it, using switch instead of a bunch of if's and more things.

Well, there are bazillions of plugins to transform a select to a list:

http://ivaynberg.github.io/select2/

http://harvesthq.github.io/chosen/

http://meetselva.github.io/combobox/

and I have mine too! (ready for touch devices using the same trick as http://uniformjs.com)

https://github.com/coma/jquery.select

But this question is about taking that HTML and make it behave like a select avoiding the hover issue right?

coma
  • 16,429
  • 4
  • 51
  • 76
2

Here's a solution, I'm using mousemove as this will ensure that the right list item is selected as soon as the mouse starts moving again, with mouseover it only starts to select a list item upon entering a new list item:

Take the anonymous function and give it a name:

function mousemove() {
  console.log('mousie')
  jQuery(".btn-group .dropdown-menu li").removeClass('selected');
  jQuery(this).addClass('selected');
}

Declare a global variable mousemoved indicating if the mouse has moved over the document and set it to false, on mousemove over the document, set it to true and attach the mousemove function to the mousemove event on the list items.

var mousemoved = false;

jQuery(document).mousemove(function() {
  if(!mousemoved) {
    $('.btn-group .dropdown-menu li').mousemove(mousemove);  
    mousemoved = true;    
  }
})  

As soon as a key is pressed (at the start of the keydown event), use jQuery's .off() method to remove the mousemove event on the list items if it is present, and set mousemoved to false to ensure the mousemove event doesn't get attached again until the mouse is moved again.

jQuery(".btn-group").keydown(function(e) {

  $('.btn-group .dropdown-menu li').off('mousemove');
  mousemoved = false; 
  ... // Some more of your code

Here's a jsFiddle.

Mathijs Flietstra
  • 12,900
  • 3
  • 38
  • 67
1

I tried to solve your issue by prevent autoscroll, adding tabindex on the li, setting the focus on active, and using a flag to suppress mouse.

Fixed fiddle: http://jsfiddle.net/8nKJT/ [fixed an issue in Chrome ]

http://jsfiddle.net/RDSEt/

The issue is because of the automatic scroll which is triggered on keydown that again triggers mouseenter messes the selection of the li.

Note: The differences with the other approaches(answers here) I noticed is it scrolls on every keypress instead of scrolling only after reaching the top or bottom(normal behavior). You will feel the difference when you check the demo side-by-side.

Below is the list of change description and a small demo to explain how it was fixed,

  • Prevented auto scroll that is triggered on pressing up arrow/down arrow using e.preventDefault() http://jsfiddle.net/TRkAb/ [press up/down on the ul li], Now try the same on http://jsfiddle.net/TRkAb/1/ [No more scroll]
  • Added a flag on keydown to suppress the mouseevents on keydown, this flag is reset onmousemove
  • Added tabindex to li which would allow you to set focus using .focus function. [More info: https://stackoverflow.com/a/6809236/297641 ]
  • Calling .focus would automatically scroll to the desired location. (no need for scrollTo plugin) http://jsfiddle.net/39h3J/ - [Check how it scrolls to li that is on focus]

Check out the demo and code changes too (added few improvements) and let me know.

Also thanks to your question, I noticed this issue and bunch of other issue in one of the plugin I wrote.

I wrote a plugin few months back to filter options and also act exactly like a drop down.

DEMO: http://jsfiddle.net/nxmBQ/ [change filterType to '' to turnoff the filtering ]

The original plugin page is http://meetselva.github.io/combobox/

.. more

Community
  • 1
  • 1
Selvakumar Arumugam
  • 79,297
  • 15
  • 120
  • 134
0

You could use a global to ignore the mouseover event if a keydown was pressed recently on the widget. For example:

var last_key_event = 0;

jQuery(".btn-group .dropdown-menu li").mouseover(function() {
    if ((new Date).getTime() > last_key_event + 1000) {
        console.log('mousie')
        jQuery(".btn-group .dropdown-menu li").removeClass('selected');
        jQuery(this).addClass('selected');
    }
});

Then the keydown handler can set when it was handled to avoid interaction with the mouse:

//What to do when the keyboard is pressed
jQuery(".btn-group").keydown(function(e) {
    last_key_event = (new Date).getTime();
    ...
});

May be it could make sense to have the last_key_event variable separate for each widget instead of being a global.

6502
  • 112,025
  • 15
  • 165
  • 265
0

You could try this solution. It ignores the mousemove event if the coordinates have not changed (since the last mousemove event)

//The function that is listing the the mouse
var lastOffsets = "";

jQuery(".btn-group .dropdown-menu li").mouseover(function(e) {
        var curOffsets = e.clientX+":"+e.clientY;
        if(curOffsets == lastOffsets) {
           // mouse did not really move
            return false;
        }

        lastOffsets = curOffsets;

        ///// rest of your code
}

Updated fiddle to verify if this is what you were after: http://jsfiddle.net/pdW75/1/

lostsource
  • 21,070
  • 8
  • 66
  • 88
0

Approach A reasonable solution should imitate the behavior of other UI elements that serve a similar purpose. On all checked systems (Windows, Linux, major browsers), drop-down boxes behave as follows:

Mousing over an item highlights it. Pressing arrow keys change the selected element, and scroll accoringly. Moving the mouse selects the element underneath. If the selection is empty, pressing down selects the first element. Pressing up selects the last element.

Solution This code illustrates my approach to imitating the described behavior. It's kinda cool, try it...

Additional Considerations There would be a number of other options to suppress unwanted mouse movement to change the selected element. These include:

  • Keeping a state of last input method. If last selection was using the keyboard, hovering over an element will not select it, only clicking will
  • ignoring the mouseover event if the coordinates have not changed by a specified distance, e.g. 10 pixels
  • ignoring mouseover if the user has ever used the keyboard

However, at least for an application accessible to the public, it's always best to stick with established UI patterns.

likeitlikeit
  • 5,563
  • 5
  • 42
  • 56
0

The problem showing up is that when the mouse is left over a part of the expanded list, then selecting using the keys is nullified because the selection made by the keyboard immediately reverts to the item that happens to be under the mouse.

You can solve this problem and retain all functionality without doing any complicated conditional behavior or any removing of event handlers.

Just change your mouseover event handler to be a mousemove event handler. This way any keyboard navigation and selection is listened to and the mouse position is ignored anytime that the user is using the keyboard to select. And anytime the mouse is being used to select, then the mouse is listened to.

This sounds trivial but it seems to make your JS Fiddle behave perfectly and without any conflicting behavior between mouse and keyboard. Like this:

//The function that is listening to the mouse
jQuery(".btn-group .dropdown-menu li").mousemove...

(your code continues unchanged, only replacing mouseover with mousemove)

Joseph Myers
  • 6,434
  • 27
  • 36