From 32a7801cf448c8a5659d92834b6db9dec381af75 Mon Sep 17 00:00:00 2001 From: Petr Vobornik Date: Thu, 14 Feb 2013 10:18:09 +0100 Subject: 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 | 213 ++++++++++++++++++++++++++++++++++----- 1 file changed, 186 insertions(+), 27 deletions(-) (limited to 'install') diff --git a/install/ui/src/freeipa/widget.js b/install/ui/src/freeipa/widget.js index dc39c7ecd..b67d6776a 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 = $('
', { '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 = $('
', { '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 = $('
', { @@ -2152,24 +2153,28 @@ IPA.combobox_widget = function(spec) { that.filter = $('', { 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('
'); } @@ -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.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(); - that.value_changed.notify([[value]], that); + 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 = $('