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(