summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPetr Vobornik <pvoborni@redhat.com>2013-02-14 10:18:09 +0100
committerPetr Vobornik <pvoborni@redhat.com>2013-03-06 12:55:12 +0100
commit32a7801cf448c8a5659d92834b6db9dec381af75 (patch)
treebe17e6cfe5194dc3221f9c5329933a2485e786d3
parent2546e4fd5603cabfe500e881949345ae847f2522 (diff)
downloadfreeipa-32a7801cf448c8a5659d92834b6db9dec381af75.tar.gz
freeipa-32a7801cf448c8a5659d92834b6db9dec381af75.tar.xz
freeipa-32a7801cf448c8a5659d92834b6db9dec381af75.zip
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
-rw-r--r--install/ui/src/freeipa/widget.js213
1 files changed, 186 insertions, 27 deletions
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 = $('<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.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 = $('<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) {