summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
authorDavid Monjoie <dmo@odoo.com>2019-03-25 15:29:32 +0000
committerAaron Bohy <aab@odoo.com>2019-04-02 11:57:00 +0000
commit8ea5a40417c2da2e82db66ad36fbf1e8e78bbe11 (patch)
tree1233647e18260747f3fd315f79f4cae8b06bc0f6 /addons/web/static/src
parentca19b17fd3e09d2f4e53208542f4356d3a6ee25a (diff)
[IMP] web: list: directional keys navigation in readonly
With this rev., it is now possible to navigate with arrows in list views (except when editing, as in this case hitting the arrows moves the cursor in the input/textarea), and to expand/collapse groups with keyboard navigation. Part of task 1915702
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js9
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js51
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js145
-rw-r--r--addons/web/static/src/scss/list_view.scss8
-rw-r--r--addons/web/static/src/scss/list_view_extra.scss4
5 files changed, 188 insertions, 29 deletions
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index c4d1ca8959f..5db0323c87d 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -706,9 +706,16 @@ var ListController = BasicController.extend({
* @param {OdooEvent} ev
*/
_onToggleGroup: function (ev) {
+ var self = this;
this.model
.toggleGroup(ev.data.group.id)
- .then(this.update.bind(this, {}, {keepSelection: true, reload: false}));
+ .then(function () {
+ self.update({}, {keepSelection: true, reload: false}).then(function () {
+ if (ev.data.onSuccess) {
+ ev.data.onSuccess();
+ }
+ });
+ });
},
});
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index 040a186d4ee..9f37fcecbea 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -645,7 +645,7 @@ ListRenderer.include({
// row of the group)
if (options.forceCreate || record.isDirty()) {
// if we modified the current record, add a row to create a new record
- groupId = $nextRow.data('groupID');
+ groupId = $nextRow.data('group-id');
} else {
// if we didn't change anything to the current line (e.g. we pressed TAB on
// each cell without modifying/entering any data), we discard that line (if
@@ -742,13 +742,14 @@ ListRenderer.include({
var $groupBody = result[0];
var $a = $('<a href="#" role="button">')
.text(_t("Add a line"))
- .data('groupID', group.id);
+ .attr('data-group-id', group.id);
var $td = $('<td>')
.attr('colspan', this._getNumberOfCols())
.addClass('o_group_field_row_add')
+ .attr('tabindex', -1)
.append($a);
var $tr = $('<tr>', {class: 'o_add_record_row'})
- .data('groupID', group.id)
+ .attr('data-group-id', group.id)
.append($td);
$groupBody.append($tr.prepend($('<td>').html('&nbsp;')));
}
@@ -1037,7 +1038,8 @@ ListRenderer.include({
ev.stopPropagation();
var self = this;
- var groupId = $(ev.target).data('groupID');
+ var groupId = $(ev.target).data('group-id');
+ this.currentGroupId = groupId;
this.unselectRow().then(function () {
self.trigger_up('add_record', {
groupId: groupId,
@@ -1098,6 +1100,35 @@ ListRenderer.include({
this.unselectRow();
},
/**
+ * Manages the keyboard events on the list. If the list is not editable, when the user navigates to
+ * a cell using the keyboard, if he presses enter, enter the model represented by the line
+ *
+ * @private
+ * @param {KeyboardEvent} ev
+ * @override
+ */
+ _onKeyDown: function (ev) {
+ var $target = $(ev.currentTarget);
+ var $tr = $target.closest('tr');
+
+ if (this.editable && ev.keyCode === $.ui.keyCode.ENTER && $tr.hasClass('o_selected_row')) {
+ // enter on a textarea for example, let it bubble
+ return;
+ }
+
+ if (this.editable && ev.keyCode === $.ui.keyCode.ENTER && !$tr.hasClass('o_selected_row') && !$tr.hasClass('o_group_header')) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ if ($target.closest('td').hasClass('o_group_field_row_add')) {
+ this._onAddRecordToGroup(ev);
+ } else {
+ this._onCellClick(ev);
+ }
+ } else {
+ this._super.apply(this, arguments);
+ }
+ },
+ /**
* @private
* @param {KeyDownEvent} e
*/
@@ -1180,10 +1211,20 @@ ListRenderer.include({
// prevent from closing the potential dialog containing this list
// also auto-focus the 1st control, if any.
ev.data.originalEvent.stopPropagation();
+ var rowIndex = this.currentRow;
+ var cellIndex = this.currentFieldIndex + 1;
this.trigger_up('discard_changes', {
recordID: ev.target.dataPointID,
onSuccess: function () {
- self.$('.o_field_x2many_list_row_add a:first').focus(); // FIXME
+ var recordId = self._getRecordID(rowIndex);
+ if (recordId) {
+ var correspondingRow = self._getRow(recordId);
+ correspondingRow.children().eq(cellIndex).focus();
+ } else if (self.currentGroupId) {
+ self.$('a[data-group-id=' + self.currentGroupId + ']').focus();
+ } else {
+ self.$('.o_field_x2many_list_row_add a:first').focus(); // FIXME
+ }
}
});
break;
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index 7a46e3b3cf2..471598ca4e4 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -39,8 +39,8 @@ var ListRenderer = BasicRenderer.extend({
'click .o_group_header': '_onToggleGroup',
'click thead .o_list_record_selector input': '_onToggleSelection',
'keypress thead tr td': '_onKeyPress',
- 'keydown tr': '_onKeyDown',
- 'keydown thead tr': '_onKeyDown',
+ 'keydown td': '_onKeyDown',
+ 'keydown th': '_onKeyDown',
},
/**
* @constructor
@@ -78,7 +78,7 @@ var ListRenderer = BasicRenderer.extend({
* @public
*/
giveFocus: function () {
- this.$('tbody .o_list_record_selector input:first()').focus();
+ this.$('th:eq(0) input, th:eq(1)').first().focus();
},
/**
* @override
@@ -169,6 +169,56 @@ var ListRenderer = BasicRenderer.extend({
}
},
/**
+ *
+ * @private
+ * @param {jQuery} $cell
+ * @param {string} direction
+ * @param {integer} colIndex
+ * @returns {jQuery|null}
+ */
+ _findConnectedCell: function ($cell, direction, colIndex) {
+ var $connectedRow = $cell.closest('tr')[direction]('tr');
+
+ if (!$connectedRow.length) {
+ // Is there another group ? Look at our parent's sibling
+ // We can have th in tbody so we can't simply look for thead
+ // if cell is a th and tbody instead
+ var tbody = $cell.closest('tbody, thead');
+ var $connectedGroup = tbody[direction]('tbody, thead');
+ if ($connectedGroup.length) {
+ // Found another group
+ var $connectedRows = $connectedGroup.find('tr');
+ var rowIndex;
+ if (direction === 'prev') {
+ rowIndex = $connectedRows.length - 1;
+ } else {
+ rowIndex = 0;
+ }
+ $connectedRow = $connectedRows.eq(rowIndex);
+ } else {
+ // End of the table
+ return;
+ }
+ }
+
+ var $connectedCell;
+ if ($connectedRow.hasClass('o_group_header')) {
+ $connectedCell = $connectedRow.children();
+ this.currentColIndex = colIndex;
+ } else if ($connectedRow.has('td.o_group_field_row_add').length) {
+ $connectedCell = $connectedRow.find('.o_group_field_row_add');
+ this.currentColIndex = colIndex;
+ } else {
+ var connectedRowChildren = $connectedRow.children();
+ if (colIndex === -1) {
+ colIndex = connectedRowChildren.length - 1;
+ }
+ $connectedCell = connectedRowChildren.eq(colIndex);
+ }
+
+ return $connectedCell;
+ },
+ /**
* return the number of visible columns. Note that this number depends on
* the state of the renderer. For example, in editable mode, it could be
* one more that in non editable mode, because there may be a visible 'trash
@@ -283,7 +333,7 @@ var ListRenderer = BasicRenderer.extend({
tdClassName += (' o_' + node.attrs.widget + '_cell');
}
}
- var $td = $('<td>', { class: tdClassName });
+ var $td = $('<td>', { class: tdClassName, tabindex: -1 });
// We register modifiers on the <td> element so that it gets the correct
// modifiers classes (for styling)
@@ -492,6 +542,7 @@ var ListRenderer = BasicRenderer.extend({
}
var $th = $('<th>')
.addClass('o_group_name')
+ .attr('tabindex', -1)
.text(name + ' (' + group.count + ')')
var $arrow = $('<span>')
.css('padding-left', (groupLevel * 20) + 'px')
@@ -649,6 +700,7 @@ var ListRenderer = BasicRenderer.extend({
}
$th.text(description)
.attr('data-name', name)
+ .attr('tabindex', -1)
.toggleClass('o-sort-down', isNodeSorted ? !order[0].asc : false)
.toggleClass('o-sort-up', isNodeSorted ? order[0].asc : false)
.addClass(field.sortable && 'o_column_sortable');
@@ -848,23 +900,59 @@ var ListRenderer = BasicRenderer.extend({
* @param {KeyboardEvent} ev
*/
_onKeyDown: function (ev) {
- if (!this.editable) {
- switch (ev.which) {
- case $.ui.keyCode.DOWN:
- $(ev.currentTarget).next().find('input').focus();
- ev.preventDefault();
- break;
- case $.ui.keyCode.UP:
- $(ev.currentTarget).prev().find('input').focus();
- ev.preventDefault();
- break;
- case $.ui.keyCode.ENTER:
- ev.preventDefault();
- var id = $(ev.currentTarget).data('id');
+ var $cell = $(ev.currentTarget);
+ var $tr;
+ var $futureCell;
+ var colIndex;
+ switch (ev.keyCode) {
+ case $.ui.keyCode.LEFT:
+ ev.preventDefault();
+ $tr = $cell.closest('tr');
+ if ($tr.hasClass('o_group_header') && $tr.hasClass('o_group_open')) {
+ this._onToggleGroup(ev);
+ } else {
+ $futureCell = $cell.prev();
+ }
+ break;
+ case $.ui.keyCode.RIGHT:
+ ev.preventDefault();
+ $tr = $cell.closest('tr');
+ if ($tr.hasClass('o_group_header') && !$tr.hasClass('o_group_open')) {
+ this._onToggleGroup(ev);
+ } else {
+ $futureCell = $cell.next();
+ }
+ break;
+ case $.ui.keyCode.UP:
+ ev.preventDefault();
+ colIndex = this.currentColIndex || $cell.index();
+ $futureCell = this._findConnectedCell($cell, 'prev', colIndex);
+ break;
+ case $.ui.keyCode.DOWN:
+ ev.preventDefault();
+ colIndex = this.currentColIndex || $cell.index();
+ $futureCell = this._findConnectedCell($cell, 'next', colIndex);
+ break;
+ case $.ui.keyCode.ENTER:
+ ev.preventDefault();
+ $tr = $cell.closest('tr');
+ if ($tr.hasClass('o_group_header')) {
+ this._onToggleGroup(ev);
+ } else {
+ var id = $tr.data('id');
if (id) {
this.trigger_up('open_record', { id: id, target: ev.target });
}
- break;
+ }
+ break;
+ }
+ if ($futureCell) {
+ // If the cell contains activable elements, focus them instead
+ var $activables = $futureCell.find(':focusable');
+ if ($activables.length) {
+ $activables[0].focus();
+ } else {
+ $futureCell.focus();
}
}
},
@@ -903,12 +991,27 @@ var ListRenderer = BasicRenderer.extend({
},
/**
* @private
- * @param {MouseEvent} ev
+ * @param {DOMEvent} ev
*/
_onToggleGroup: function (ev) {
- var group = $(ev.currentTarget).data('group');
+ ev.preventDefault();
+ ev.stopPropagation();
+ var group = $(ev.currentTarget).closest('tr').data('group');
if (group.count) {
- this.trigger_up('toggle_group', { group: group });
+ this.trigger_up('toggle_group', {
+ group: group,
+ onSuccess: function() {
+ // Refocus the header after re-render unless the user
+ // already focused something else by now
+ if (document.activeElement.tagName === 'BODY') {
+ var groupHeaders = $('tr.o_group_header:data("group")');
+ var header = groupHeaders.filter(function () {
+ return $(this).data('group').id === group.id;
+ });
+ header.find('.o_group_name').focus();
+ }
+ },
+ });
}
},
/**
diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss
index 16f14f3717e..07da78731f3 100644
--- a/addons/web/static/src/scss/list_view.scss
+++ b/addons/web/static/src/scss/list_view.scss
@@ -136,11 +136,15 @@
border-top: none; // Override bootstrap for grouped list views
}
- tbody {
+ &, &.table-striped {
tr:focus-within {
- background-color: $o-form-lightsecondary;
+ background-color: lighten($o-form-lightsecondary, 10%);
}
}
+ thead th:focus-within, tbody td:focus-within {
+ background-color: $o-form-lightsecondary;
+ outline: none;
+ }
.o_data_cell.o_text_overflow,
.o_data_cell.o_invisible_modifier {
diff --git a/addons/web/static/src/scss/list_view_extra.scss b/addons/web/static/src/scss/list_view_extra.scss
index 83791ce42dd..7532ec2084f 100644
--- a/addons/web/static/src/scss/list_view_extra.scss
+++ b/addons/web/static/src/scss/list_view_extra.scss
@@ -6,5 +6,9 @@
tbody > tr.o_group_header {
background-image: linear-gradient(to bottom, #fcfcfc, #dedede);
+
+ &:focus-within {
+ background-image: linear-gradient(to bottom, #fcfcfc, $o-form-lightsecondary);
+ }
}
}