Skip to content

Commit 46dcc48

Browse files
committed
fix: treat a re-resolved anchor as a fresh session
1 parent c6e054f commit 46dcc48

2 files changed

Lines changed: 21 additions & 5 deletions

File tree

src/hooks/useShiftRangeSelection.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,12 @@ function seedRangeState<TItem>(params: Params<TItem>, isIncluded: (key: string)
144144
}
145145

146146
/**
147-
* What a session with no history starts from. It adopts rows that read as selected without having been picked on their own,
148-
* since those came from a block selection like a group that a range is entitled to narrow. Empty unless `isItemProtected` is passed.
147+
* A session with no continuity adopts rows that read as selected without having been picked on their own, since those came from a
148+
* block selection a range may narrow. Inert unless `isItemProtected` is passed.
149149
*/
150-
function startingPainted<TItem>(params: Params<TItem>, state: SessionState): ReadonlySet<string> {
150+
function startingPainted<TItem>(params: Params<TItem>, sameAnchor: boolean): ReadonlySet<string> {
151151
const keys = new Set<string>();
152-
if (state.kind !== 'idle') {
152+
if (sameAnchor) {
153153
return keys;
154154
}
155155
const isProtected = params.isItemProtected ?? params.isItemSelected;
@@ -197,7 +197,7 @@ function computeShiftRange<TItem>(params: Params<TItem>, state: SessionState, ta
197197
// The session survives only while the same anchor does; a re-resolved or cold anchor starts fresh.
198198
const sameAnchor = state.kind !== 'idle' && anchor === state.anchor;
199199
const continuing = state.kind === 'ranging' && sameAnchor;
200-
const prevPainted: ReadonlySet<string> = continuing ? state.painted : startingPainted(params, state);
200+
const prevPainted: ReadonlySet<string> = continuing ? state.painted : startingPainted(params, sameAnchor);
201201
const preSelected = protectedKeys(params, prevPainted);
202202

203203
const anchorIdx = keyToIndex.get(anchor);

tests/unit/hooks/useShiftRangeSelection.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,22 @@ describe('useShiftRangeSelection', () => {
533533
expect(nthBatchKeys(onApplyRange, 1)).toEqual({toSelect: ['b', 'c'], toDeselect: ['d']});
534534
});
535535

536+
it('adopts the block when the remembered anchor is gone, since that session is over', () => {
537+
const onApplyRange = makeApplyMock();
538+
const {result, rerender} = renderHook(
539+
({items}: {items: Row[]}) => useShiftRangeSelection<Row>(makeParams({items, onApplyRange, isItemSelected: (row) => row.keyForList !== 'a', isItemProtected: () => false})),
540+
{initialProps: {items: [...ROWS]}},
541+
);
542+
// Anchored on 'a', which the next render drops from the list
543+
act(() => result.current.notifyAnchor(ROW_A));
544+
rerender({items: ROWS.slice(1)});
545+
act(() => {
546+
result.current.applyShiftClick(ROW_C, true);
547+
});
548+
// The anchor re-resolved to the block's first row, and the rest of the block fell out of the range with it.
549+
expect(nthBatchKeys(onApplyRange, 0)).toEqual({toSelect: ['b', 'c'], toDeselect: ['d', 'e']});
550+
});
551+
536552
it('leaves a block alone when the session already has an anchor, so an unrelated range cannot dissolve it', () => {
537553
const onApplyRange = makeApplyMock();
538554
const {result} = renderHook(() =>

0 commit comments

Comments
 (0)