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 = $('')
.text(_t("Add a line"))
- .data('groupID', group.id);
+ .attr('data-group-id', group.id);
var $td = $('
', {class: 'o_add_record_row'})
- .data('groupID', group.id)
+ .attr('data-group-id', group.id)
.append($td);
$groupBody.append($tr.prepend($('| ').html(' ')));
}
@@ -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,
@@ -1097,6 +1099,35 @@ ListRenderer.include({
_onFooterClick: function () {
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
@@ -168,6 +168,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
@@ -283,7 +333,7 @@ var ListRenderer = BasicRenderer.extend({
tdClassName += (' o_' + node.attrs.widget + '_cell');
}
}
- var $td = $(' | ', { class: tdClassName });
+ var $td = $(' | ', { class: tdClassName, tabindex: -1 });
// We register modifiers on the | element so that it gets the correct
// modifiers classes (for styling)
@@ -492,6 +542,7 @@ var ListRenderer = BasicRenderer.extend({
}
var $th = $(' | ')
.addClass('o_group_name')
+ .attr('tabindex', -1)
.text(name + ' (' + group.count + ')')
var $arrow = $('')
.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);
+ }
}
}
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index a275f3602f7..5e5a0c7b7a1 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -2975,6 +2975,68 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('pressing TAB in editable list with several fields [REQUIRE FOCUS]', async function (assert) {
+ assert.expect(6);
+
+ var list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '' +
+ '' +
+ '' +
+ '',
+ });
+
+ await testUtils.dom.click(list.$('.o_data_cell:first'));
+ assert.hasClass(list.$('.o_data_row:first'), 'o_selected_row');
+ assert.strictEqual(document.activeElement, list.$('.o_data_row:first .o_data_cell:first input')[0]);
+
+ // Press 'Tab' -> should go to next cell (still in first row)
+ await testUtils.fields.triggerKeydown(list.$('.o_selected_row input'), 'tab');
+ assert.hasClass(list.$('.o_data_row:first'), 'o_selected_row');
+ assert.strictEqual(document.activeElement, list.$('.o_data_row:first .o_data_cell:last input')[0]);
+
+ // Press 'Tab' -> should go to next line (first cell)
+ await testUtils.fields.triggerKeydown(list.$('.o_selected_row input'), 'tab');
+ assert.hasClass(list.$('.o_data_row:nth(1)'), 'o_selected_row');
+ assert.strictEqual(document.activeElement, list.$('.o_data_row:nth(1) .o_data_cell:first input')[0]);
+
+ list.destroy();
+ });
+
+ QUnit.test('pressing SHIFT-TAB in editable list with several fields [REQUIRE FOCUS]', async function (assert) {
+ assert.expect(6);
+
+ var list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '' +
+ '' +
+ '' +
+ '',
+ });
+
+ await testUtils.dom.click(list.$('.o_data_row:nth(2) .o_data_cell:nth(1)'));
+ assert.hasClass(list.$('.o_data_row:nth(2)'), 'o_selected_row');
+ assert.strictEqual(document.activeElement, list.$('.o_data_row:nth(2) .o_data_cell:last input')[0]);
+
+ // Press 'shift-Tab' -> should go to previous line (last cell)
+ list.$('tr.o_selected_row input').trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
+ await testUtils.nextTick();
+ assert.hasClass(list.$('.o_data_row:nth(2)'), 'o_selected_row');
+ assert.strictEqual(document.activeElement, list.$('.o_data_row:nth(2) .o_data_cell:first input')[0]);
+
+ // Press 'shift-Tab' -> should go to previous cell
+ list.$('tr.o_selected_row input').trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
+ await testUtils.nextTick();
+ assert.hasClass(list.$('.o_data_row:nth(1)'), 'o_selected_row');
+ assert.strictEqual(document.activeElement, list.$('.o_data_row:nth(1) .o_data_cell:last input')[0]);
+
+ list.destroy();
+ });
+
QUnit.test('navigation with tab and readonly field (no modification)', async function (assert) {
// This test makes sure that if we have 2 cells in a row, the first in
// edit mode, and the second one readonly, then if we press TAB when the
@@ -5244,6 +5306,276 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('cell-level keyboard navigation in non-editable list', async function (assert) {
+ assert.expect(16);
+
+ var list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '',
+ intercepts: {
+ switch_view: function (event) {
+ assert.strictEqual(event.data.res_id, 3,
+ "'switch_view' event has been triggered");
+ },
+ },
+ });
+
+ assert.ok(document.activeElement.classList.contains('o_searchview_input'), 'default focus should be in search view');
+ // switch focus to the create button in tests while it works on live
+ $('.o_list_button_add').focus();
+ assert.ok(document.activeElement.classList.contains('o_list_button_add'),
+ 'focus should now be in create button');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+ assert.strictEqual(document.activeElement.tagName, 'INPUT',
+ 'focus should now be on the record selector');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+ assert.strictEqual(document.activeElement.tagName, 'INPUT',
+ 'focus should now be in first row input');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'right');
+ assert.strictEqual(document.activeElement.tagName, 'TD',
+ 'focus should now be in field TD');
+ assert.strictEqual(document.activeElement.textContent, 'yop',
+ 'focus should now be in first row field');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'right');
+ assert.strictEqual(document.activeElement.textContent, 'yop',
+ 'should not cycle at end of line');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+ assert.strictEqual(document.activeElement.textContent, 'blip',
+ 'focus should now be in second row field');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+ assert.strictEqual(document.activeElement.textContent, 'gnap',
+ 'focus should now be in third row field');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+ assert.strictEqual(document.activeElement.textContent, 'blip',
+ 'focus should now be in last row field');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+ assert.strictEqual(document.activeElement.textContent, 'blip',
+ 'focus should still be in last row field (arrows do not cycle)');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'right');
+ assert.strictEqual(document.activeElement.textContent, 'blip',
+ 'focus should still be in last row field (arrows still do not cycle)');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'left');
+ assert.strictEqual(document.activeElement.tagName, 'INPUT',
+ 'focus should now be in last row input');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'left');
+ assert.strictEqual(document.activeElement.tagName, 'INPUT',
+ 'should not cycle at start of line');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'up');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'right');
+ assert.strictEqual(document.activeElement.textContent, 'gnap',
+ 'focus should now be in third row field');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'enter');
+ list.destroy();
+ });
+
+ QUnit.test('cell-level keyboard navigation in editable grouped list', async function (assert) {
+ assert.expect(56);
+
+ var list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '',
+ groupBy: ['bar'],
+ });
+
+ await testUtils.dom.click(list.$('.o_group_header:first')); // open first group
+ await testUtils.dom.click(list.$('td:contains(blip)')); // select row of first group
+ assert.hasClass(list.$('tr.o_data_row:eq(1)'), 'o_selected_row',
+ 'second row should be opened');
+
+ var $secondRowInput = list.$('tr.o_data_row:eq(1) td:eq(1) input');
+ assert.strictEqual($secondRowInput.val(), 'blip',
+ 'second record should be in edit mode');
+
+ await testUtils.fields.editAndTrigger($secondRowInput, 'blipbloup', 'input');
+ assert.strictEqual($secondRowInput.val(), 'blipbloup',
+ 'second record should be changed but not saved yet');
+
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'escape');
+
+ assert.hasClass($('body'), 'modal-open',
+ 'record has been modified, are you sure modal should be opened');
+ await testUtils.dom.click($('body .modal button span:contains(Ok)'));
+
+ assert.doesNotHaveClass(list.$('tr.o_data_row:eq(1)'), 'o_selected_row',
+ 'second row should be closed');
+ assert.strictEqual(document.activeElement.tagName, 'TD',
+ 'focus is in field td');
+ assert.strictEqual(document.activeElement.textContent, 'blip',
+ 'second field of second record should be focused');
+ assert.strictEqual(list.$('tr.o_data_row:eq(1) td:eq(1)').text(), 'blip',
+ 'change should not have been saved');
+
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'left');
+ assert.strictEqual(document.activeElement.tagName, 'INPUT',
+ 'record selector should be focused');
+
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'up');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'right');
+ assert.strictEqual(document.activeElement.tagName, 'TD',
+ 'focus is in first record td');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'enter');
+ var $firstRowInput = list.$('tr.o_data_row:eq(0) td:eq(1) input');
+ assert.hasClass(list.$('tr.o_data_row:eq(0)'), 'o_selected_row',
+ 'first row should be selected');
+ assert.strictEqual($firstRowInput.val(), 'yop',
+ 'first record should be in edit mode');
+
+ await testUtils.fields.editAndTrigger($firstRowInput, 'Zipadeedoodah', 'input');
+ assert.strictEqual($firstRowInput.val(), 'Zipadeedoodah',
+ 'first record should be changed but not saved yet');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'enter');
+ assert.strictEqual(list.$('tr.o_data_row:eq(0) td:eq(1)').text(), 'Zipadeedoodah',
+ 'first record should be saved');
+ assert.doesNotHaveClass(list.$('tr.o_data_row:eq(0)'), 'o_selected_row',
+ 'first row should be closed');
+ assert.hasClass(list.$('tr.o_data_row:eq(1)'), 'o_selected_row',
+ 'second row should be opened');
+ assert.strictEqual(list.$('tr.o_data_row:eq(1) td:eq(1) input').val(), 'blip',
+ 'second record should be in edit mode');
+
+ assert.strictEqual(document.activeElement.value, 'blip',
+ 'second record input should be focused');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'up');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'right');
+ assert.strictEqual(document.activeElement.value, 'blip',
+ 'second record input should still be focused (arrows movements are disabled in edit)');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'left');
+ assert.strictEqual(document.activeElement.value, 'blip',
+ 'second record input should still be focused (arrows movements are still disabled in edit)');
+
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'escape');
+ assert.doesNotHaveClass(list.$('tr.o_data_row:eq(1)'), 'o_selected_row',
+ 'second row should be closed');
+ assert.strictEqual(document.activeElement.tagName, 'TD',
+ 'focus is in field td');
+ assert.strictEqual(document.activeElement.textContent, 'blip',
+ 'second field of second record should be focused');
+
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+
+ assert.strictEqual(document.activeElement.tagName, 'A',
+ 'should focus the "Add a line" button');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+
+ assert.strictEqual(document.activeElement.textContent, 'false (1)',
+ 'focus should be on second group header');
+ assert.strictEqual(list.$('tr.o_data_row').length, 3,
+ 'should have 3 rows displayed');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'enter');
+ assert.strictEqual(list.$('tr.o_data_row').length, 4,
+ 'should have 4 rows displayed');
+ assert.strictEqual(document.activeElement.textContent, 'false (1)',
+ 'focus should still be on second group header');
+
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+ assert.strictEqual(document.activeElement.textContent, 'blip',
+ 'second field of last record should be focused');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+ assert.strictEqual(document.activeElement.tagName, 'A',
+ 'should focus the "Add a line" button');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+ assert.strictEqual(document.activeElement.tagName, 'A',
+ 'arrow navigation should not cycle (focus still on last row)');
+
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'enter');
+ await testUtils.fields.editAndTrigger($('tr.o_data_row:eq(4) td:eq(1) input'),
+ 'cheateur arrete de cheater', 'input');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'enter');
+ assert.strictEqual(list.$('tr.o_data_row').length, 6,
+ 'should have 6 rows displayed (new record + new edit line)');
+
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'escape');
+ assert.strictEqual(document.activeElement.tagName, 'A',
+ 'should focus the "Add a line" button');
+
+ // come back to the top
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'up');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'up');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'up');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'up');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'up');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'up');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'up');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'up');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'up');
+
+ assert.strictEqual(document.activeElement.tagName, 'TH',
+ 'focus is in table header');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'left');
+ assert.strictEqual(document.activeElement.tagName, 'INPUT',
+ 'focus is in header input');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'right');
+ assert.strictEqual(document.activeElement.tagName, 'TD',
+ 'focus is in field td');
+ assert.strictEqual(document.activeElement.textContent, 'Zipadeedoodah',
+ 'second field of first record should be focused');
+
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'up');
+ assert.strictEqual(document.activeElement.textContent, 'true (3)',
+ 'focus should be on first group header');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'enter');
+ assert.strictEqual(list.$('tr.o_data_row').length, 2,
+ 'should have 2 rows displayed (first group should be closed)');
+ assert.strictEqual(document.activeElement.textContent, 'true (3)',
+ 'focus should still be on first group header');
+
+ assert.strictEqual(list.$('tr.o_data_row').length, 2,
+ 'should have 2 rows displayed');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'right');
+ assert.strictEqual(list.$('tr.o_data_row').length, 5,
+ 'should have 5 rows displayed');
+ assert.strictEqual(document.activeElement.textContent, 'true (3)',
+ 'focus is still in header');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'right');
+ assert.strictEqual(list.$('tr.o_data_row').length, 5,
+ 'should have 5 rows displayed');
+ assert.strictEqual(document.activeElement.textContent, 'true (3)',
+ 'focus is still in header');
+
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'left');
+ assert.strictEqual(list.$('tr.o_data_row').length, 2,
+ 'should have 2 rows displayed');
+ assert.strictEqual(document.activeElement.textContent, 'true (3)',
+ 'focus is still in header');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'left');
+ assert.strictEqual(list.$('tr.o_data_row').length, 2,
+ 'should have 2 rows displayed');
+ assert.strictEqual(document.activeElement.textContent, 'true (3)',
+ 'focus is still in header');
+
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+ assert.strictEqual(document.activeElement.textContent, 'false (2)',
+ 'focus should now be on second group header');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+ assert.strictEqual(document.activeElement.tagName, 'TD',
+ 'record td should be focused');
+ assert.strictEqual(document.activeElement.textContent, 'blip',
+ 'second field of first record of second group should be focused');
+
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+ assert.strictEqual(document.activeElement.textContent, 'cheateur arrete de cheater',
+ 'second field of last record of second group should be focused');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'down');
+ assert.strictEqual(document.activeElement.tagName, 'A',
+ 'should focus the "Add a line" button');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'up');
+ assert.strictEqual(document.activeElement.textContent, 'cheateur arrete de cheater',
+ 'second field of last record of second group should be focused (special case: the first td of the "Add a line" line was skipped');
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'up');
+ assert.strictEqual(document.activeElement.textContent, 'blip',
+ 'second field of first record of second group should be focused');
+
+ list.destroy();
+ });
+
QUnit.test('add a new row in grouped editable="top" list', async function (assert) {
assert.expect(7);
|