Hi,

On Tue, Feb 27, 2018 at 1:08 AM, Dave Page <dp...@pgadmin.org> wrote:

> Hi,
>
> I'm still able to make it get stuck, if I tab back and forth quickly.
>
Quickly switching tabs was causing to switch to next tab before previous
navigation was completed and
this was leading to lose focus on tab pane.
Now I have made changes that it won't process any user navigation events
until current navigation is completed.


> On Mon, Feb 26, 2018 at 6:04 PM, Harshal Dhumal <
> harshal.dhu...@enterprisedb.com> wrote:
>
>> Hi,
>>
>> Please find the updated patch.
>>
>> On Mon, Feb 26, 2018 at 8:03 PM, Dave Page <dp...@pgadmin.org> wrote:
>>
>>> Hi
>>>
>>> On Mon, Feb 26, 2018 at 12:03 PM, Harshal Dhumal <
>>> harshal.dhu...@enterprisedb.com> wrote:
>>>
>>>> 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.
>>>>
>>>
>>> This is still getting stuck on the Connection tab when I test on the
>>> server dialog.
>>>
>> The disabled input field (Host name/address) on connection tab was
>> causing issue.
>>
>>
>>>
>>> BTW, I've updated the documentation a little - please find the attached
>>> version.
>>>
>> Thanks for this. I have included revised version of documentation in
>> current patch.
>>
>>
>>>
>>> Thanks.
>>>
>>>
>>>
>>>>
>>>>
>>>>
>>>> --
>>>> *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
>>>>>>>
>>>>>>
>>>>>
>>>>
>>>
>>>
>>> --
>>> 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..af840ad 100644
--- a/docs/en_US/keyboard_shortcuts.rst
+++ b/docs/en_US/keyboard_shortcuts.rst
@@ -10,36 +10,51 @@ desired.˝
 
 When using main browser window, the following keyboard shortcuts are available:
 
-+---------------------------+--------------------------------------------------------+
-| Shortcut for all platform | Function                                               |
-+===========================+========================================================+
-| Alt+Shift+F               | Open the File menu                                     |
-+---------------------------+--------------------------------------------------------+
-| Alt+Shift+O               | Open the Object menu                                   |
-+---------------------------+--------------------------------------------------------+
-| Alt+Shift+L               | Open the Tools menu                                    |
-+---------------------------+--------------------------------------------------------+
-| Alt+Shift+H               | Open the Help menu                                     |
-+---------------------------+--------------------------------------------------------+
-| Alt+Shift+B               | Focus the browser tree                                 |
-+---------------------------+--------------------------------------------------------+
-| Alt+Shift+[               | Move tabbed panel backward/forward                     |
-| Alt+Shift+]               |                                                        |
-+---------------------------+--------------------------------------------------------+
-| Alt+Shift+Q               | Open the Query Tool in the current database            |
-+---------------------------+--------------------------------------------------------+
-| Alt+Shift+V               | View Data in the selected table/view                   |
-+---------------------------+--------------------------------------------------------+
-| Alt+Shift+C               | Open the context menu                                  |
-+---------------------------+--------------------------------------------------------+
-| Alt+Shift+N               | Create an object                                       |
-+---------------------------+--------------------------------------------------------+
-| Alt+Shift+E               | Edit object properties                                 |
-+---------------------------+--------------------------------------------------------+
-| Alt+Shift+D               | Delete the object                                      |
-+---------------------------+--------------------------------------------------------+
-| Alt+Shift+G               | Direct debugging                                       |
-+---------------------------+--------------------------------------------------------+
++----------------------------+-------------------------------------------------------+
+| Shortcut for all platforms | Function                                              |
++============================+=======================================================+
+| Alt+Shift+F                | Open the File menu                                    |
++----------------------------+-------------------------------------------------------+
+| Alt+Shift+O                | Open the Object menu                                  |
++----------------------------+-------------------------------------------------------+
+| Alt+Shift+L                | Open the Tools menu                                   |
++----------------------------+-------------------------------------------------------+
+| Alt+Shift+H                | Open the Help menu                                    |
++----------------------------+-------------------------------------------------------+
+| Alt+Shift+B                | Focus the browser tree                                |
++----------------------------+-------------------------------------------------------+
+| Alt+Shift+[                | Move tabbed panel backward                            |
++----------------------------+-------------------------------------------------------+
+| Alt+Shift+]                | Move tabbed panel forward                             |
++----------------------------+-------------------------------------------------------+
+| Alt+Shift+Q                | Open the Query Tool in the current database           |
++----------------------------+-------------------------------------------------------+
+| Alt+Shift+V                | View Data in the selected table/view                  |
++----------------------------+-------------------------------------------------------+
+| Alt+Shift+C                | Open the context menu                                 |
++----------------------------+-------------------------------------------------------+
+| Alt+Shift+N                | Create an object                                      |
++----------------------------+-------------------------------------------------------+
+| Alt+Shift+E                | Edit object properties                                |
++----------------------------+-------------------------------------------------------+
+| Alt+Shift+D                | Delete the object                                     |
++----------------------------+-------------------------------------------------------+
+| Alt+Shift+G                | Direct debugging                                      |
++----------------------------+-------------------------------------------------------+
+
+
+**Dialog tab shortcuts**
+
+Use the shortcuts below to navigate the tabsets on dialogs:
+
++----------------------------+-------------------------------------------------------+
+| Shortcut for all platforms | Function                                              |
++============================+=======================================================+
+| Control+Shift+[            | Dialog tab backward                                   |
++----------------------------+-------------------------------------------------------+
+| Control+Shift+]            | Dialog tab forward                                    |
++----------------------------+-------------------------------------------------------+
+
 
 **SQL Editors**
 
diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py
index 9faa564..ca1edd0 100644
--- a/web/pgadmin/browser/__init__.py
+++ b/web/pgadmin/browser/__init__.py
@@ -433,6 +433,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..8b7f853
--- /dev/null
+++ b/web/pgadmin/static/js/dialog_tab_navigator.js
@@ -0,0 +1,143 @@
+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.tabSwitching = false;
+
+    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 (this.tabSwitching) {
+      return;
+    }
+
+    this.tabSwitching = true;
+
+    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 self = this,
+      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);
+      }
+
+      setTimeout(function() {
+        self.tabSwitching = false;
+      }, 200);
+
+      return true;
+    }
+
+    this.tabSwitching = false;
+    return false;
+  }
+
+  navigateForward(tabs, tab_pane) {
+    var self = this,
+      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);
+      }
+
+      setTimeout(function() {
+        self.tabSwitching = false;
+      }, 200);
+
+      return true;
+    }
+    this.tabSwitching = false;
+    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..026297f
--- /dev/null
+++ b/web/pgadmin/static/js/utils.js
@@ -0,0 +1,38 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// 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:enabled,.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