Hi Dave,

Ticket number 3919 refers to keyboard accessibility issues for sub-node
control and back grid but this patch covers only sub-node control. Should I
create new one?
I have added the screenshot having newly added shortcut and updated
keyboard shortcut rst file. Also moved the code changes from backgrid.js to
backgrid.pgadmin.js.

Please find the attached updated patch.

Regards,
Ganesh Jaybhay

On Tue, Jul 9, 2019 at 9:28 PM Dave Page <dp...@pgadmin.org> wrote:

> Hi,
>
> What's the ticket number for this? That's a fair amount of code for a
> one-line description. There are also no doc changes describing how to
> navigate the sub-node control that I can see.
>
> Also; instead of changing the repo used for backgrid, can we not
> un-vendorise it?
>
> On Mon, Jul 8, 2019 at 4:49 PM Ganesh Jaybhay <
> ganesh.jayb...@enterprisedb.com> wrote:
>
>> Hi Hackers,
>>
>> Attached is the patch for keyboard accessibility of sub-node control. It
>> also covers sub-node dialog forward/backward tab navigation with shortcuts.
>>
>> Kindly review.
>>
>> Regards,
>> Ganesh Jaybhay
>>
>
>
> --
> 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/images/preferences_browser_keyboard_shortcuts.png b/docs/en_US/images/preferences_browser_keyboard_shortcuts.png
old mode 100755
new mode 100644
index a4088a8..b0044b9
Binary files a/docs/en_US/images/preferences_browser_keyboard_shortcuts.png and b/docs/en_US/images/preferences_browser_keyboard_shortcuts.png differ
diff --git a/docs/en_US/keyboard_shortcuts.rst b/docs/en_US/keyboard_shortcuts.rst
index 360efb3..6f3727d 100644
--- a/docs/en_US/keyboard_shortcuts.rst
+++ b/docs/en_US/keyboard_shortcuts.rst
@@ -63,6 +63,8 @@ Use the shortcuts below to navigate the tabsets on dialogs:
    +----------------------------+-------------------------------------------------------+
    | Control+Shift+]            | Dialog tab forward                                    |
    +----------------------------+-------------------------------------------------------+
+   | Control+Shift+A            | Add row in Grid                                       |
+   +----------------------------+-------------------------------------------------------+
 
 SQL Editors
 ***********
diff --git a/web/pgadmin/browser/register_browser_preferences.py b/web/pgadmin/browser/register_browser_preferences.py
index 009f1c1..bb5da06 100644
--- a/web/pgadmin/browser/register_browser_preferences.py
+++ b/web/pgadmin/browser/register_browser_preferences.py
@@ -387,3 +387,18 @@ def register_browser_preferences(self):
         category_label=gettext('Keyboard shortcuts'),
         fields=fields
     )
+
+    self.preference.register(
+        'keyboard_shortcuts',
+        'add_grid_row',
+        gettext('Add grid row'),
+        'keyboardshortcut',
+        {
+            'alt': False,
+            'shift': True,
+            'control': True,
+            'key': {'key_code': 65, 'char': 'a'}
+        },
+        category_label=gettext('Keyboard shortcuts'),
+        fields=fields
+    )
diff --git a/web/pgadmin/browser/static/js/keyboard.js b/web/pgadmin/browser/static/js/keyboard.js
index 9ad59b6..b50d025 100644
--- a/web/pgadmin/browser/static/js/keyboard.js
+++ b/web/pgadmin/browser/static/js/keyboard.js
@@ -41,6 +41,7 @@ _.extend(pgBrowser.keyboardNavigation, {
         'direct_debugging': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'direct_debugging').value),
         'drop_multiple_objects': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'grid_menu_drop_multiple').value),
         'drop_cascade_multiple_objects': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'grid_menu_drop_cascade_multiple').value),
+        'add_grid_row': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'add_grid_row').value),
 
       };
       this.shortcutMethods = {
@@ -61,6 +62,7 @@ _.extend(pgBrowser.keyboardNavigation, {
         'bindDirectDebugging': {'shortcuts': this.keyboardShortcut.direct_debugging}, // Sub menu - Direct Debugging
         'bindDropMultipleObjects': {'shortcuts': this.keyboardShortcut.drop_multiple_objects}, // Grid Menu Drop Multiple
         'bindDropCascadeMultipleObjects': {'shortcuts': this.keyboardShortcut.drop_cascade_multiple_objects}, // Grid Menu Drop Cascade Multiple
+        'bindAddGridRow': {'shortcuts': this.keyboardShortcut.add_grid_row}, // Subnode Grid Add Row
       };
       this.bindShortcuts();
     }
@@ -330,6 +332,12 @@ _.extend(pgBrowser.keyboardNavigation, {
       $('button.delete_multiple_cascade').click();
     }
   },
+  bindAddGridRow: function() {
+    let subNode = $(document.activeElement).closest('.object.subnode');
+    if($(subNode).length) {
+      $(subNode).find('.add').click();
+    }
+  },
   isPropertyPanelVisible: function() {
     let isPanelVisible = false;
     _.each(pgAdmin.Browser.docker.findPanels(), (panel) => {
diff --git a/web/pgadmin/static/js/backform.pgadmin.js b/web/pgadmin/static/js/backform.pgadmin.js
index b97fc38..156430d 100644
--- a/web/pgadmin/static/js/backform.pgadmin.js
+++ b/web/pgadmin/static/js/backform.pgadmin.js
@@ -10,8 +10,10 @@
 define([
   'sources/gettext', 'underscore', 'underscore.string', 'jquery',
   'backbone', 'backform', 'backgrid', 'codemirror', 'sources/sqleditor_utils',
+  'sources/keyboard_shortcuts',
   'spectrum', 'pgadmin.backgrid', 'select2', 'bootstrap.toggle',
-], function(gettext, _, S, $, Backbone, Backform, Backgrid, CodeMirror, SqlEditorUtils) {
+], function(gettext, _, S, $, Backbone, Backform, Backgrid, CodeMirror,
+  SqlEditorUtils, keyboardShortcuts) {
 
   var pgAdmin = (window.pgAdmin = window.pgAdmin || {}),
     pgBrowser = pgAdmin.Browser;
@@ -1269,6 +1271,13 @@ define([
 
       var $dialog = gridBody.append(subNodeGrid);
 
+      let preferences = pgBrowser.get_preferences_for_module('browser');
+      let addBtn = $dialog.find('.add');
+      // Add title to the buttons
+      $(addBtn)
+        .attr('title',
+          keyboardShortcuts.shortcut_title(gettext('Add new row'),preferences.add_grid_row));
+
       // Add button callback
       if (!(data.disabled || data.canAdd == false)) {
         $dialog.find('button.add').first().on('click',(e) => {
@@ -1554,6 +1563,14 @@ define([
 
       var $dialog = gridBody.append(subNodeGrid);
 
+      let preferences = pgBrowser.get_preferences_for_module('browser');
+      let addBtn = $dialog.find('.add');
+      // Add title to the buttons
+      $(addBtn)
+        .attr('title',
+          keyboardShortcuts.shortcut_title(gettext('Add new row'),preferences.add_grid_row));
+
+
       // Add button callback
       $dialog.find('button.add').on('click',(e) => {
         e.preventDefault();
diff --git a/web/pgadmin/static/js/backgrid.pgadmin.js b/web/pgadmin/static/js/backgrid.pgadmin.js
index c804609..0d968aa 100644
--- a/web/pgadmin/static/js/backgrid.pgadmin.js
+++ b/web/pgadmin/static/js/backgrid.pgadmin.js
@@ -9,15 +9,18 @@
 
 define([
   'sources/gettext', 'underscore', 'jquery', 'backbone', 'backform', 'backgrid', 'alertify',
-  'moment', 'bignumber', 'bootstrap.datetimepicker', 'backgrid.filter',
-  'bootstrap.toggle',
+  'moment', 'bignumber', 'sources/utils', 'sources/keyboard_shortcuts',
+  'bootstrap.datetimepicker', 'backgrid.filter', 'bootstrap.toggle',
 ], function(
-  gettext, _, $, Backbone, Backform, Backgrid, Alertify, moment, BigNumber
+  gettext, _, $, Backbone, Backform, Backgrid, Alertify, moment, BigNumber,
+  commonUtils, keyboardShortcuts
 ) {
   /*
    * Add mechanism in backgrid to render different types of cells in
    * same column;
    */
+  let pgAdmin = (window.pgAdmin = window.pgAdmin || {}),
+    pgBrowser = pgAdmin.Browser;
 
   // Add new property cellFunction in Backgrid.Column.
   _.extend(Backgrid.Column.prototype.defaults, {
@@ -37,6 +40,18 @@ define([
     },
   });
 
+  // bind shortcut in cell edit mode
+  _.extend(Backgrid.InputCellEditor.prototype.events, {
+    'keydown': function(e) {
+      let preferences = pgBrowser.get_preferences_for_module('browser');
+      if(keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,e)) {
+        pgBrowser.keyboardNavigation.bindAddGridRow();
+      } else {
+        Backgrid.InputCellEditor.prototype.saveOrCancel.apply(this, arguments);
+      }
+    },
+  });
+
   /* Overriding backgrid sort method.
    * As we are getting numeric, integer values as string
    * from server side, but on client side javascript truncates
@@ -151,6 +166,62 @@ define([
         }
       };
     },
+    moveToNextCell: function (model, column, command) {
+      var i = this.collection.indexOf(model);
+      var j = this.columns.indexOf(column);
+      var cell, renderable, editable, m, n;
+
+      // return if model being edited in a different grid
+      if (j === -1) return this;
+
+      this.rows[i].cells[j].exitEditMode();
+
+      if (command.moveUp() || command.moveDown() || command.moveLeft() ||
+          command.moveRight() || command.save()) {
+        var l = this.columns.length;
+        var maxOffset = l * this.collection.length;
+
+        if (command.moveUp() || command.moveDown()) {
+          m = i + (command.moveUp() ? -1 : 1);
+          var row = this.rows[m];
+          if (row) {
+            cell = row.cells[j];
+            if (Backgrid.callByNeed(cell.column.editable(), cell.column, model)) {
+              cell.enterEditMode();
+              model.trigger('backgrid:next', m, j, false);
+            }
+          }
+          else model.trigger('backgrid:next', m, j, true);
+        }
+        else if (command.moveLeft() || command.moveRight()) {
+          var right = command.moveRight();
+          for (var offset = i * l + j + (right ? 1 : -1);
+            offset >= 0 && offset < maxOffset;
+            right ? offset++ : offset--) {
+            m = ~~(offset / l);
+            n = offset - m * l;
+            cell = this.rows[m].cells[n];
+            renderable = Backgrid.callByNeed(cell.column.renderable(), cell.column, cell.model);
+            editable = Backgrid.callByNeed(cell.column.editable(), cell.column, model);
+            if(cell && cell.$el.hasClass('edit-cell') &&
+              !cell.$el.hasClass('privileges') || cell.$el.hasClass('delete-cell')) {
+              model.trigger('backgrid:next', m, n, false);
+              break;
+            } else if (renderable && editable) {
+              cell.enterEditMode();
+              model.trigger('backgrid:next', m, n, false);
+              break;
+            }
+          }
+
+          if (offset == maxOffset) {
+            model.trigger('backgrid:next', ~~(offset / l), offset - m * l, true);
+          }
+        }
+      }
+
+      return this;
+    },
   });
 
   _.extend(Backgrid.Row.prototype, {
@@ -189,7 +260,7 @@ define([
 
   var ObjectCellEditor = Backgrid.Extension.ObjectCellEditor = Backgrid.CellEditor.extend({
     modalTemplate: _.template([
-      '<div class="subnode-dialog" tabindex="1">',
+      '<div class="subnode-dialog" tabindex="0">',
       '    <div class="subnode-body"></div>',
       '</div>',
     ].join('\n')),
@@ -235,6 +306,14 @@ define([
         tabPanelClassName: function() {
           return 'sub-node-form col-sm-12';
         },
+        events: {
+          'keydown': function (event) {
+            let preferences = pgBrowser.get_preferences_for_module('browser');
+            if(keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,event)) {
+              pgBrowser.keyboardNavigation.bindAddGridRow();
+            }
+          },
+        },
       });
 
       this.objectView.render();
@@ -315,12 +394,18 @@ define([
 
       editorOptions['el'] = $(this.el);
       editorOptions['columns_length'] = this.column.collection.length;
-      editorOptions['el'].attr('tabindex', 1);
+      editorOptions['el'].attr('tabindex', 0);
 
       this.listenTo(this.model, 'backgrid:edit', function(model, column, cell, editor) {
         if (column.get('name') == this.column.get('name'))
           editor.extendWithOptions(editorOptions);
       });
+      // Listen for Tab key, open subnode dialog on space key
+      this.$el.on('keydown', function(e) {
+        if (e.keyCode == 32) {
+          $(this).click();
+        }
+      });
     },
     enterEditMode: function() {
       // Notify that we are about to enter in edit mode for current cell.
@@ -342,6 +427,10 @@ define([
           this.$el.html(
             '<i class=\'fa fa-pencil-square subnode-edit-in-process\' title=\'' + _('Edit row') + '\'></i>'
           );
+          let body = $(this.$el).parents()[1],
+            container = $(body).find('.tab-content:first > .tab-pane.active:first');
+          commonUtils.findAndSetFocus(container);
+          pgBrowser.keyboardNavigation.getDialogTabNavigator($(body).find('.subnode-dialog'));
           this.model.trigger(
             'pg-sub-node:opened', this.model, this
           );
@@ -362,14 +451,16 @@ define([
       return this;
     },
     exitEditMode: function() {
-      var index = $(this.currentEditor.objectView.el)
-        .find('.nav-tabs > .active > a[data-toggle="tab"]').first()
-        .data('tabIndex');
-      Backgrid.Cell.prototype.exitEditMode.apply(this, arguments);
-      this.model.trigger(
-        'pg-sub-node:closed', this, index
-      );
-      this.grabFocus = true;
+      if(!_.isUndefined(this.currentEditor) || !_.isEmpty(this.currentEditor)) {
+        var index = $(this.currentEditor.objectView.el)
+          .find('.nav-tabs > .active > a[data-toggle="tab"]').first()
+          .data('tabIndex');
+        Backgrid.Cell.prototype.exitEditMode.apply(this, arguments);
+        this.model.trigger(
+          'pg-sub-node:closed', this, index
+        );
+        this.grabFocus = true;
+      }
     },
     events: {
       'click': function(e) {
@@ -382,6 +473,17 @@ define([
         }
         e.preventDefault();
       },
+      'keydown': function(e) {
+        var model = this.model;
+        var column = this.column;
+        var command = new Backgrid.Command(e);
+
+        if (command.moveLeft()) {
+          setTimeout(function() {
+            model.trigger('backgrid:edited', model, column, command);
+          }, 20);
+        }
+      },
     },
   });
 
@@ -413,7 +515,16 @@ define([
           delete_title,
           delete_msg,
           function() {
+            let tbody = $(that.el).parents('tbody').eq(0);
             that.model.collection.remove(that.model);
+            let row = $(tbody).find('tr');
+            if(row.length > 0) {
+              // set focus to first tr
+              row.first().children()[0].focus();
+            } else {
+              // set focus to add button
+              $(tbody).parents('.subnode').eq(0).find('.add').focus();
+            }
           },
           function() {
             return true;
@@ -427,12 +538,54 @@ define([
         );
       }
     },
+    exitEditMode: function() {
+      this.$el.removeClass('editor');
+    },
     initialize: function() {
       Backgrid.Cell.prototype.initialize.apply(this, arguments);
     },
     render: function() {
+      var self = this;
       this.$el.empty();
+      $(this.$el).attr('tabindex', 0);
       this.$el.html('<i class=\'fa fa-trash\' title=\'' + _('Delete row') + '\'></i>');
+      // Listen for Tab/Shift-Tab key
+      this.$el.on('keydown', function(e) {
+        // with keyboard navigation on space key, mark row for deletion
+        if (e.keyCode == 32) {
+          self.$el.click();
+        }
+        var gotoCell;
+        if (e.keyCode == 9 || e.keyCode == 16) {
+          // go to Next Cell & if Shift is also pressed go to Previous Cell
+          gotoCell = e.shiftKey ? self.$el.prev() : self.$el.next();
+        }
+
+        if (gotoCell) {
+          let command = new Backgrid.Command({
+            key: 'Tab',
+            keyCode: 9,
+            which: 9,
+            shiftKey: e.shiftKey,
+          });
+          setTimeout(function() {
+            // When we have Editable Cell
+            if (gotoCell.hasClass('editable')) {
+              e.preventDefault();
+              e.stopPropagation();
+              self.model.trigger('backgrid:edited', self.model,
+                self.column, command);
+            }
+            else {
+              // When we have Non-Editable Cell
+              self.model.trigger('backgrid:edited', self.model,
+                self.column, command);
+            }
+          }, 20);
+        }
+      });
+
+
       this.delegateEvents();
       return this;
     },
@@ -488,6 +641,7 @@ define([
       'change input': 'onChange',
       'keyup': 'toggleSwitch',
       'blur input': 'exitEditMode',
+      'keydown': 'onKeyDown',
     },
 
     toggleSwitch: function(e) {
@@ -497,6 +651,13 @@ define([
       }
     },
 
+    onKeyDown: function(e) {
+      let preferences = pgBrowser.get_preferences_for_module('browser');
+      if(keyboardShortcuts.validateShortcutKeys(preferences.add_grid_row,e)) {
+        pgBrowser.keyboardNavigation.bindAddGridRow();
+      }
+    },
+
     onChange: function() {
       var model = this.model,
         column = this.column,
@@ -553,7 +714,11 @@ define([
           });
           setTimeout(function() {
             // When we have Editable Cell
-            if (gotoCell.hasClass('editable')) {
+            if (gotoCell.hasClass('editable') && gotoCell.hasClass('edit-cell')) {
+              e.preventDefault();
+              e.stopPropagation();
+              gotoCell.trigger('focus');
+            } else if (gotoCell.hasClass('editable')) {
               e.preventDefault();
               e.stopPropagation();
               self.model.trigger('backgrid:edited', self.model,
@@ -608,8 +773,7 @@ define([
     },
 
     saveOrCancel: function (e) {
-      var model = this.model;
-      var column = this.column;
+      var self = this;
 
       var command = new Backgrid.Command(e);
       var blurred = e.type === 'blur';
@@ -617,10 +781,32 @@ define([
       if (command.moveUp() || command.moveDown() || command.moveLeft() || command.moveRight() ||
           command.save() || blurred) {
 
-        this.exitEditMode();
-        e.preventDefault();
-        e.stopPropagation();
-        model.trigger('backgrid:edited', model, column, command);
+        let gotoCell;
+        // go to Next Cell & if Shift is also pressed go to Previous Cell
+        gotoCell = e.shiftKey ? self.$el.prev() : self.$el.next();
+
+        if (gotoCell) {
+          let command = new Backgrid.Command({
+            key: 'Tab',
+            keyCode: 9,
+            which: 9,
+            shiftKey: e.shiftKey,
+          });
+          setTimeout(function() {
+            // When we have Editable Cell
+            if (gotoCell.hasClass('editable')) {
+              e.preventDefault();
+              e.stopPropagation();
+              self.model.trigger('backgrid:edited', self.model,
+                self.column, command);
+            }
+            else {
+              // When we have Non-Editable Cell
+              self.model.trigger('backgrid:edited', self.model,
+                self.column, command);
+            }
+          }, 20);
+        }
       }
     },
     events: {
diff --git a/web/pgadmin/static/js/dialog_tab_navigator.js b/web/pgadmin/static/js/dialog_tab_navigator.js
index 4472172..37bff6b 100644
--- a/web/pgadmin/static/js/dialog_tab_navigator.js
+++ b/web/pgadmin/static/js/dialog_tab_navigator.js
@@ -46,13 +46,13 @@ class dialogTabNavigator {
 
     if(childTabData) {
       var res = this.navigate(shortcut, childTabData.childTab,
-        childTabData.childTabPane);
+        childTabData.childTabPane, event);
 
       if (!res) {
-        this.navigate(shortcut, this.tabs, currentTabPane);
+        this.navigate(shortcut, this.tabs, currentTabPane, event);
       }
     } else {
-      this.navigate(shortcut, this.tabs, currentTabPane);
+      this.navigate(shortcut, this.tabs, currentTabPane, event);
     }
   }
 
@@ -73,16 +73,16 @@ class dialogTabNavigator {
     return null;
   }
 
-  navigate(shortcut, tabs, tab_pane) {
+  navigate(shortcut, tabs, tab_pane, event) {
     if(shortcut == this.dialogTabBackward) {
-      return this.navigateBackward(tabs, tab_pane);
+      return this.navigateBackward(tabs, tab_pane, event);
     }else if (shortcut == this.dialogTabForward) {
-      return this.navigateForward(tabs, tab_pane);
+      return this.navigateForward(tabs, tab_pane, event);
     }
     return false;
   }
 
-  navigateBackward(tabs, tab_pane) {
+  navigateBackward(tabs, tab_pane, event) {
     var self = this,
       nextTabPane,
       innerTabContainer,
@@ -105,6 +105,7 @@ class dialogTabNavigator {
         self.tabSwitching = false;
       }, 200);
 
+      event.stopPropagation();
       return true;
     }
 
@@ -112,7 +113,7 @@ class dialogTabNavigator {
     return false;
   }
 
-  navigateForward(tabs, tab_pane) {
+  navigateForward(tabs, tab_pane, event) {
     var self = this,
       nextTabPane,
       innerTabContainer,
@@ -135,6 +136,8 @@ class dialogTabNavigator {
         self.tabSwitching = false;
       }, 200);
 
+      event.stopPropagation();
+
       return true;
     }
     this.tabSwitching = false;
diff --git a/web/pgadmin/static/scss/_backgrid.overrides.scss b/web/pgadmin/static/scss/_backgrid.overrides.scss
index b0b7475..41266eb 100644
--- a/web/pgadmin/static/scss/_backgrid.overrides.scss
+++ b/web/pgadmin/static/scss/_backgrid.overrides.scss
@@ -288,6 +288,10 @@ table.backgrid {
     background-color: $color-bg-theme !important;
   }
 
+  & td.edit-cell.editor:focus {
+    outline: $input-focus-border-color auto 5px !important;
+  }
+
   tr.editor-row  {
     background-color: $color-gray-light !important;
     & > td {
diff --git a/web/pgadmin/static/scss/_pgadmin.style.scss b/web/pgadmin/static/scss/_pgadmin.style.scss
index f5a8877..8f1e248 100644
--- a/web/pgadmin/static/scss/_pgadmin.style.scss
+++ b/web/pgadmin/static/scss/_pgadmin.style.scss
@@ -729,10 +729,17 @@ table tr th {
     padding: 0;
   }
   & button:focus {
-    outline: none;
+    outline: $input-focus-border-color auto 5px !important;
   }
 }
 
+table tr td {
+  td.edit-cell:focus,
+  td.delete-cell:focus,
+  td.string-cell:focus {
+    outline: $input-focus-border-color auto 5px !important;
+  }
+}
 
 .privilege_label{
   font-size: 10px!important;

Reply via email to