diff --git a/addons/web/static/src/views/list/list_renderer.js b/addons/web/static/src/views/list/list_renderer.js index 0fe4fa41f1b..2616fcb90e3 100644 --- a/addons/web/static/src/views/list/list_renderer.js +++ b/addons/web/static/src/views/list/list_renderer.js @@ -174,6 +174,20 @@ export class ListRenderer extends Component { this.columnWidths = null; this.freezeColumnWidths(); }); + + useExternalListener(window, "keydown", (ev) => { + this.shiftKeyMode = ev.shiftKey; + }); + useExternalListener(window, "keyup", (ev) => { + this.shiftKeyMode = ev.shiftKey; + const hotkey = getActiveHotkey(ev); + if (hotkey === "shift") { + this.shiftKeyedRecord = undefined; + } + }); + useExternalListener(window, "blur", (ev) => { + this.shiftKeyMode = false; + }); } getActiveColumns(list) { @@ -1059,6 +1073,38 @@ export class ListRenderer extends Component { return null; } + expandCheckboxes(record, direction) { + const { records } = this.props.list; + const recordIndex = records.indexOf(record); + const shiftKeyedRecordIndex = records.indexOf(this.shiftKeyedRecord); + let nextRecord; + let isExpanding; + switch (direction) { + case "up": + if (recordIndex <= 0) { + return false; + } + nextRecord = records[recordIndex - 1]; + isExpanding = shiftKeyedRecordIndex > recordIndex - 1; + break; + case "down": + if (recordIndex === records.length - 1) { + return false; + } + nextRecord = records[recordIndex + 1]; + isExpanding = shiftKeyedRecordIndex < recordIndex + 1; + break; + } + + if (isExpanding) { + nextRecord.toggleSelection(this.shiftKeyedRecord.selected); + } else { + record.toggleSelection(false); + } + + return true; + } + applyCellKeydownMultiEditMode(hotkey, cell, group, record) { const { list } = this.props; const row = cell.parentElement; @@ -1387,6 +1433,25 @@ export class ListRenderer extends Component { toFocus = buttons[index - 1] || currentButton; } break; + case "shift+arrowdown": { + if (this.expandCheckboxes(record, "down")) { + toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, "down"); + } + break; + } + case "shift+arrowup": { + if (this.expandCheckboxes(record, "up")) { + toFocus = this.findFocusFutureCell(cell, cellIsInGroupRow, "up"); + } + break; + } + case "shift+space": + this.toggleRecordSelection(record); + toFocus = getElementToFocus(cell); + break; + case "shift": + this.shiftKeyedRecord = record; + break; case "enter": if (!group && !record) { return false; @@ -1518,10 +1583,28 @@ export class ListRenderer extends Component { } toggleRecordSelection(record) { - record.toggleSelection(); + if (this.shiftKeyMode && this.lastCheckedRecord) { + this.toggleRecordShiftSelection(record); + } else { + record.toggleSelection(); + } + this.lastCheckedRecord = record; this.props.list.selectDomain(false); } + toggleRecordShiftSelection(record) { + const { records } = this.props.list; + const recordIndex = records.indexOf(record); + const lastCheckedRecordIndex = records.indexOf(this.lastCheckedRecord); + const start = Math.min(recordIndex, lastCheckedRecordIndex); + const end = Math.max(recordIndex, lastCheckedRecordIndex); + const { selected } = record; + + for (let i = start; i <= end; i++) { + records[i].toggleSelection(!selected); + } + } + async toggleOptionalField(fieldName) { this.optionalActiveFields[fieldName] = !this.optionalActiveFields[fieldName]; this.state.columns = this.getActiveColumns(this.props.list); diff --git a/addons/web/static/src/views/list/list_renderer.xml b/addons/web/static/src/views/list/list_renderer.xml index a6a9edbbd66..d74ce34a9eb 100644 --- a/addons/web/static/src/views/list/list_renderer.xml +++ b/addons/web/static/src/views/list/list_renderer.xml @@ -217,7 +217,7 @@ t-on-touchend="() => this.onRowTouchEnd(record)" t-on-touchmove="() => this.onRowTouchMove(record)" > - + diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js index 6cf46d1da7f..144ebc51b5b 100644 --- a/addons/web/static/tests/views/list_view_tests.js +++ b/addons/web/static/tests/views/list_view_tests.js @@ -259,6 +259,158 @@ QUnit.module("Views", (hooks) => { assert.isNotVisible(target.querySelector(".o_list_button_discard")); }); + QUnit.test("select record range with shift click", async function (assert) { + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: '', + }); + await click(target.querySelectorAll(".o_data_row .o_list_record_selector input")[0]); + assert.containsOnce(target.querySelector(".o_cp_buttons"), ".o_list_selection_box"); + assert.containsNone(target.querySelector(".o_list_selection_box"), ".o_list_select_domain"); + assert.strictEqual( + target.querySelector(".o_list_selection_box").textContent.trim(), + "1 selected" + ); + assert.strictEqual( + document.querySelectorAll(".o_data_row .o_list_record_selector input:checked").length, + 1 + ); + + // shift click the 4th record to have 0-1-2-3 toggled + await triggerEvents( + target.querySelectorAll(".o_data_row .o_list_record_selector input")[3], + null, + [["keydown", { key: "Shift", shiftKey: true }], "click"] + ); + + assert.strictEqual( + target.querySelector(".o_list_selection_box").textContent.trim(), + "4 selected" + ); + assert.strictEqual( + document.querySelectorAll(".o_data_row .o_list_record_selector input:checked").length, + 4 + ); + + // shift click the 3rd record to untoggle 2-3 + await triggerEvents( + target.querySelectorAll(".o_data_row .o_list_record_selector input")[2], + null, + [["keydown", { key: "Shift", shiftKey: true }], "click"] + ); + assert.strictEqual( + target.querySelector(".o_list_selection_box").textContent.trim(), + "2 selected" + ); + assert.strictEqual( + document.querySelectorAll(".o_data_row .o_list_record_selector input:checked").length, + 2 + ); + + // shift click the 1st record to untoggle 0-1 + await triggerEvents( + target.querySelectorAll(".o_data_row .o_list_record_selector input")[0], + null, + [["keydown", { key: "Shift", shiftKey: true }], "click"] + ); + assert.strictEqual( + document.querySelectorAll(".o_data_row .o_list_record_selector input:checked").length, + 0 + ); + }); + + QUnit.test("select record range with shift+space", async function (assert) { + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: '', + }); + + // Go to the first checkbox and check it + triggerHotkey("ArrowDown"); + triggerHotkey("ArrowDown"); + await nextTick(); + let checkbox = target.querySelector( + ".o_data_row:nth-child(1) .o_list_record_selector input" + ); + assert.strictEqual(document.activeElement, checkbox); + await click(checkbox); + assert.ok(checkbox.checked); + + // Go to the fourth checkbox and shift+space + triggerHotkey("ArrowDown"); + triggerHotkey("ArrowDown"); + triggerHotkey("ArrowDown"); + await nextTick(); + checkbox = target.querySelector(".o_data_row:nth-child(4) .o_list_record_selector input"); + assert.strictEqual(document.activeElement, checkbox); + assert.ok(!checkbox.checked); + await triggerEvents(document.activeElement, null, [ + ["keydown", { key: "Shift", shiftKey: true }], + ["keydown", { key: " ", shiftKey: true }], + ]); + // focus is on the input and not in the td cell + assert.strictEqual(document.activeElement.tagName, "INPUT"); + + // Check that all checkbox is checked + for (let i = 1; i < 5; i++) { + checkbox = target.querySelector( + `.o_data_row:nth-child(${i}) .o_list_record_selector input` + ); + assert.ok(checkbox.checked); + } + }); + + QUnit.test("expand range of checkbox with shift+arrow", async function (assert) { + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: '', + }); + + // Go to the first checkbox and check it + triggerHotkey("ArrowDown"); + triggerHotkey("ArrowDown"); + await nextTick(); + let checkbox = target.querySelector( + ".o_data_row:nth-child(1) .o_list_record_selector input" + ); + assert.strictEqual(document.activeElement, checkbox); + await click(checkbox); + assert.ok(checkbox.checked); + + // expand the checkbox with arrowdown + await triggerEvent(document.activeElement, null, "keydown", { + key: "Shift", + shiftKey: true, + }); + triggerHotkey("shift+ArrowDown"); + triggerHotkey("shift+ArrowDown"); + triggerHotkey("shift+ArrowDown"); + triggerHotkey("shift+ArrowUp"); + await nextTick(); + await triggerEvent(document.activeElement, null, "keyup", { + key: "Shift", + shiftKey: false, + }); + + checkbox = target.querySelector(".o_data_row:nth-child(3) .o_list_record_selector input"); + assert.strictEqual(document.activeElement, checkbox); + assert.ok(checkbox.checked); + + // Check that the three checkbox are checked + for (let i = 1; i < 4; i++) { + checkbox = target.querySelector( + `.o_data_row:nth-child(${i}) .o_list_record_selector input` + ); + assert.ok(checkbox.checked); + } + }); + QUnit.test("list with class", async function (assert) { await makeView({ type: "list", @@ -1993,8 +2145,7 @@ QUnit.module("Views", (hooks) => { type: "list", resModel: "foo", serverData, - arch: - '', + arch: '', mockRPC: async (route, args) => { if (args.method === "write") { assert.step(`write ${args.args[0]}`); @@ -2035,8 +2186,7 @@ QUnit.module("Views", (hooks) => { type: "list", resModel: "foo", serverData, - arch: - '', + arch: '', groupBy: ["bar"], mockRPC: async (route, args) => { if (args.method === "write") { @@ -3048,8 +3198,7 @@ QUnit.module("Views", (hooks) => { resModel: "foo", serverData, groupBy: ["m2o"], - arch: - '', + arch: '', }); const groupHeaders = target.querySelectorAll(".o_group_header"); assert.strictEqual(