From 2e41d080dd6950e70a85ec9a905e5da1f483a345 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Tue, 16 Jun 2026 12:23:27 -0400 Subject: [PATCH] Keep at least one collection-tree row selected Cmd-clicking (or Space on) the last selected row deselected it, leaving the collection tree with no selection while its items stayed shown. Add an opt-in requireSelection option to VirtualizedTable that refuses to toggle off the last remaining row, and enable it for the collection tree. --- chrome/content/zotero/collectionTree.jsx | 4 ++- .../zotero/components/virtualized-table.jsx | 11 ++++++ test/tests/collectionTreeTest.js | 34 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/chrome/content/zotero/collectionTree.jsx b/chrome/content/zotero/collectionTree.jsx index 398a5a519c..ed66fde62e 100644 --- a/chrome/content/zotero/collectionTree.jsx +++ b/chrome/content/zotero/collectionTree.jsx @@ -487,7 +487,9 @@ var CollectionTree = class CollectionTree extends LibraryTree { getRowString: this.getRowString.bind(this), multiSelect: this.props.multiSelect, - + // The collection tree must always have a selection + requireSelection: true, + onItemContextMenu: (...args) => this.props.onContextMenu && this.props.onContextMenu(...args), onKeyDown: this.handleKeyDown, diff --git a/chrome/content/zotero/components/virtualized-table.jsx b/chrome/content/zotero/components/virtualized-table.jsx index 0ad5b64af6..5539bbba1f 100644 --- a/chrome/content/zotero/components/virtualized-table.jsx +++ b/chrome/content/zotero/components/virtualized-table.jsx @@ -379,6 +379,10 @@ class VirtualizedTable extends React.Component { multiSelect: false, + // When true, the last selected row can't be toggled off, so the selection + // never becomes empty through user action + requireSelection: false, + onSelectionChange: noop, // The below are for arrow-key navigation @@ -443,6 +447,8 @@ class VirtualizedTable extends React.Component { multiSelect: PropTypes.bool, + requireSelection: PropTypes.bool, + onSelectionChange: PropTypes.func, // The below are for arrow-key navigation @@ -823,6 +829,11 @@ class VirtualizedTable extends React.Component { this.selection.shiftSelect(index, toggleSelection, shouldDebounce); } else if (toggleSelection) { + // Don't allow toggling off the last selected row when a selection is required + if (this.props.requireSelection + && this.selection.count === 1 && this.selection.isSelected(index)) { + return; + } this.selection.toggleSelect(index, shouldDebounce); } else if (moveFocused) { diff --git a/test/tests/collectionTreeTest.js b/test/tests/collectionTreeTest.js index 33b91500ed..80143205a0 100644 --- a/test/tests/collectionTreeTest.js +++ b/test/tests/collectionTreeTest.js @@ -723,6 +723,40 @@ describe("Zotero.CollectionTree", function () { }); }); + describe("selection", function () { + // Toggle a row the way a Cmd-click/Space does (through the table's selection handler) + function toggleRow(index) { + cv.tree._onSelection(index, false, true); + } + + it("should keep a selection when the last selected row is toggled off", async function () { + let c = await createDataObject('collection'); + await cv.selectByID("C" + c.id); + let row = cv.getRowIndexByID("C" + c.id); + assert.equal(cv.selection.count, 1); + + // Toggling off the only selected row would empty the selection + toggleRow(row); + + assert.equal(cv.selection.count, 1); + assert.isTrue(cv.selection.isSelected(row)); + }); + + it("should allow toggling off a row when others remain selected", async function () { + let c1 = await createDataObject('collection'); + let c2 = await createDataObject('collection'); + await cv.selectByID("C" + c1.id); + let r2 = cv.getRowIndexByID("C" + c2.id); + toggleRow(r2); + assert.equal(cv.selection.count, 2); + + // Toggling one off leaves the other + toggleRow(r2); + assert.equal(cv.selection.count, 1); + assert.isTrue(cv.selection.isSelected(cv.getRowIndexByID("C" + c1.id))); + }); + }); + describe("#onDrop()", function () { /** * Simulate a drag and drop