Hi, -- *Harshal Dhumal* *Sr. Software Engineer*
EnterpriseDB India: http://www.enterprisedb.com The Enterprise PostgreSQL Company On Tue, Feb 20, 2018 at 10:34 PM, Dave Page <dp...@pgadmin.org> wrote: > Hi > > On Tue, Feb 20, 2018 at 7:22 AM, Harshal Dhumal < > harshal.dhu...@enterprisedb.com> wrote: > >> 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. >> > > > This seems unreliable to me - for example, it keeps getting stuck on the > connection tab on the server properties dialog. > > > Also, can we use the same wording as for the tabbed panel navigation > please? E.g. Next/Previous instead of Forward/Back. > I have fixed all of above issues. Please find updated patch. Thanks, > > -- > Dave Page > Blog: http://pgsnake.blogspot.com > Twitter: @pgsnake > > EnterpriseDB UK: 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..7699798 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 backward | ++---------------------------+--------------------------------------------------------+ +| Control+Shift+] | Dialog tab forward | ++---------------------------+--------------------------------------------------------+ + + **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 1d0374a..d827a49 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_forward', + gettext('Dialog tab forward'), + '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_backward', + gettext('Dialog tab backward'), + '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..cf739bb 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -1951,7 +1951,25 @@ define('pgadmin.browser', [ brace_matching: pgBrowser.utils.braceMatching, indent_with_tabs: pgBrowser.utils.is_indent_with_tabs, }, + find_and_set_focus: function(container) { + if (container.length == 0) { + return; + } + setTimeout(function() { + var first_el = container + .find('button.fa-plus:first'); + + if (first_el.length == 0) { + first_el = container.find('.pgadmin-controls:first>input,.CodeMirror-scroll'); + } + if(first_el.length > 0) { + first_el[0].focus(); + } else { + container[0].focus(); + } + }, 500); + }, }); /* Remove paste event mapping from CodeMirror's emacsy KeyMap binding diff --git a/web/pgadmin/browser/static/js/keyboard.js b/web/pgadmin/browser/static/js/keyboard.js index 95b77db..982ef48 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_backward': getShortcut(pgBrowser.get_preference('browser', 'dialog_tab_backward').value), + 'dialog_tab_forward': getShortcut(pgBrowser.get_preference('browser', 'dialog_tab_forward').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_backward = { + 'shortcuts': self.keyboardShortcut.dialog_tab_backward, + }; + + this.dialog_tab_forward = { + 'shortcuts': self.keyboardShortcut.dialog_tab_forward, + } + self.attachDialogTabNavigatorShortcut(this, this.dialog_tab_backward.shortcuts); + self.attachDialogTabNavigatorShortcut(this, this.dialog_tab_forward.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_backward.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_forward.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_backward.shortcuts, this.dialog.el); + self.detachShortcut(this.dialog_tab_forward.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..5bbbf9f 100644 --- a/web/pgadmin/static/js/backform.pgadmin.js +++ b/web/pgadmin/static/js/backform.pgadmin.js @@ -500,12 +500,12 @@ 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(' ')), 'panel': _.template( - '<div role="tabpanel" class="tab-pane <%=label%> pg-el-sm-12 pg-el-md-12 pg-el-lg-12 pg-el-xs-12 fade" id="<%=cId%>" aria-labelledby="<%=hId%>"></div>' + '<div role="tabpanel" tabindex="-1" class="tab-pane <%=label%> pg-el-sm-12 pg-el-md-12 pg-el-lg-12 pg-el-xs-12 fade" id="<%=cId%>" aria-labelledby="<%=hId%>"></div>' ), }, render: function() { 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') !== '') {