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') !== '') {

Reply via email to