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 = $('') .attr('colspan', this._getNumberOfCols()) .addClass('o_group_field_row_add') + .attr('tabindex', -1) .append($a); var $tr = $('', {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);