Hi,

Please find the updated patch for keyboard navigation.

In this patch I have reduced delay which is required until current tab
navigation is completed.
Extracted class dialogTabNavigator and put it in new file.
Added jasmine test cases.
Fixed linting issues, variable naming convention issues.



-- 
*Harshal Dhumal*
*Sr. Software Engineer*

EnterpriseDB India: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

On Wed, Feb 21, 2018 at 11:39 PM, Harshal Dhumal <harshaldhuma...@gmail.com>
wrote:

> Hi,
>
> On Wed, Feb 21, 2018 at 10:59 PM, Joao De Almeida Pereira <
> jdealmeidapere...@pivotal.io> wrote:
>
>> Yep I installed the V2 file
>>
>> On Wed, Feb 21, 2018 at 11:31 AM Dave Page <dp...@pgadmin.org> wrote:
>>
>>> On Wed, Feb 21, 2018 at 4:22 PM, Joao De Almeida Pereira <
>>> jdealmeidapere...@pivotal.io> wrote:
>>>
>>>> Hello Harshal,
>>>>
>>>> I passed the patch through our CI and all the tests passed. The changes
>>>> do not break previous behavior but because there are no tests on the new
>>>> feature  we could not be sure it was really working. So we did some manual
>>>> testing and sometimes it doesn't work, like it gets stuck in a place and
>>>> you need to press the shortcut again in order for it to move.
>>>>
>>>
>>> It stuck because I have to wait until next tab is completely visible
> (fade in effect is completed).
> The fade in or fade out transition duration is 150 ms (set by bootstrap).
> So I can not set focus back to tab pane
> until fade in or fade out transition is completed. May be one improvement
> I can do is to reduce wait time to
> something 200 ms (currently it's 500 ms).
>
> Note that the original issue reported by Dave is already fixed in updated
> patch.
>
>
>> Was that with the updated patch? It sounds like the issue I saw with the
>>> original one.
>>>
>>>
>>>>
>>>> Codewise I have some suggestions:
>>>>  - dialogTabNavigator looks a nice candidate for a class with its own
>>>> file. This way we can test the behavior
>>>>
>>> Ok I'll move dialogTabNavigator to new file and will add test cases.
>
>>  - There is no difference between a variable called e and a variable
>>>> error so for sake of clarity I would love to see variable names that
>>>> we can easily read
>>>>  - We are also using 2 different types of variable naming camelCase and
>>>> snake_case, if we could use only camelCase on Javascript it would make the
>>>> code more uniform
>>>>
>>> Ok I'll do this.
>
>
>>  - I noticed that there are some linting issues in the Javascript code
>>>>
>>> I just found that linter was disabled for this file by adding comment
> /* eslint-disable */ at first line. (not sure why we did that)
>
>
>>>> Summing it up I believe that despite the feature not working 100% of
>>>> the time, looks like the code is almost there but needs some refactoring to
>>>> make it more readable, instead of comments we could have function calls
>>>> that more clearly state what we are looking for something like
>>>> DialogTabNavigator.isLastTabOfChild
>>>> ​
>>>> ​
>>>>
>>>>
>>>> Thanks
>>>> Joao
>>>>
>>>> On Wed, Feb 21, 2018 at 4:32 AM Harshal Dhumal <
>>>> harshal.dhu...@enterprisedb.com> wrote:
>>>>
>>>>> 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
>>>>>>
>>>>>
>>>
>>>
>>> --
>>> 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/keyboard.js b/web/pgadmin/browser/static/js/keyboard.js
index 95b77db..5c71b80 100644
--- a/web/pgadmin/browser/static/js/keyboard.js
+++ b/web/pgadmin/browser/static/js/keyboard.js
@@ -1,7 +1,6 @@
-/* eslint-disable */
-define(
-   ['underscore', 'underscore.string', 'sources/pgadmin', 'jquery', 'mousetrap'],
-function(_, S, pgAdmin, $, Mousetrap) {
+define(['underscore', 'underscore.string', 'sources/pgadmin', 'jquery', 'mousetrap',
+  'sources/utils', 'sources/dialog_tab_navigator'],
+function(_, S, pgAdmin, $, Mousetrap, commonUtils, dialogTabNavigator) {
   'use strict';
 
   var pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {};
@@ -12,27 +11,26 @@ function(_, S, pgAdmin, $, Mousetrap) {
     init: function() {
       Mousetrap.reset();
       if (pgBrowser.preferences_cache.length > 0) {
-        var getShortcut = this.parseShortcutValue;
         this.keyboardShortcut = {
-          'file_shortcut': getShortcut(pgBrowser.get_preference('browser', 'main_menu_file').value),
-          'object_shortcut': getShortcut(pgBrowser.get_preference('browser', 'main_menu_object').value),
-          'tools_shortcut': getShortcut(pgBrowser.get_preference('browser', 'main_menu_tools').value),
-          'help_shortcut': getShortcut(pgBrowser.get_preference('browser', 'main_menu_help').value),
-          'left_tree_shortcut': getShortcut(pgBrowser.get_preference('browser', 'browser_tree').value),
-          'tabbed_panel_backward': getShortcut(pgBrowser.get_preference('browser', 'tabbed_panel_backward').value),
-          'tabbed_panel_forward': getShortcut(pgBrowser.get_preference('browser', 'tabbed_panel_forward').value),
-          'sub_menu_query_tool': getShortcut(pgBrowser.get_preference('browser', 'sub_menu_query_tool').value),
-          'sub_menu_view_data': getShortcut(pgBrowser.get_preference('browser', 'sub_menu_view_data').value),
-          'sub_menu_properties': getShortcut(pgBrowser.get_preference('browser', 'sub_menu_properties').value),
-          '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)
+          'file_shortcut': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'main_menu_file').value),
+          'object_shortcut': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'main_menu_object').value),
+          'tools_shortcut': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'main_menu_tools').value),
+          'help_shortcut': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'main_menu_help').value),
+          'left_tree_shortcut': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'browser_tree').value),
+          'tabbed_panel_backward': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'tabbed_panel_backward').value),
+          'tabbed_panel_forward': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'tabbed_panel_forward').value),
+          'sub_menu_query_tool': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'sub_menu_query_tool').value),
+          'sub_menu_view_data': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'sub_menu_view_data').value),
+          'sub_menu_properties': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'sub_menu_properties').value),
+          'sub_menu_create': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'sub_menu_create').value),
+          'sub_menu_delete': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'sub_menu_delete').value),
+          'context_menu': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'context_menu').value),
+          'direct_debugging': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'direct_debugging').value),
         };
         this.shortcutMethods = {
           'bindMainMenu': {'shortcuts': [this.keyboardShortcut.file_shortcut,
-           this.keyboardShortcut.object_shortcut, this.keyboardShortcut.tools_shortcut,
-           this.keyboardShortcut.help_shortcut]}, // Main menu
+            this.keyboardShortcut.object_shortcut, this.keyboardShortcut.tools_shortcut,
+            this.keyboardShortcut.help_shortcut]}, // Main menu
           'bindRightPanel': {'shortcuts': [this.keyboardShortcut.tabbed_panel_backward, this.keyboardShortcut.tabbed_panel_forward]}, // Main window panels
           'bindMainMenuLeft': {'shortcuts': 'left', 'bindElem': '.pg-navbar'}, // Main menu
           'bindMainMenuRight': {'shortcuts': 'right', 'bindElem': '.pg-navbar'}, // Main menu
@@ -44,9 +42,9 @@ function(_, S, pgAdmin, $, Mousetrap) {
           'bindSubMenuCreate': {'shortcuts': this.keyboardShortcut.sub_menu_create}, // Sub menu - Create Object,
           'bindSubMenuDelete': {'shortcuts': this.keyboardShortcut.sub_menu_delete}, // Sub menu - Delete object,
           'bindContextMenu': {'shortcuts': this.keyboardShortcut.context_menu}, // Sub menu - Open context menu,
-          'bindDirectDebugging': {'shortcuts': this.keyboardShortcut.direct_debugging} // Sub menu - Direct Debugging
+          'bindDirectDebugging': {'shortcuts': this.keyboardShortcut.direct_debugging}, // Sub menu - Direct Debugging
         };
-      this.bindShortcuts();
+        this.bindShortcuts();
       }
     },
     bindShortcuts: function() {
@@ -71,6 +69,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);
@@ -211,18 +223,18 @@ function(_, S, pgAdmin, $, Mousetrap) {
     },
     bindContextMenu: function(e) {
       var tree = this.getTreeDetails(),
-          e = window.event,
-          left = $(e.srcElement).find('.aciTreeEntry').position().left + 70,
-          top = $(e.srcElement).find('.aciTreeEntry').position().top + 70;
+        left = $(e.srcElement).find('.aciTreeEntry').position().left + 70,
+        top = $(e.srcElement).find('.aciTreeEntry').position().top + 70;
+      e = window.event;
 
       tree.t.blur(tree.i);
       $('#tree').blur();
       // Call context menu and set position
-      var ctx = tree.i.children().contextMenu({x: left, y:top});
+      tree.i.children().contextMenu({x: left, y:top});
     },
-    bindDirectDebugging: function(e) {
+    bindDirectDebugging: function() {
       var tree = this.getTreeDetails(),
-          type = tree.t.itemData(tree.i)._type;
+        type = tree.t.itemData(tree.i)._type;
 
       if (!tree.d || (type != 'function' && type != 'procedure'))
         return;
@@ -232,26 +244,23 @@ function(_, S, pgAdmin, $, Mousetrap) {
         pgAdmin.Tools.Debugger.get_function_information(pgAdmin.Browser.Nodes[type]);
       }
     },
-    parseShortcutValue: function(obj) {
-      var shortcut = "";
-      if (obj.alt) { shortcut += 'alt+'; }
-      if (obj.shift) { shortcut += 'shift+'; }
-      if (obj.control) { shortcut += 'ctrl+'; }
-      shortcut += String.fromCharCode(obj.key.key_code).toLowerCase();
-      return shortcut;
-    },
     getTreeDetails: function() {
       var t = pgAdmin.Browser.tree,
-      i = t.selected().length > 0 ? t.selected() : t.first(),
-      d = i && i.length == 1 ? t.itemData(i) : undefined;
+        i = t.selected().length > 0 ? t.selected() : t.first(),
+        d = i && i.length == 1 ? t.itemData(i) : undefined;
 
       return {
         t: t,
         i: i,
-        d: d
-      }
-    }
+        d: d,
+      };
+    },
+    getDialogTabNavigator: function(dialog) {
+      var backward_shortcut = pgBrowser.get_preference('browser', 'dialog_tab_backward').value,
+        forward_shortcut = pgBrowser.get_preference('browser', 'dialog_tab_forward').value;
 
+      return new dialogTabNavigator.dialogTabNavigator(dialog, backward_shortcut, forward_shortcut);
+    },
   });
 
   return pgAdmin.keyboardNavigation;
diff --git a/web/pgadmin/browser/static/js/node.js b/web/pgadmin/browser/static/js/node.js
index 250bd5d..ad58248 100644
--- a/web/pgadmin/browser/static/js/node.js
+++ b/web/pgadmin/browser/static/js/node.js
@@ -1,8 +1,9 @@
 define('pgadmin.browser.node', [
   'sources/gettext', 'jquery', 'underscore', 'underscore.string', 'sources/pgadmin',
   'pgadmin.browser.menu', 'backbone', 'pgadmin.alertifyjs', 'pgadmin.browser.datamodel',
-  'backform', 'sources/browser/generate_url', 'pgadmin.browser.utils', 'pgadmin.backform',
-], function(gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl) {
+  'backform', 'sources/browser/generate_url', 'sources/utils', 'pgadmin.browser.utils',
+  'pgadmin.backform',
+], function(gettext, $, _, S, pgAdmin, Menu, Backbone, Alertify, pgBrowser, Backform, generateUrl, commonUtils) {
 
   var wcDocker = window.wcDocker,
     keyCode = {
@@ -365,9 +366,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');
+            commonUtils.findAndSetFocus(container);
           };
 
           if (!newModel.isNew()) {
@@ -394,6 +394,8 @@ define('pgadmin.browser.node', [
                 view.render();
                 setFocusOnEl();
                 newModel.startNewSession();
+                // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
+                pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
               },
               error: function(xhr, error, message) {
                 var _label = that && item ?
@@ -430,8 +432,11 @@ define('pgadmin.browser.node', [
             view.render();
             setFocusOnEl();
             newModel.startNewSession();
+            // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
+            pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
           }
         }
+
         return view;
       }
 
diff --git a/web/pgadmin/browser/static/js/wizard.js b/web/pgadmin/browser/static/js/wizard.js
index 56b1edf..ef59b00 100644
--- a/web/pgadmin/browser/static/js/wizard.js
+++ b/web/pgadmin/browser/static/js/wizard.js
@@ -1,7 +1,7 @@
 define([
   'underscore', 'jquery', 'backbone', 'sources/pgadmin', 'pgadmin.browser',
-  'sources/gettext',
-], function(_, $, Backbone, pgAdmin, pgBrowser, gettext) {
+  'sources/gettext', 'sources/utils',
+], function(_, $, Backbone, pgAdmin, pgBrowser, gettext, commonUtils) {
 
   var wcDocker = window.wcDocker;
 
@@ -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);
+        commonUtils.findAndSetFocus(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/static/js/dialog_tab_navigator.js b/web/pgadmin/static/js/dialog_tab_navigator.js
new file mode 100644
index 0000000..8e7cf84
--- /dev/null
+++ b/web/pgadmin/static/js/dialog_tab_navigator.js
@@ -0,0 +1,120 @@
+import $ from 'jquery';
+import Mousetrap from 'mousetrap';
+import { findAndSetFocus } from './utils';
+import { parseShortcutValue } from './utils';
+
+class dialogTabNavigator {
+  constructor(dialog, backwardShortcut, forwardShortcut) {
+
+    this.dialog = dialog;
+
+    this.tabs = this.dialog.$el.find('.nav-tabs');
+
+    if (this.tabs.length > 0 ) {
+      this.tabs = this.tabs[0];
+    }
+
+    this.dialogTabBackward = parseShortcutValue(backwardShortcut);
+    this.dialogTabForward = parseShortcutValue(forwardShortcut);
+
+    Mousetrap(this.dialog.el).bind(this.dialogTabBackward, this.onKeyboardEvent.bind(this));
+    Mousetrap(this.dialog.el).bind(this.dialogTabForward, this.onKeyboardEvent.bind(this));
+
+  }
+
+  onKeyboardEvent(event, shortcut) {
+    var currentTabPane =  this.dialog.$el
+        .find('.tab-content:first > .tab-pane.active:first'),
+      childTabData = this.isActivePaneHasChildTabs(currentTabPane);
+
+    if(childTabData) {
+      var res = this.navigate(shortcut, childTabData.childTab,
+        childTabData.childTabPane);
+
+      if (!res) {
+        this.navigate(shortcut, this.tabs, currentTabPane);
+      }
+    } else {
+      this.navigate(shortcut, this.tabs, currentTabPane);
+    }
+  }
+
+  isActivePaneHasChildTabs(currentTabPane) {
+    var childTab = currentTabPane.find('.nav-tabs:first'),
+      childTabPane;
+
+    if (childTab.length > 0) {
+      childTabPane = currentTabPane
+        .find('.tab-content:first > .tab-pane.active:first');
+
+      return {
+        'childTab': childTab,
+        'childTabPane': childTabPane,
+      };
+    }
+
+    return null;
+  }
+
+  navigate(shortcut, tabs, tab_pane) {
+    if(shortcut == this.dialogTabBackward) {
+      return this.navigateBackward(tabs, tab_pane);
+    }else if (shortcut == this.dialogTabForward) {
+      return this.navigateForward(tabs, tab_pane);
+    }
+    return false;
+  }
+
+  navigateBackward(tabs, tab_pane) {
+    var nextTabPane,
+      innerTabContainer,
+      prevtab = $(tabs).find('li.active').prev('li');
+
+    if (prevtab.length > 0) {
+      prevtab.find('a').tab('show');
+
+      nextTabPane = tab_pane.prev();
+      innerTabContainer = nextTabPane
+        .find('.tab-content:first > .tab-pane.active:first');
+
+      if (innerTabContainer.length > 0) {
+        findAndSetFocus(innerTabContainer);
+      } else {
+        findAndSetFocus(nextTabPane);
+      }
+      return true;
+    }
+    return false;
+  }
+
+  navigateForward(tabs, tab_pane) {
+    var nextTabPane,
+      innerTabContainer,
+      nexttab = $(tabs).find('li.active').next('li');
+
+    if(nexttab.length > 0) {
+      nexttab.find('a').tab('show');
+
+      nextTabPane = tab_pane.next();
+      innerTabContainer = nextTabPane
+        .find('.tab-content:first > .tab-pane.active:first');
+
+      if (innerTabContainer.length > 0) {
+        findAndSetFocus(innerTabContainer);
+      } else {
+        findAndSetFocus(nextTabPane);
+      }
+      return true;
+    }
+    return false;
+  }
+
+  detach() {
+    Mousetrap(this.dialog.el).unbind(this.dialogTabBackward);
+    Mousetrap(this.dialog.el).unbind(this.dialogTabForward);
+  }
+}
+
+module.exports = {
+  dialogTabNavigator: dialogTabNavigator,
+};
\ No newline at end of file
diff --git a/web/pgadmin/static/js/utils.js b/web/pgadmin/static/js/utils.js
new file mode 100644
index 0000000..6371449
--- /dev/null
+++ b/web/pgadmin/static/js/utils.js
@@ -0,0 +1,37 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+export function parseShortcutValue(obj) {
+  var shortcut = '';
+  if (obj.alt) { shortcut += 'alt+'; }
+  if (obj.shift) { shortcut += 'shift+'; }
+  if (obj.control) { shortcut += 'ctrl+'; }
+  shortcut += String.fromCharCode(obj.key.key_code).toLowerCase();
+  return shortcut;
+}
+
+export function findAndSetFocus(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();
+    }
+  }, 200);
+}
diff --git a/web/pgadmin/tools/backup/static/js/backup.js b/web/pgadmin/tools/backup/static/js/backup.js
index 367f354..ddbfaee 100644
--- a/web/pgadmin/tools/backup/static/js/backup.js
+++ b/web/pgadmin/tools/backup/static/js/backup.js
@@ -2,9 +2,10 @@
 define([
   'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
   'underscore.string', 'pgadmin.alertifyjs', 'backbone', 'pgadmin.backgrid',
-  'pgadmin.backform', 'pgadmin.browser',
+  'pgadmin.backform', 'pgadmin.browser', 'sources/utils',
 ], function(
-  gettext, url_for, $, _, S, alertify, Backbone, Backgrid, Backform, pgBrowser
+  gettext, url_for, $, _, S, alertify, Backbone, Backgrid, Backform, pgBrowser,
+commonUtils
 ) {
 
   // if module is already initialized, refer to that.
@@ -696,6 +697,9 @@ define([
 
               this.elements.content.appendChild($container.get(0));
 
+              var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
+              commonUtils.findAndSetFocus(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 +944,13 @@ define([
 
               this.elements.content.appendChild($container.get(0));
 
+              if(view) {
+                view.$el.attr('tabindex', -1);
+                // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
+                pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
+                var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
+                commonUtils.findAndSetFocus(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..3058f12 100644
--- a/web/pgadmin/tools/import_export/static/js/import_export.js
+++ b/web/pgadmin/tools/import_export/static/js/import_export.js
@@ -1,9 +1,10 @@
 define([
   'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'underscore.string', 'pgadmin.alertifyjs',
   'sources/pgadmin', 'pgadmin.browser', 'backbone', 'backgrid', 'backform',
-  'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.node.ui',
+  'sources/utils', 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.node.ui',
 ], function(
-  gettext, url_for, $, _, S, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid, Backform
+  gettext, url_for, $, _, S, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,
+Backform, commonUtils
 ) {
 
   pgAdmin = pgAdmin || window.pgAdmin || {};
@@ -652,6 +653,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);
+              pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
+              var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
+              commonUtils.findAndSetFocus(container);
             },
           };
         });
diff --git a/web/pgadmin/tools/maintenance/static/js/maintenance.js b/web/pgadmin/tools/maintenance/static/js/maintenance.js
index 7f67dbf..81e4594 100644
--- a/web/pgadmin/tools/maintenance/static/js/maintenance.js
+++ b/web/pgadmin/tools/maintenance/static/js/maintenance.js
@@ -1,12 +1,12 @@
 define([
   'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
   'underscore.string', 'pgadmin.alertifyjs', 'sources/pgadmin', 'pgadmin.browser', 'backbone',
-  'backgrid', 'backform',
+  'backgrid', 'backform', 'sources/utils',
   'pgadmin.backform', 'pgadmin.backgrid',
   'pgadmin.browser.node.ui',
 ], function(
   gettext, url_for, $, _, S, Alertify, pgAdmin, pgBrowser, Backbone, Backgrid,
-  Backform
+  Backform, commonUtils
 ) {
 
   pgAdmin = pgAdmin || window.pgAdmin || {};
@@ -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');
+              commonUtils.findAndSetFocus(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..5c082a9 100644
--- a/web/pgadmin/tools/restore/static/js/restore.js
+++ b/web/pgadmin/tools/restore/static/js/restore.js
@@ -2,9 +2,10 @@
 define('tools.restore', [
   'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone',
   'underscore.string', 'pgadmin.alertifyjs', 'pgadmin.browser',
-  'pgadmin.backgrid', 'pgadmin.backform',
+  'pgadmin.backgrid', 'pgadmin.backform', 'sources/utils',
 ], function(
-  gettext, url_for, $, _, Backbone, S, alertify, pgBrowser, Backgrid, Backform
+  gettext, url_for, $, _, Backbone, S, alertify, pgBrowser, Backgrid, Backform,
+commonUtils
 ) {
 
   // if module is already initialized, refer to that.
@@ -572,6 +573,12 @@ define('tools.restore', [
 
               this.elements.content.appendChild($container.get(0));
 
+              view.$el.attr('tabindex', -1);
+              // var dialogTabNavigator = pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
+              pgBrowser.keyboardNavigation.getDialogTabNavigator(view);
+              var container = view.$el.find('.tab-content:first > .tab-pane.active:first');
+              commonUtils.findAndSetFocus(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/regression/javascript/dialog_tab_navigator_spec.js b/web/regression/javascript/dialog_tab_navigator_spec.js
new file mode 100644
index 0000000..d4082c8
--- /dev/null
+++ b/web/regression/javascript/dialog_tab_navigator_spec.js
@@ -0,0 +1,115 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+import dialogTabNavigator from 'sources/dialog_tab_navigator';
+import $ from 'jquery';
+import 'bootstrap';
+
+  describe('dialogTabNavigator', function () {
+    let dialog, tabNavigator, backward_shortcut, forward_shortcut;
+
+    beforeEach(() => {
+      let dialogHtml =$('<div tabindex="1" class="backform-tab" role="tabpanel">'+
+        '   <ul class="nav nav-tabs" role="tablist">'+
+        '      <li role="presentation" class="active">'+
+        '         <a data-toggle="tab" tabindex="-1" data-tab-index="1" href="#1" aria-controls="1"> General</a>'+
+        '      </li>'+
+        '     <li role="presentation">'+
+        '         <a data-toggle="tab" tabindex="-1" data-tab-index="5" href="#2" aria-controls="2"> Default Privileges</a>'+
+        '      </li>'+
+        '      <li role="presentation">'+
+        '         <a data-toggle="tab" tabindex="-1" data-tab-index="6" href="#3" aria-controls="3"> SQL</a>'+
+        '      </li>'+
+        '   </ul>'+
+        '   <ul class="tab-content">'+
+        '      <div role="tabpanel" tabindex="-1" class="tab-pane fade collapse in active" id="1">'+
+        '      </div>'+
+        '      <div role="tabpanel" tabindex="-1" class="tab-pane fade collapse" id="2">'+
+        '         <div class="inline-tab-panel" role="tabpanel">'+
+        '            <ul class="nav nav-tabs" role="tablist">'+
+        '               <li role="presentation" class="active">'+
+        '                  <a data-toggle="tab" tabindex="-1" data-tab-index="601" href="#11" aria-controls="11"> Tables</a>'+
+        '               </li>'+
+        '               <li role="presentation">'+
+        '                  <a data-toggle="tab" tabindex="-1" data-tab-index="602" href="#22" aria-controls="22"> Sequences</a>'+
+        '               </li>'+
+        '            </ul>'+
+        '            <ul class="tab-content">'+
+        '               <div role="tabpanel" tabindex="-1" class="tab-pane fade collapse in active" id="11" >'+
+        '               </div>'+
+        '               <div role="tabpanel" tabindex="-1" class="tab-pane fade collapse" id="22">'+
+        '               </div>'+
+        '            </ul>'+
+        '         </div>'+
+        '      </div>'+
+        '      <div role="tabpanel" tabindex="-1" class="tab-pane fade collapse" id="3">'+
+        '      </div>'+
+        '   </ul>'+
+        '</div>');
+
+        dialog = {};
+
+        dialog.el = dialogHtml[0];
+        dialog.$el = dialogHtml;
+
+        backward_shortcut = {
+          'alt': false,
+          'shift': true,
+          'control': true,
+          'key': {'key_code': 91, 'char': '['}
+        };
+
+        forward_shortcut = {
+          'alt': false,
+          'shift': true,
+          'control': true,
+          'key': {'key_code': 93, 'char': ']'}
+        };
+
+        tabNavigator = new dialogTabNavigator.dialogTabNavigator(
+          dialog, backward_shortcut, forward_shortcut);
+    });
+
+    describe('navigate', function () {
+
+      beforeEach(() => {
+        spyOn(tabNavigator, 'navigateBackward').and.callThrough();
+
+        spyOn(tabNavigator, 'navigateForward').and.callThrough();
+      });
+
+      it('navigate backward', function () {
+          tabNavigator.onKeyboardEvent({}, 'shift+ctrl+[');
+
+          expect(tabNavigator.navigateBackward).toHaveBeenCalled();
+
+          expect(tabNavigator.navigateForward).not.toHaveBeenCalled();
+
+      });
+
+      it('navigate forward', function () {
+          tabNavigator.onKeyboardEvent({}, 'shift+ctrl+]');
+
+          expect(tabNavigator.navigateForward).toHaveBeenCalled();
+
+          expect(tabNavigator.navigateBackward).not.toHaveBeenCalled();
+
+      });
+
+      it('should not navigate', function () {
+          tabNavigator.onKeyboardEvent({}, 'shift+ctrl+a');
+
+          expect(tabNavigator.navigateForward).not.toHaveBeenCalled();
+
+          expect(tabNavigator.navigateBackward).not.toHaveBeenCalled();
+
+      });
+
+    });
+
+  });
\ No newline at end of file
diff --git a/web/webpack.shim.js b/web/webpack.shim.js
index b0cf5fd..d36fe61 100644
--- a/web/webpack.shim.js
+++ b/web/webpack.shim.js
@@ -126,6 +126,7 @@ var webpackShimConfig = {
     'pgadmin': path.join(__dirname, './pgadmin/static/js/pgadmin'),
     'translations': path.join(__dirname, './pgadmin/tools/templates/js/translations'),
     'sources/gettext': path.join(__dirname, './pgadmin/static/js/gettext'),
+    'sources/utils': path.join(__dirname, './pgadmin/static/js/utils'),
     'babel-polyfill': path.join(__dirname, './node_modules/babel-polyfill/dist/polyfill'),
 
     // Vendor JS

Reply via email to