Combobox can be controlled just by using keyboard.

When value list is closed, user can:
  * use UP and DOWN error to open list, it will focus the list and
    select previous/next value
* when CB is non-editable, user can start typing, first character will open list, second will be entered into search input. Note: I wanted to copy the first char to the search box as well, but I did not figure out reliable method for converting keycode to char for non ASCII keyboard layouts * ESCAPE, ENTER, TAB keys are handled to allow keyboard operations in a container

When value list is opened:
  * CB tries to keep focus on either search input or a select
* when focus is lost, the value list is closed. So user can click anywhere on a page to close it - two comboboxes can't be opened on the same time
  * hitting TAB key switches between search and select
* if CB is not searchable, hitting TAB will close the value list and select input textbox
  * hitting ESCAPE on will close the value list
  * hitting ENTER on search input will invoke search operation
  * hitting ENTER on select will close the value list
  * hitting UP/DOWN arrows will select previous/next values

Additional modifications:
* opening arrow and search button were made non-focusable. It fixes the 'wrong focus area' bug and simplifies keyboard usage. It doesn't affect mouse usage.

https://fedorahosted.org/freeipa/ticket/3324
--
Petr Vobornik
From c81a7f21a3117243e14e4cc98827224e0666b01d Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Thu, 14 Feb 2013 10:18:09 +0100
Subject: [PATCH] Combobox keyboard support

Combobox can be controlled just by using keyboard.

When value list is closed, user can:
  * use UP and DOWN error to open list, it will focus the list and
    select previous/next value
  * when CB is non-editable, user can start typing, first character will open
    list, second will be entered into search input. Note: I wanted to copy the
    first char to the search box as well, but I did not figure out reliable
    method for converting keycode to char for non ASCII keyboard layouts
  * ESCAPE, ENTER, TAB keys are handled to allow keyboard operations in a
    container

When value list is opened:
  * CB tries to keep focus on either search input or a select
  * when focus is lost, the value list is closed. So  user can click anywhere
    on a page to close it - two comboboxes can't be opened on the same time
  * hitting TAB key switches between search and select
  * if CB is not searchable, hitting TAB will close the value list and select
    input textbox
  * hitting ESCAPE on will close the value list
  * hitting ENTER on search input will invoke search operation
  * hitting ENTER on select will close the value list
  * hitting UP/DOWN arrows will select previous/next values

Additional modifications:
  * opening arrow and search button were made non-focusable. It fixes the
    'wrong focus area' bug and simplifies keyboard usage. It doesn't affect
    mouse usage.

https://fedorahosted.org/freeipa/ticket/3324
---
 install/ui/src/freeipa/widget.js | 215 ++++++++++++++++++++++++++++++++++-----
 1 file changed, 187 insertions(+), 28 deletions(-)

diff --git a/install/ui/src/freeipa/widget.js b/install/ui/src/freeipa/widget.js
index dc39c7ecd74fd1bce4d992b286a96cf19ba38aca..b67d6776a3e164ba5fe67b5e477e2c3d35570b4d 100644
--- a/install/ui/src/freeipa/widget.js
+++ b/install/ui/src/freeipa/widget.js
@@ -2088,12 +2088,6 @@ IPA.combobox_widget = function(spec) {
 
         container.addClass('combobox-widget');
 
-        $(document).keyup(function(e) {
-            if (e.which == 27) { // Escape
-                that.close();
-            }
-        });
-
         that.input_container = $('<div/>', {
             'class': 'combobox-widget-input'
         }).appendTo(container);
@@ -2107,41 +2101,48 @@ IPA.combobox_widget = function(spec) {
             type: 'text',
             name: that.name,
             title: that.tooltip,
-            readonly: !that.editable || that.read_only,
-            keyup: function() {
-                that.input_field_changed.notify([], that);
-            },
+            keydown: that.on_input_keydown,
+            mousedown: that.on_no_close,
             click: function() {
+                that.no_close_flag = false;
                 if (that.editable) return false;
                 if (that.is_open()) {
                     that.close();
+                    IPA.select_range(that.input, 0, 0);
                 } else {
                     that.open();
+                    that.list.focus();
                 }
                 return false;
             }
         }).appendTo(that.input_container);
 
-        that.input.bind('input', function() {
-            that.input_field_changed.notify([], that);
-        });
+
+        that.input.bind('input', that.on_input_input);
 
         that.open_button = IPA.action_button({
             name: 'open',
             icon: 'combobox-icon',
+            focusable: false,
             click: function() {
+                that.no_close_flag = false;
                 if (that.is_open()) {
                     that.close();
+                    IPA.select_range(that.input, 0, 0);
                 } else {
                     that.open();
+                    that.list.focus();
                 }
                 return false;
             }
         }).appendTo(that.input_container);
 
+        that.open_button.bind('mousedown', that.on_no_close);
+
         that.list_container = $('<div/>', {
             'class': 'combobox-widget-list',
-            css: { 'z-index': that.z_index }
+            css: { 'z-index': that.z_index },
+            keydown: that.on_list_container_keydown
         }).appendTo(that.input_container);
 
         var div = $('<div/>', {
@@ -2152,24 +2153,28 @@ IPA.combobox_widget = function(spec) {
             that.filter = $('<input/>', {
                 type: 'text',
                 name: 'filter',
-                keypress: function(e) {
-                    if (e.which == 13) { // Enter
-                        var filter = that.filter.val();
-                        that.search(filter);
-                    }
-                }
+                keyup: that.on_filter_keyup,
+                keydown: that.on_filter_keydown,
+                blur: that.list_child_on_blur
             }).appendTo(div);
 
             that.search_button = IPA.action_button({
                 name: 'search',
                 icon: 'search-icon',
+                focusable: false,
                 click: function() {
+                    that.no_close_flag = false;
                     var filter = that.filter.val();
                     that.search(filter);
+                    // focus the list to allow keyboard usage and to allow
+                    // closing on focus lost
+                    that.list.focus();
                     return false;
                 }
             }).appendTo(div);
 
+            that.search_button.bind('mousedown', that.on_no_close);
+
             div.append('<br/>');
         }
 
@@ -2177,7 +2182,10 @@ IPA.combobox_widget = function(spec) {
             name: 'list',
             size: that.size,
             style: 'width: 100%',
-            change: that.select_on_change
+            keydown: that.list_on_keydown,
+            keyup: that.list_on_keyup,
+            change: that.list_on_change,
+            blur: that.list_child_on_blur
         }).appendTo(div);
 
         if (that.undo) {
@@ -2187,21 +2195,152 @@ IPA.combobox_widget = function(spec) {
         that.create_error_link(container);
     };
 
-    that.select_on_change = function() {
+    that.on_no_close = function() {
+        // tell list_child_on_blur that focus lost is caused intentionally
+        that.no_close_flag = true;
+    };
 
-        if (!that.is_open()) return;
+    that.on_input_keydown = function(e) {
+
+        var key = e.which;
+
+        if (key === $.ui.keyCode.TAB ||
+            key === $.ui.keyCode.ESCAPE ||
+            key === $.ui.keyCode.ENTER ||
+            key === $.ui.keyCode.SHIFT ||
+            e.ctrlKey ||
+            e.metaKey ||
+            e.altKey) return true;
+
+        if (that.read_only) {
+            e.preventDefault();
+            return true;
+        }
+
+        if (key === $.ui.keyCode.UP || key === $.ui.keyCode.DOWN) {
+            e.preventDefault();
+            that.open();
+
+            if (key === $.ui.keyCode.UP) {
+                that.select_prev();
+            } else {
+                that.select_next();
+            }
+            that.list.focus();
+            return false;
+        }
+
+        if (!that.editable) {
+            e.preventDefault();
+            that.open();
+            that.filter.focus();
+            return false;
+        }
+
+        that.input_field_changed.notify([], that);
+        return true;
+    };
+
+    that.on_input_input = function(e) {
+        if (!that.editable || that.read_only) {
+            e.preventDefault();
+        } else {
+            that.input_field_changed.notify([], that);
+        }
+    };
+
+    that.on_list_container_keydown = function(e) {
+        // close on ESCAPE and consume event to prevent unwanted
+        // behaviour like closing dialog
+        if (e.which == $.ui.keyCode.ESCAPE) {
+            e.preventDefault();
+            e.stopPropagation();
+            that.close();
+            IPA.select_range(that.input, 0, 0);
+            return false;
+        }
+    };
+
+    that.on_filter_keyup = function(e) {
+        if (e.which == $.ui.keyCode.ENTER) {
+            e.preventDefault();
+            e.stopPropagation();
+
+            var filter = that.filter.val();
+            that.search(filter);
+            return false;
+        }
+    };
+
+    that.on_filter_keydown = function(e) {
+        var key = e.which;
+        if (key === $.ui.keyCode.UP) {
+            e.preventDefault();
+            that.select_prev();
+            that.list.focus();
+        } else if (key === $.ui.keyCode.DOWN) {
+            e.preventDefault();
+            that.select_next();
+            that.list.focus();
+        }
+    };
+
+    that.list_on_keydown = function(e) {
+        if (e.which === $.ui.keyCode.TAB) {
+            e.preventDefault();
+            if (that.searchable) {
+                that.filter.focus();
+            } else {
+                that.input.focus();
+            }
+            return false;
+        }
+    };
+
+    that.list_on_keyup = function(e) {
+        if (e.which === $.ui.keyCode.ENTER || e.which === $.ui.keyCode.SPACE) {
+            e.stopPropagation();
+            that.close();
+            IPA.select_range(that.input, 0, 0);
+            return false;
+        }
+    };
+
+    that.list_on_change = function(e) {
 
         var value = that.list.val();
         that.input.val(value);
-        IPA.select_range(that.input, 0, 0);
-
-        that.close();
         that.value_changed.notify([[value]], that);
     };
 
+    that.list_child_on_blur = function(e) {
+
+        // wait for the browser to focus new element
+        window.setTimeout(function() {
+
+            // close only when focus went outside of list_container
+            if (that.list_container.find(':focus').length === 0 &&
+                    // don't close when clicked on input, open_button or
+                    // search_button their handlers will call close, otherwise
+                    // they would reopen the list_container
+                    !that.no_close_flag) {
+                that.close();
+            }
+        }, 50);
+    };
+
+    that.option_on_click = function(e) {
+        // Close list when user selects and option by click
+        // doesn't work in IE, can be fixed by moving the handler to list.click,
+        // but it breaks UI automation tests. #3014
+        that.close();
+        IPA.select_range(that.input, 0, 0);
+    };
+
     that.open = function() {
-        if (!that.read_only)
+        if (!that.read_only) {
             that.list_container.css('visibility', 'visible');
+        }
     };
 
     that.close = function() {
@@ -2311,6 +2450,22 @@ IPA.combobox_widget = function(spec) {
         that.value_changed.notify([], that);
     };
 
+    that.select_next = function() {
+        var value = that.list.val();
+        var option = $('option[value="'+value+'"]', that.list);
+        var next = option.next();
+        if (!next.length) return;
+        that.select(next.val());
+    };
+
+    that.select_prev = function() {
+        var value = that.list.val();
+        var option = $('option[value="'+value+'"]', that.list);
+        var prev = option.prev();
+        if (!prev.length) return;
+        that.select(prev.val());
+    };
+
     that.save = function() {
         var value = that.input.val();
         return value === '' ? [] : [value];
@@ -2320,7 +2475,7 @@ IPA.combobox_widget = function(spec) {
         var option = $('<option/>', {
             text: label,
             value: value,
-            click: that.select_on_change
+            click: that.option_on_click
         }).appendTo(that.list);
     };
 
@@ -2457,6 +2612,10 @@ IPA.action_button = function(spec) {
         blur: spec.blur
     });
 
+    if (spec.focusable === false) {
+        button.attr('tabindex', '-1');
+    }
+
     if (spec['class']) button.addClass(spec['class']);
 
     if (spec.icon) {
-- 
1.7.11.7

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to