You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[$250] New Feature : Shift+Click Range Selection Not Supported in Multi-Select Lists #90539
Version Number: 9.3.73-4 Reproducible in staging?: Y Reproducible in production?: Y If this was caught during regression testing, add the test name, ID and link from BrowserStack: Email or phone of affected tester (no customers): Logs:https://stackoverflow.com/c/expensify/questions/4856 Expensify/Expensify Issue URL: Issue reported by:@twisterdotcom Slack conversation (hyperlinked to channel name): #Expensify Bugs
Action Performed:
Open a multi-select list: Reports list, Expenses list
Select an item near the top
Hold Shift
Select another item farther down the list
Expected Result:
All items between the first and second selection should:
Become selected automatically
Actual Result:
Shift+Click does nothing special
Only the clicked item is selected/deselected
Workaround:
Unknown
Platforms:
Select the officially supported platforms where the issue was reproduced:
The multi-select system is built on a simple per-item toggle model with no awareness of click modifier keys or item ordering. When a user clicks a checkbox, toggleTransaction fires and toggles only that single item — it never receives the browser MouseEvent, so it cannot check event.shiftKey. There is no concept of a "last selected index" stored anywhere in the selection state (SearchContext.tsx), which means even if shift were detected there would be no anchor point to compute a range from.
What changes do you think we should make in order to solve the problem?
Thread the native event through to the toggle handler — In ListSelectionButton (and its parent ListCheckbox / SelectableListItem), pass the click GestureResponderEvent (or MouseEvent on web) up through onSelectRow / onCheckboxPress so the consumer can inspect event.shiftKey.
Track the last-selected index in SearchContext — Add a lastSelectedIndex: number | null field to the search selection state. Update it every time a single item is toggled.
Implement range selection in toggleTransaction — When event.shiftKey is true and lastSelectedIndex is set, compute the range between the anchor and the newly clicked index in the current sorted/visible item list, then select (or deselect) every item in that range. Reset the anchor to the new index afterward.
Platform guard — Only enable on web/desktop where shift+click is a standard interaction pattern; on native mobile the shift key is not applicable and the behavior should remain unchanged.
What alternative solutions did you explore? (Optional)
Keyboard-only approach (Shift+Arrow): Would avoid threading events through the press handler, but wouldn't match the standard desktop UX of shift+click and would require focus tracking per row, which the current list components don't support.
Next Steps for Contributor+ team: Reply with @MelvinBot implement this to create a draft PR, @MelvinBot <your feedback> to refine this analysis, or explain why you are rejecting Melvin's proposal.
Shift+Click range selection is a desktop-web convention that the multi-select machinery in Search never implements. The checkbox callback the list exposes to consumers takes only the row item — there is no mouse event, no modifier flag, and no anchor index — so the handler in Search/index.tsx has nothing to range-select against:
Result: every click goes through the per-row "toggle this one item" branch, and no anchor is ever remembered, so the platform-standard "click A, Shift+click B, everything in between selects" behaviour cannot trigger.
What changes do you think we should make in order to solve the problem?
I'd plumb the shift modifier through the existing checkbox callback (web-only — GestureResponderEvent.nativeEvent.shiftKey is set by react-native-web) and use it together with a remembered anchor index to expand the selection.
1. Widen the onCheckboxPress contract so callers can read the event, then forward event from the row that fires the press (TransactionItemRow's checkbox onPress already receives the GestureResponderEvent):
2. In Search/index.tsx, track the last toggled index and, when Shift is held, toggle every selectable item in stableSortedData between the anchor and the current row to the target state (selecting if the anchor became selected, deselecting if it became deselected — this matches Gmail / Finder semantics):
applyRangeSelection is a thin wrapper around the existing prepareTransactionsList / mapTransactionItemToSelectedEntry paths that already power the single-item branch — it loops the slice, skips disabled / pending-delete rows, and writes one combined setSelectedTransactions update. Group-list (TransactionGroupListItem) rows expand to their currentTransactions the same way the existing branch does.
3. Wire the event through the two intermediate hops (SearchList → handleLongPressRow already-noted call site at line 367, and ListItem.onSelectionButtonPress → TransactionItemRow's checkbox onPress). Both just pass the third argument straight through — no behavioural changes for non-web platforms, where shiftKey is always undefined.
4. Clear lastToggledKeyRef on clearSelectedTransactions so a fresh selection round starts without a stale anchor.
What alternative solutions did you explore? (Optional)
DOM-level keydown/keyup tracker for Shift to avoid plumbing the event through three layers. Works, but adds a global listener and a race where rapid Shift release between mousedown and click misses the modifier; reading event.nativeEvent.shiftKey at the click site is authoritative.
Implement it inside BaseSelectionList so every multi-select list inherits it. Tempting, but the reported lists (Reports, Expenses) use Search/SearchList, not BaseSelectionList, so we'd ship the helper and still need this PR. Worth a follow-up once this lands.
Use a Set<keyForList> anchor instead of an index to survive list re-sorts between clicks. Marginally more robust but slower (O(n) lookup twice per shift-click); since stableSortedData is, by construction, stable across a selection session, index lookup is fine and matches the existing prepareTransactionsList style.
changed the title [-]New Feature : Shift+Click Range Selection Not Supported in Multi-Select Lists[/-][+][$250] New Feature : Shift+Click Range Selection Not Supported in Multi-Select Lists[/+]on May 13, 2026
Please re-state the problem that we are trying to solve in this issue.
Currently, multi-select lists (Reports list, Expenses list) do not support Shift+Click range selection. Users expect that when they click a checkbox to select an item, then hold Shift and click another item farther down, all items between the two selections should automatically become selected. Instead, Shift+Click only toggles the clicked item, ignoring the shift modifier entirely.
What is the current gap in the implementation?
The Shift+Click range selection feature is absent from the multi-select list implementation. The insertion point is in src/components/Search/index.tsx in the toggleTransaction function (lines 1046–1144), which is the single entry point for all checkbox presses in both Reports and Expenses lists. Currently, toggleTransaction receives the clicked item but has no knowledge of the keyboard modifier (shiftKey) or a "pivot" point (the previously selected item). The Checkbox onPress callback at src/components/Search/SearchList/ListItem/BaseListItemHeader.tsx (line 147) passes only the item itself without the native event containing shiftKey information.
The working analog is found in standard desktop multi-select UI patterns, where holding Shift while clicking extends the selection range from the last clicked item to the newly clicked item. This pattern is implemented in many desktop applications (e.g., file managers, spreadsheets) and provides a fast way to select many items at once.
What changes do you think we should make in order to implement this feature?
File:src/components/Search/SearchList/ListItem/BaseListItemHeader.tsx Function:BaseListItemHeader Description: Modify the Checkbox onPress callback to capture and pass the native event object so that shiftKey information becomes available to the parent handler. The event parameter must be treated as optional and null-checked before use, since the Checkbox component may invoke onPress without an event on some platforms:
This replaces the current onPress={() => onCheckboxPress?.(item as unknown as TItem)} call. If event is null or undefined (e.g., on mobile where GestureResponderEvent does not carry a shiftKey field), the handler receives undefined and falls through to single-item toggle behavior — preserving existing mobile behavior exactly.
File:src/components/Search/SearchList/index.tsx Function:SearchList (onCheckboxPress callback receiver) Description: Update the onCheckboxPress callback signature to accept an optional event parameter and pass it through to the parent handler:
Modify toggleTransaction to accept an optional event parameter and implement range selection. The function must guard against a null pivot, an empty stableSortedData, and indices that cannot be resolved — falling back to single-item toggle in all such cases:
consttoggleTransaction=useCallback((item: SearchListItem,itemTransactions?: TransactionListItemType[],event?: GestureResponderEvent)=>{// existing early returns for non-transaction items stay here// Extract shiftKey safely — only available on web (KeyboardEvent/MouseEvent).// GestureResponderEvent on mobile does not expose shiftKey, so this evaluates// to false on iOS/Android, leaving single-item toggle intact.constisShiftHeld=!!(eventasunknownasMouseEvent|undefined)?.shiftKey;if(isShiftHeld&&lastSelectedPivot!==null&&stableSortedData.length>0){constpivotIndex=stableSortedData.findIndex((d)=>d.keyForList===lastSelectedPivot.keyForList);constclickedIndex=stableSortedData.findIndex((d)=>d.keyForList===item.keyForList);// If either item is no longer in the current data snapshot (e.g., data// changed between the pivot click and the shift+click), fall through to// single-item toggle so the user never ends up in a broken selection state.if(pivotIndex===-1||clickedIndex===-1){// fall through to single-item toggle below}else{conststart=Math.min(pivotIndex,clickedIndex);constend=Math.max(pivotIndex,clickedIndex);constrangeItems=stableSortedData.slice(start,end+1);// Build updatedTransactions from all valid, non-pending-delete items in rangeconstupdatedTransactions={ ...selectedTransactions};for(constrangeItemofrangeItems){// reuse existing per-item selection logic for each item in rangeprepareRangeItem(rangeItem,updatedTransactions);}setSelectedTransactions(updatedTransactions,filteredData);// Do NOT update lastSelectedPivot on shift+click — pivot stays fixed// so the user can extend or contract the range with subsequent shift+clicks.return;}}// Single-item toggle (existing logic)// ... existing toggleTransaction body ...setLastSelectedPivot(item);},[lastSelectedPivot,stableSortedData,selectedTransactions,filteredData],);
The isShiftHeld cast to MouseEvent is intentional: on web, the Pressable/Checkbox onPress synthetic event carries the underlying native browser event where shiftKey is a boolean. On mobile the cast resolves to undefined and !!undefined?.shiftKey is false, so the branch is never taken.
File:src/components/Search/index.tsx Function: (new helper prepareRangeItem or inline loop within toggleTransaction) Description: Implement the range item preparation: for each item in the range slice, check that it is a transaction-type item and is not pending delete before adding it to updatedTransactions. This mirrors the existing guard logic already present in toggleTransaction for single items, ensuring consistency.
Blast radius: The change is localized to the checkbox press flow. It affects only the multi-select interaction in SearchList (Reports and Expenses). No other UI flows are touched. Existing single-click behavior is preserved when Shift is not held. The range selection applies only to transaction-type items (other item types already have early returns in toggleTransaction). On mobile platforms, shiftKey is never set, so range selection is unreachable and mobile behavior is entirely unchanged.
What alternative approaches did you explore? (Optional)
⚠️@MobileMage Your proposal is a duplicate of an already existing proposal and has been automatically withdrawn to prevent spam. Please review the existing proposals before submitting a new one.
⚠️@mukhrr Thanks for your proposal. Please update it to follow the proposal template, as proposals are only reviewed if they follow that format (note the mandatory sections).
Yes it is but the account is rate-limited and I can't login, also not sure how many expenses left.
I have to create another account and duplicate but
The number of expenses created and deleted that way came up in a Slack discussion with @MonilBhavsar, and it caused some problems on the site, so I won't rebuild that data to record a demo for PR 4.
changed the title [-][Due for payment 2026-10-08] [$250] New Feature : Shift+Click Range Selection Not Supported in Multi-Select Lists[/-][+][$250] New Feature : Shift+Click Range Selection Not Supported in Multi-Select Lists[/+]on Oct 13, 2026
Activity
MelvinBot commentedon May 13, 2026
Proposal
What is the root cause of that problem?
The multi-select system is built on a simple per-item toggle model with no awareness of click modifier keys or item ordering. When a user clicks a checkbox,
toggleTransactionfires and toggles only that single item — it never receives the browserMouseEvent, so it cannot checkevent.shiftKey. There is no concept of a "last selected index" stored anywhere in the selection state (SearchContext.tsx), which means even if shift were detected there would be no anchor point to compute a range from.The same gap exists in the report-level transaction list (
MoneyRequestReportTransactionList.tsx).What changes do you think we should make in order to solve the problem?
Thread the native event through to the toggle handler — In
ListSelectionButton(and its parentListCheckbox/SelectableListItem), pass the clickGestureResponderEvent(orMouseEventon web) up throughonSelectRow/onCheckboxPressso the consumer can inspectevent.shiftKey.Track the last-selected index in
SearchContext— Add alastSelectedIndex: number | nullfield to the search selection state. Update it every time a single item is toggled.Implement range selection in
toggleTransaction— Whenevent.shiftKeyis true andlastSelectedIndexis set, compute the range between the anchor and the newly clicked index in the current sorted/visible item list, then select (or deselect) every item in that range. Reset the anchor to the new index afterward.Platform guard — Only enable on web/desktop where shift+click is a standard interaction pattern; on native mobile the shift key is not applicable and the behavior should remain unchanged.
What alternative solutions did you explore? (Optional)
Next Steps for Contributor+ team: Reply with
@MelvinBot implement thisto create a draft PR,@MelvinBot <your feedback>to refine this analysis, or explain why you are rejecting Melvin's proposal.mukhrr commentedon May 13, 2026
Proposal
What is the root cause of that problem?
Shift+Click range selection is a desktop-web convention that the multi-select machinery in
Searchnever implements. The checkbox callback the list exposes to consumers takes only the row item — there is no mouse event, no modifier flag, and no anchor index — so the handler inSearch/index.tsxhas nothing to range-select against:App/src/components/Search/SearchList/index.tsx
Lines 81 to 87 in 9d65949
The downstream consumer (
toggleTransaction) therefore can only flip the single clicked row, regardless of whether Shift was held:App/src/components/Search/index.tsx
Lines 1046 to 1056 in 9d65949
Result: every click goes through the per-row "toggle this one item" branch, and no anchor is ever remembered, so the platform-standard "click A, Shift+click B, everything in between selects" behaviour cannot trigger.
What changes do you think we should make in order to solve the problem?
I'd plumb the shift modifier through the existing checkbox callback (web-only —
GestureResponderEvent.nativeEvent.shiftKeyis set by react-native-web) and use it together with a remembered anchor index to expand the selection.1. Widen the
onCheckboxPresscontract so callers can read the event, then forwardeventfrom the row that fires the press (TransactionItemRow's checkboxonPressalready receives theGestureResponderEvent):2. In
Search/index.tsx, track the last toggled index and, when Shift is held, toggle every selectable item instableSortedDatabetween the anchor and the current row to the target state (selecting if the anchor became selected, deselecting if it became deselected — this matches Gmail / Finder semantics):applyRangeSelectionis a thin wrapper around the existingprepareTransactionsList/mapTransactionItemToSelectedEntrypaths that already power the single-item branch — it loops the slice, skips disabled / pending-delete rows, and writes one combinedsetSelectedTransactionsupdate. Group-list (TransactionGroupListItem) rows expand to theircurrentTransactionsthe same way the existing branch does.3. Wire the event through the two intermediate hops (
SearchList→handleLongPressRowalready-noted call site at line 367, andListItem.onSelectionButtonPress→TransactionItemRow's checkboxonPress). Both just pass the third argument straight through — no behavioural changes for non-web platforms, whereshiftKeyis always undefined.4. Clear
lastToggledKeyRefonclearSelectedTransactionsso a fresh selection round starts without a stale anchor.What alternative solutions did you explore? (Optional)
event.nativeEvent.shiftKeyat the click site is authoritative.BaseSelectionListso every multi-select list inherits it. Tempting, but the reported lists (Reports, Expenses) useSearch/SearchList, notBaseSelectionList, so we'd ship the helper and still need this PR. Worth a follow-up once this lands.Set<keyForList>anchor instead of an index to survive list re-sorts between clicks. Marginally more robust but slower (O(n) lookup twice per shift-click); sincestableSortedDatais, by construction, stable across a selection session, index lookup is fine and matches the existingprepareTransactionsListstyle.Result:
REC-20260513215053.mp4
melvin-bot commentedon May 13, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @QichenZhu (
External)MobileMage commentedon May 13, 2026
🚫 Duplicated proposal withdrawn by 🤖 ProposalPolice.
[-]New Feature : Shift+Click Range Selection Not Supported in Multi-Select Lists[/-][+][$250] New Feature : Shift+Click Range Selection Not Supported in Multi-Select Lists[/+]melvin-bot commentedon May 13, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022054603471216095472
aswin-s commentedon May 13, 2026
Please re-state the problem that we are trying to solve in this issue.
Currently, multi-select lists (Reports list, Expenses list) do not support Shift+Click range selection. Users expect that when they click a checkbox to select an item, then hold Shift and click another item farther down, all items between the two selections should automatically become selected. Instead, Shift+Click only toggles the clicked item, ignoring the shift modifier entirely.
What is the current gap in the implementation?
The Shift+Click range selection feature is absent from the multi-select list implementation. The insertion point is in
src/components/Search/index.tsxin thetoggleTransactionfunction (lines 1046–1144), which is the single entry point for all checkbox presses in both Reports and Expenses lists. Currently,toggleTransactionreceives the clicked item but has no knowledge of the keyboard modifier (shiftKey) or a "pivot" point (the previously selected item). The Checkbox onPress callback atsrc/components/Search/SearchList/ListItem/BaseListItemHeader.tsx(line 147) passes only the item itself without the native event containing shiftKey information.The working analog is found in standard desktop multi-select UI patterns, where holding Shift while clicking extends the selection range from the last clicked item to the newly clicked item. This pattern is implemented in many desktop applications (e.g., file managers, spreadsheets) and provides a fast way to select many items at once.
App/src/components/Search/index.tsx
Lines 1046 to 1080 in c5403b4
App/src/components/Search/SearchList/ListItem/BaseListItemHeader.tsx
Lines 145 to 152 in c5403b4
What changes do you think we should make in order to implement this feature?
File:
src/components/Search/SearchList/ListItem/BaseListItemHeader.tsxFunction:
BaseListItemHeaderDescription: Modify the Checkbox
onPresscallback to capture and pass the native event object so thatshiftKeyinformation becomes available to the parent handler. The event parameter must be treated as optional and null-checked before use, since the Checkbox component may invokeonPresswithout an event on some platforms:This replaces the current
onPress={() => onCheckboxPress?.(item as unknown as TItem)}call. Ifeventis null or undefined (e.g., on mobile whereGestureResponderEventdoes not carry ashiftKeyfield), the handler receivesundefinedand falls through to single-item toggle behavior — preserving existing mobile behavior exactly.File:
src/components/Search/SearchList/index.tsxFunction:
SearchList(onCheckboxPress callback receiver)Description: Update the
onCheckboxPresscallback signature to accept an optional event parameter and pass it through to the parent handler:File:
src/components/Search/index.tsxFunction:
toggleTransactionDescription: Add state to track the last selected pivot item:
Modify
toggleTransactionto accept an optional event parameter and implement range selection. The function must guard against a null pivot, an emptystableSortedData, and indices that cannot be resolved — falling back to single-item toggle in all such cases:The
isShiftHeldcast toMouseEventis intentional: on web, the Pressable/Checkbox onPress synthetic event carries the underlying native browser event whereshiftKeyis a boolean. On mobile the cast resolves toundefinedand!!undefined?.shiftKeyisfalse, so the branch is never taken.File:
src/components/Search/index.tsxFunction: (new helper
prepareRangeItemor inline loop withintoggleTransaction)Description: Implement the range item preparation: for each item in the range slice, check that it is a transaction-type item and is not pending delete before adding it to
updatedTransactions. This mirrors the existing guard logic already present intoggleTransactionfor single items, ensuring consistency.Blast radius: The change is localized to the checkbox press flow. It affects only the multi-select interaction in SearchList (Reports and Expenses). No other UI flows are touched. Existing single-click behavior is preserved when Shift is not held. The range selection applies only to transaction-type items (other item types already have early returns in
toggleTransaction). On mobile platforms,shiftKeyis never set, so range selection is unreachable and mobile behavior is entirely unchanged.What alternative approaches did you explore? (Optional)
N/A
github-actions commentedon May 13, 2026
github-actions commentedon May 13, 2026
138 remaining items
situchan commentedon Oct 7, 2026
Isn't 120 reports enough for test?
TaduJR commentedon Oct 7, 2026
Yes it is but the account is rate-limited and I can't login, also not sure how many expenses left.
I have to create another account and duplicate but
stitesExpensify commentedon Oct 7, 2026
Yes we can proceed
melvin-bot commentedon Oct 7, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)[-][Due for payment 2026-10-08] [$250] New Feature : Shift+Click Range Selection Not Supported in Multi-Select Lists[/-][+][$250] New Feature : Shift+Click Range Selection Not Supported in Multi-Select Lists[/+]mallenexpensify commentedon Oct 13, 2026
Please comment and tag me if/when payment is due, thx
TaduJR commentedon Oct 14, 2026
4th PR is ready and in review.