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 <[email protected]> wrote:
> Hi
>
> On Tue, Feb 20, 2018 at 7:22 AM, Harshal Dhumal <
> [email protected]> 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') !== '') {