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.
This commit is contained in:
Dan Stillman 2026-06-16 12:23:27 -04:00
parent 9e324fe62b
commit 2e41d080dd
3 changed files with 48 additions and 1 deletions

View file

@ -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,

View file

@ -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) {

View file

@ -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