Hi, Please find attached patch to enable keyboard navigation in dialog.
To allow navigation from one tab pane (bootstrap tab pane) to another one I have added two new shortcut preferences *Dialog tab previous *( shift+control+[ ) and *Dialog tab next* ( shift+control+] ) for backward and forward tab navigation. Also all dialog controls (within same tab pane) can be navigated using TAB key. -- *Harshal Dhumal* *Sr. Software Engineer* EnterpriseDB India: http://www.enterprisedb.com The Enterprise PostgreSQL Company
diff --git a/docs/en_US/keyboard_shortcuts.rst b/docs/en_US/keyboard_shortcuts.rst index 1142975..f7bf402 100644 --- a/docs/en_US/keyboard_shortcuts.rst +++ b/docs/en_US/keyboard_shortcuts.rst @@ -41,6 +41,21 @@ When using main browser window, the following keyboard shortcuts are available: | Alt+Shift+G | Direct debugging | +---------------------------+--------------------------------------------------------+ + +**Dialog tab shortcuts** + +When any dialog which has bootstrap tabs (nav tabs) below shortcuts are +available to navigate within them: + ++---------------------------+--------------------------------------------------------+ +| Shortcut for all platform | Function | ++===========================+========================================================+ +| Control+Shift+[ | Dialog tab previous | ++---------------------------+--------------------------------------------------------+ +| Control+Shift+] | Dialog tab next | ++---------------------------+--------------------------------------------------------+ + + **SQL Editors** When using the syntax-highlighting SQL editors, the following shortcuts are available: diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index 04065f5..1e74e96 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -430,6 +430,36 @@ class BrowserModule(PgAdminModule): fields=fields ) + self.preference.register( + 'keyboard_shortcuts', + 'dialog_tab_next', + gettext('Dialog tab next'), + 'keyboardshortcut', + { + 'alt': False, + 'shift': True, + 'control': True, + 'key': {'key_code': 93, 'char': ']'} + }, + category_label=gettext('Keyboard shortcuts'), + fields=fields + ) + + self.preference.register( + 'keyboard_shortcuts', + 'dialog_tab_previous', + gettext('Dialog tab previous'), + 'keyboardshortcut', + { + 'alt': False, + 'shift': True, + 'control': True, + 'key': {'key_code': 91, 'char': '['} + }, + category_label=gettext('Keyboard shortcuts'), + fields=fields + ) + def get_exposed_url_endpoints(self): """ Returns: diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js index 13af4ea..e166293 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -1951,6 +1951,29 @@ define('pgadmin.browser', [ brace_matching: pgBrowser.utils.braceMatching, indent_with_tabs: pgBrowser.utils.is_indent_with_tabs, }, + find_and_set_focus: function(container) { + setTimeout(function() { + var first_el = container + .find('button.fa-plus:first'); + + if (first_el.length == 0) { + first_el = container.find('.pgadmin-controls:first') + .find('input,.select2-selection,.CodeMirror-scroll'); + } + + // don't club textarea with input,select2,codemirror, + // as it has least precedence. + + if (first_el.length == 0) { + first_el = container.find('.pgadmin-controls:first').find('textarea'); + } + + if(first_el.length > 0) { + first_el[0].focus(); + } + + }, 500); + }, }); diff --git a/web/pgadmin/browser/static/js/keyboard.js b/web/pgadmin/browser/static/js/keyboard.js index 95b77db..ea775d7 100644 --- a/web/pgadmin/browser/static/js/keyboard.js +++ b/web/pgadmin/browser/static/js/keyboard.js @@ -27,7 +27,9 @@ function(_, S, pgAdmin, $, Mousetrap) { 'sub_menu_create': getShortcut(pgBrowser.get_preference('browser', 'sub_menu_create').value), 'sub_menu_delete': getShortcut(pgBrowser.get_preference('browser', 'sub_menu_delete').value), 'context_menu': getShortcut(pgBrowser.get_preference('browser', 'context_menu').value), - 'direct_debugging': getShortcut(pgBrowser.get_preference('browser', 'direct_debugging').value) + 'direct_debugging': getShortcut(pgBrowser.get_preference('browser', 'direct_debugging').value), + 'dialog_tab_previous': getShortcut(pgBrowser.get_preference('browser', 'dialog_tab_previous').value), + 'dialog_tab_next': getShortcut(pgBrowser.get_preference('browser', 'dialog_tab_next').value), }; this.shortcutMethods = { 'bindMainMenu': {'shortcuts': [this.keyboardShortcut.file_shortcut, @@ -71,6 +73,20 @@ function(_, S, pgAdmin, $, Mousetrap) { attachShortcut: function(shortcut, callback, bindElem) { this._bindWithMousetrap(shortcut, callback, bindElem); }, + attachDialogTabNavigatorShortcut: function(dialogTabNavigator, shortcuts) { + var callback = dialogTabNavigator.on_keyboard_event, + domElem = dialogTabNavigator.dialog.el; + + if (domElem) { + Mousetrap(domElem).bind(shortcuts, function() { + callback.apply(dialogTabNavigator, arguments); + }.bind(domElem)); + } else { + Mousetrap.bind(shortcuts, function() { + callback.apply(dialogTabNavigator, arguments); + }); + } + }, detachShortcut: function(shortcut, bindElem) { if (bindElem) Mousetrap(bindElem).unbind(shortcut); else Mousetrap.unbind(shortcut); @@ -250,8 +266,113 @@ function(_, S, pgAdmin, $, Mousetrap) { i: i, d: d } - } + }, + getDialogTabNavigator: function(dialog) { + var self = this, + dialogTabNavigator = function() {}; + + _.extend(dialogTabNavigator, { + init: function() { + + this.dialog = dialog + + this.tabs = this.dialog.$el.find('.nav-tabs'); + + if (this.tabs.length > 0 ) { + this.tabs = this.tabs[0]; + } + + this.dialog_tab_previous = { + 'shortcuts': self.keyboardShortcut.dialog_tab_previous, + }; + + this.dialog_tab_next = { + 'shortcuts': self.keyboardShortcut.dialog_tab_next, + } + self.attachDialogTabNavigatorShortcut(this, this.dialog_tab_previous.shortcuts); + self.attachDialogTabNavigatorShortcut(this, this.dialog_tab_next.shortcuts); + }, + on_keyboard_event: function(e, shortcut) { + var current_tab_pane = this.dialog.$el + .find('.tab-content:first > .tab-pane.active:first'), + child_tab_data = this.is_active_pane_has_child_tabs(current_tab_pane); + + if(child_tab_data) { + var res = this.navigate(shortcut, child_tab_data.child_tab, + child_tab_data.child_tab_pane); + + // child tab navigation was not successful because we reached + // to either of ends of tabs. + // so navigate parent tabs. + if (!res) { + this.navigate(shortcut, this.tabs, current_tab_pane); + } + } else { + this.navigate(shortcut, this.tabs, current_tab_pane); + } + }, + is_active_pane_has_child_tabs: function (current_tab_pane) { + var child_tab = current_tab_pane.find('.nav-tabs:first'), + child_tab_pane; + + if (child_tab.length > 0) { + child_tab_pane = current_tab_pane + .find('.tab-content:first > .tab-pane.active:first'); + + return { + 'child_tab': child_tab, + 'child_tab_pane': child_tab_pane, + } + } + + return null; + }, + navigate: function(shortcut, tabs, tab_pane) { + if(shortcut == this.dialog_tab_previous.shortcuts) { + var prevtab = $(tabs).find('li.active').prev('li'); + if (prevtab.length > 0) { + prevtab.find('a').tab('show'); + + var next_tab_pane = tab_pane.prev(), + inner_tab_container = next_tab_pane + .find('.tab-content:first > .tab-pane.active:first'); + + if (inner_tab_container.length > 0) { + pgBrowser.find_and_set_focus(inner_tab_container); + } else { + pgBrowser.find_and_set_focus(next_tab_pane); + } + return true; + } + }else if (shortcut == this.dialog_tab_next.shortcuts) { + var nexttab = $(tabs).find('li.active').next('li'); + if(nexttab.length > 0) { + nexttab.find('a').tab('show'); + + var next_tab_pane = tab_pane.next(), + inner_tab_container = next_tab_pane + .find('.tab-content:first > .tab-pane.active:first'); + + if (inner_tab_container.length > 0) { + pgBrowser.find_and_set_focus(inner_tab_container); + } else { + pgBrowser.find_and_set_focus(next_tab_pane); + } + return true; + } + } + return false; + }, + detach: function() { + self.detachShortcut(this.dialog_tab_previous.shortcuts, this.dialog.el); + self.detachShortcut(this.dialog_tab_next.shortcuts, this.dialog.el); + }, + }); + + return dialogTabNavigator; + + }, }); return pgAdmin.keyboardNavigation; diff --git a/web/pgadmin/browser/static/js/node.js b/web/pgadmin/browser/static/js/node.js index 250bd5d..f7ddf04 100644 --- a/web/pgadmin/browser/static/js/node.js +++ b/web/pgadmin/browser/static/js/node.js @@ -365,9 +365,8 @@ define('pgadmin.browser.node', [ } var setFocusOnEl = function() { - setTimeout(function() { - $(el).find('.tab-pane.active:first').find('input:first').focus(); - }, 500); + var container = $(el).find('.tab-content:first > .tab-pane.active:first'); + pgBrowser.find_and_set_focus(container); }; if (!newModel.isNew()) { @@ -394,6 +393,8 @@ define('pgadmin.browser.node', [ view.render(); setFocusOnEl(); newModel.startNewSession(); + var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view); + dialogTabNavigator.init(); }, error: function(xhr, error, message) { var _label = that && item ? @@ -430,8 +431,11 @@ define('pgadmin.browser.node', [ view.render(); setFocusOnEl(); newModel.startNewSession(); + var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view); + dialogTabNavigator.init(); } } + return view; } diff --git a/web/pgadmin/browser/static/js/wizard.js b/web/pgadmin/browser/static/js/wizard.js index 56b1edf..32193c8 100644 --- a/web/pgadmin/browser/static/js/wizard.js +++ b/web/pgadmin/browser/static/js/wizard.js @@ -157,7 +157,8 @@ define([ this.currPage = this.collection.at(this.options.curr_page).toJSON(); }, render: function() { - var data = this.currPage; + var self = this, + data = this.currPage; /* Check Status of the buttons */ this.options.disable_next = (this.options.disable_next ? true : this.evalASFunc(this.currPage.disable_next)); @@ -179,6 +180,11 @@ define([ /* OnLoad Callback */ this.onLoad(); + setTimeout(function() { + var container = $(self.el); + pgBrowser.find_and_set_focus(container); + }, 100); + return this; }, nextPage: function() { diff --git a/web/pgadmin/static/js/backform.pgadmin.js b/web/pgadmin/static/js/backform.pgadmin.js index 2bbaa17..839c912 100644 --- a/web/pgadmin/static/js/backform.pgadmin.js +++ b/web/pgadmin/static/js/backform.pgadmin.js @@ -500,7 +500,7 @@ define([ template: { 'header': _.template([ '<li role="presentation" <%=disabled ? "disabled" : ""%>>', - ' <a data-toggle="tab" data-tab-index="<%=tabIndex%>" href="#<%=cId%>"', + ' <a data-toggle="tab" tabindex="-1" data-tab-index="<%=tabIndex%>" href="#<%=cId%>"', ' id="<%=hId%>" aria-controls="<%=cId%>">', '<%=label%></a></li>', ].join(' ')), diff --git a/web/pgadmin/tools/backup/static/js/backup.js b/web/pgadmin/tools/backup/static/js/backup.js index 367f354..392397e 100644 --- a/web/pgadmin/tools/backup/static/js/backup.js +++ b/web/pgadmin/tools/backup/static/js/backup.js @@ -696,6 +696,9 @@ define([ this.elements.content.appendChild($container.get(0)); + var container = view.$el.find('.tab-content:first > .tab-pane.active:first'); + pgBrowser.find_and_set_focus(container); + // Listen to model & if filename is provided then enable Backup button this.view.model.on('change', function() { if (!_.isUndefined(this.get('file')) && this.get('file') !== '') { @@ -940,6 +943,13 @@ define([ this.elements.content.appendChild($container.get(0)); + if(view) { + view.$el.attr('tabindex', -1); + var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view); + dialogTabNavigator.init(); + var container = view.$el.find('.tab-content:first > .tab-pane.active:first'); + pgBrowser.find_and_set_focus(container); + } // Listen to model & if filename is provided then enable Backup button this.view.model.on('change', function() { if (!_.isUndefined(this.get('file')) && this.get('file') !== '') { diff --git a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js index c0ca2a7..750887e 100644 --- a/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js +++ b/web/pgadmin/tools/grant_wizard/static/js/grant_wizard.js @@ -89,8 +89,10 @@ define([ cell: Backgrid.Extension.SelectRowCell.extend({ render: function() { - // Use the Backform Control's render function - Backgrid.Extension.SelectRowCell.prototype.render.apply(this, arguments); + // Do not use parent's render function. It set's tabindex to -1 on + // checkboxes. + this.$el.empty().append('<input type="checkbox" />'); + this.delegateEvents(); var col = this.column.get('name'); if (this.model && this.model.has(col)) { diff --git a/web/pgadmin/tools/import_export/static/js/import_export.js b/web/pgadmin/tools/import_export/static/js/import_export.js index 070ba6c..746e76f 100644 --- a/web/pgadmin/tools/import_export/static/js/import_export.js +++ b/web/pgadmin/tools/import_export/static/js/import_export.js @@ -652,6 +652,12 @@ define([ // Give the dialog initial height & width this.elements.dialog.style.minHeight = '80%'; this.elements.dialog.style.minWidth = '70%'; + + view.$el.attr('tabindex', -1); + var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view); + dialogTabNavigator.init(); + var container = view.$el.find('.tab-content:first > .tab-pane.active:first'); + pgBrowser.find_and_set_focus(container); }, }; }); diff --git a/web/pgadmin/tools/maintenance/static/js/maintenance.js b/web/pgadmin/tools/maintenance/static/js/maintenance.js index 7f67dbf..996fc37 100644 --- a/web/pgadmin/tools/maintenance/static/js/maintenance.js +++ b/web/pgadmin/tools/maintenance/static/js/maintenance.js @@ -468,6 +468,10 @@ define([ $(reindex_btn).addClass('active'); } + view.$el.attr('tabindex', -1); + var container = view.$el.find('.tab-content:first > .tab-pane.active:first'); + pgBrowser.find_and_set_focus(container); + this.elements.content.appendChild($container.get(0)); }, }; diff --git a/web/pgadmin/tools/restore/static/js/restore.js b/web/pgadmin/tools/restore/static/js/restore.js index 2b44cf0..0ab646e 100644 --- a/web/pgadmin/tools/restore/static/js/restore.js +++ b/web/pgadmin/tools/restore/static/js/restore.js @@ -572,6 +572,12 @@ define('tools.restore', [ this.elements.content.appendChild($container.get(0)); + view.$el.attr('tabindex', -1); + var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view); + dialogTabNavigator.init(); + var container = view.$el.find('.tab-content:first > .tab-pane.active:first'); + pgBrowser.find_and_set_focus(container); + // Listen to model & if filename is provided then enable Backup button this.view.model.on('change', function() { if (!_.isUndefined(this.get('file')) && this.get('file') !== '') {