feat: Shift+Click Range Selection Not Supported in Multi-Select Lists#92155
Conversation
…ttonPress is undefined
|
@QichenZhu Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
|
@codex review |
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
|
Codex Review: Didn't find any major issues. Delightful! ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
We have upstream bug on react-native-key-command for the above case. Should we remove it for now and make a second PR, or handle it first on upstream? |
|
@TaduJR I'm not sure this is relevant to the original issue, which only mentions Shift+Click. I strongly suggest writing the steps yourself to reflect the actual testing flow you did, as AI tends to generate excessive steps in seconds but testers need hours to execute them, which is not practical. |
Explanation of Change
Implements Excel/AG Grid-style shift+click range selection across every multi-select list in the app. Click a row, hold Shift, click another — every row between them gets selected. Shift+click again to extend or shrink the range. Shift+Arrow does the same via keyboard.
Hook:
src/hooks/useShiftRangeSelection.ts— an explicit, deterministic session protocol. Consumers call:applyShiftClick(item, options)for shift+clicknotifyAnchor(item)on plain click / focus changeclearAnchor()on Select All / Deselect AllextendByKeyboard(direction)for Shift+ArrowgetAnchorKey()to read the current anchorThe shift session lives between shift+clicks and is ended only by
notifyAnchororclearAnchor— no focus-based heuristic. Headers and disabled rows are excluded viaisHeaderItem/isDisabledItempredicates; ranges span the visual order; the anchor always joins the range (matches Excel / Gmail / Outlook / Finder).Consumer surfaces: 13 workspace pages wire it via
BaseSelectionList's opt-inonShiftRangeApplyprop (Tags, Categories, Members, Taxes, Rules, Distance Rates, Per Diem, Report Fields, Spend Rule Card / Category, Expense Rules, Room Members, Report Participants). Three lists with custom selection state — Search (all views including Spend grouped and Reports), MoneyRequestReportView's transaction list (including in-report grouped-by-category/tag layouts), and Workspace Expensify Card list — wire the hook directly.Group-header semantics: In grouped views (Spend grouped + in-report grouped), shift+click on a group header extends the range through the whole group by routing to
applyShiftClickwith the loaded child farthest from the anchor. Collapsed groups fall back to the existing "select all in group" toggle.Keyboard: New
SHIFT_ARROW_UP/DOWNshortcuts inCONST/index.ts(withmodifierFlags: keyModifierShiftfor native KeyCommand).BaseSelectionListregisters them;extendByKeyboardwalks the visual order skipping headers/disabled rows.Helpers exported alongside the hook:
applyShiftRangeBatchToKeySet— primitive-key selection arrays (string[]/number[])applyShiftRangeBatchToValueArray— object-keyed arrays storing hydrated values (WorkspacePerDiemPage)getShiftKeyFromEvent— extractsshiftKeyfrom any RN / RN-Web / DOM event shapeFixed Issues
$ #90539
PROPOSAL: #90539 (comment)
Tests
Setup
A. Workspace lists — basic shift+click range
Use Workspace → Tags as the representative page. Repeat on Categories, Members, Taxes, Distance Rates, Per Diem Rates, Report Fields, Expense Rules, Spend Rules → Card / Category, Room Members, Report Participants.
B. Workspace lists — deselect-then-shift (anchor stays in range)
C. Workspace lists — Select All then shift+click
D. Shift+Arrow keyboard nav (desktop with physical keyboard)
E. Single-select radio picker — checkbox still selects
F. Spend → Expenses (ungrouped)
G. Spend → Reports
H. Spend grouped — child range + group-header range
Use group by Category as the representative. Repeat for Tag, Merchant, Card, Member, Month, Week, Year, Quarter, Withdrawal ID.
I. Single expense report — in-report transaction list
J. Workspace Expensify Cards list
K. Report Fields list values — enabled/disabled state preserved
Offline tests
Same as tests
QA Steps
// TODO: These must be filled out, or the issue title must include "[No QA]."
Same as tests
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectioncanBeMissingparam foruseOnyxtoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari