Skip to content

Move pre-selected option to top in shared SelectionScreen (accounting selectors) - #101719

Merged
robertjchen merged 26 commits into
Expensify:mainfrom
marufsharifi:fix/selection-screen-shared-pin
Oct 20, 2026
Merged

robertjchen merged 26 commits into
Expensify:mainfrom
marufsharifi:fix/selection-screen-shared-pin

Conversation

@marufsharifi

@marufsharifi marufsharifi commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

Part of the "move pre-selected list items to top" work

This PR adds the "pin the pre-selected item to the top" behavior directly in the shared SelectionScreen component, so every selector built on it gets the behavior from a single place instead of editing each page individually.

Fixed Issues

$ #69184
PROPOSAL: #69184 (comment)

Tests

These steps verify it on the QuickBooks Online → Advanced → foreign-currency expense account selector (the reachable QBO one), but the same behavior applies to all ~24 searchable accounting selectors since they share this code path.

**Setup (DevTools console)** — unlocks the FX selector and seeds a long, pre-selected list
const policyID = 'PASTE_POLICY_ID'; // from any /workspaces/<policyID>/... URL

// 1) Enable the beta that surfaces the FX expense-account selector (keeps existing betas)
const betas = (await window.Onyx.get('betas')) ?? [];
await window.Onyx.set('betas', Array.from(new Set([...betas, 'globalReimbursementFX'])));

// 2) Satisfy the other gates + seed 15 expense accounts + pre-select a mid-list one
const expenseAccounts = Array.from({length: 15}, (_, i) => {
  const n = String(i + 1).padStart(2, '0');
  return {id: `test-exp-${n}`, name: `Test Expense Account ${n}`, currency: 'USD'};
});
await window.Onyx.merge(`policy_${policyID}`, {
  reimbursementChoice: 'reimburseYes',
  achAccount: {bankAccountID: 123456},
  connections: {quickbooksOnline: {data: {expenseAccounts}, config: {fxExpenseAccount: 'test-exp-10'}}},
});

(Run this right before testing and stay in the app — a server reconnect can overwrite these optimistic values.)

  1. Go to Workspace → Accounting → QuickBooks Online → Advanced.
  2. Open the foreign-currency expense account row.
  3. Verify: a search box is shown, and "Test Expense Account 10" (the pre-selected account) is pinned at the top of the list — not at its natural 10th position, and the list is not opened scrolled to the middle.
  4. Type Test in the search box to filter the list, then clear the search.
  5. Verify: "Test Expense Account 10" is still at the top and did not slip below the fold after clearing.
  • Verify that no errors appear in the JS console

Offline tests

Same as Test

QA Steps

// TODO: These must be filled out, or the issue title must include "[No QA]."
Same as Test

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
checklist.mov
Android: mWeb Chrome
iOS: Native
checklist.mov
iOS: mWeb Safari
MacOS: Chrome / Safari
check.list.mov

@codecov

codecov Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/components/SelectionScreen.tsx 100.00% <100.00%> (+100.00%) ⬆️
src/hooks/useSelectionListSearch.ts 100.00% <100.00%> (ø)
src/libs/ListUtils.tsx 100.00% <100.00%> (ø)
src/libs/SelectionListOrderUtils.ts 100.00% <ø> (ø)
.../export/CampfireCardProgramAccountSelectorPage.tsx 0.00% <0.00%> (ø)
...export/DualEntryCardProgramAccountSelectorPage.tsx 0.00% <0.00%> (ø)
...rillet/export/RilletCardProgramAccountSelector.tsx 0.00% <0.00%> (ø)
.../export/BusinessCentralPaymentMethodSelectPage.tsx 0.00% <0.00%> (ø)
...re/export/CampfireDefaultCompanyCardVendorPage.tsx 0.00% <0.00%> (ø)
...y/export/DualEntryDefaultCompanyCardVendorPage.tsx 0.00% <0.00%> (ø)
... and 2 more
... and 10 files with indirect coverage changes

@marufsharifi
marufsharifi marked this pull request as ready for review October 1, 2026 06:05
@marufsharifi
marufsharifi requested review from a team as code owners October 1, 2026 06:05
@melvin-bot
melvin-bot Bot requested review from JmillsExpensify and linhvovan29546 and removed request for a team October 1, 2026 06:05
@melvin-bot

melvin-bot Bot commented Oct 1, 2026

Copy link
Copy Markdown

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

@melvin-bot
melvin-bot Bot removed the request for review from a team October 1, 2026 06:06
Comment thread src/components/SelectionScreen.tsx Resolved

@JmillsExpensify JmillsExpensify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No product review required on this one.

@marufsharifi

Copy link
Copy Markdown
Contributor Author

@linhvovan29546 Could you please check this if it is a bug and should I fix it:

  • Go to Workspace → Accounting → QuickBooks Online → Advanced.
  • Open QuickBooks Online currency conversion fallback account.
  • Make sure Test Expense Account 10 is the originally selected account and is pinned at the top.
  • Search for Test Expense Account 05.
  • Select Test Expense Account 05.
  • Clear the search field.
  • Observe the list position.

After clearing the search, the list automatically scrolls toward the new selected item (Test Expense Account 05).

screenrecord.mov

@marufsharifi

Copy link
Copy Markdown
Contributor Author

@linhvovan29546, Gentle bump. thanks.

@linhvovan29546

Copy link
Copy Markdown
Contributor

Could you please check this if it is a bug and should I fix it:

I don’t think this is the bug.

@linhvovan29546

Copy link
Copy Markdown
Contributor

@MelvinBot Review

@MelvinBot

Copy link
Copy Markdown
Contributor

Code review

The approach looks right: pinning before filtering in useSelectionListSearch covers about 40 selectors in one place. I found one behavior issue and a few cleanups. CI is green apart from the checklist and approval jobs.

1. Multi-select pages pin only the first pre-selected item. useSelectionListSearch.ts:20 uses data.find(...), so only one value is saved. MergeATSFilterSelectionPage.tsx:67 is a canSelectMultiple list that uses this hook. With 12 or more stages and 3 pre-selected, only the first moves to the top. The other two stay in place. Shared MultiSelect pins every initial selection. Suggested fix:

const initialSelectedValues = useInitialValue(() => data.filter((item) => item.isSelected).map((item) => item.value));
const orderedData = moveInitialSelectionToTop(data, initialSelectedValues);

This also uses the existing useInitialValue hook instead of a raw useState. SingleSelect and MultiSelect already use it.

2. getKey is never used. No caller passes the new third argument to moveInitialSelectionToTop. Please revert the signature and doc change unless a caller needs it. The test comment at SelectionScreenTest.tsx:48 says the tests prove matching on keyForList, but no test checks that. Please remove that comment.

3. Small fix to the comment at SelectionScreen.tsx:150. Change "This Only" to "This only." The comment should also say why mount-scroll is off: the pinned row is already at the top.

Checked and fine:

  • Accounting pages load connection data before they render, through withPolicyConnections. The selection is present on the first render.
  • Every SelectionScreen that passes textInputOptions now pins upstream. The Xero contact page and the company card export page both pin it themselves.
  • Disabling mount-scroll for searchable lists matches the other pinned pages, such as CountrySelectionList and AssigneeStep.
  • I agree the scroll-to-new-selection after clearing search is not a bug.

view run

const shouldShowTextInput = supplierOptions.length >= CONST.STANDARD_LIST_ITEM_LIMIT;

// Pin the selected supplier to the top of the full list before filtering, so it stays pinned while searching.
const orderedSupplierOptions = useMemo(() => moveInitialSelectionToTop(supplierOptions, currentContactID ? [currentContactID] : []), [supplierOptions, currentContactID]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove useMemo. The DynamicXeroNonReimbursableDefaultContactSelectPage page has been automatically memoized by the React Compiler.

Comment thread src/hooks/useSelectionListSearch.ts Outdated
const [searchValue, setSearchValue, filteredData] = useSearchResults(data, (item, searchInput) => tokenizedSearch([item], searchInput, () => [item.text ?? '', item.value]).length > 0);
// Freeze the items selected when the list opened so they stay pinned while staged selections change and the search
// filters the list. All pre-selected values are kept so multi-select lists pin every one, not just the first.
const initialSelectedValues = useInitialValue(() => data.filter((item) => item.isSelected).map((item) => item.value));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don’t we use useInitialSelection?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@linhvovan29546 useInitialSelection always calls useFocusEffect internally, which would add a navigation-context dependency to this shared hook (used by ~40 selectors) and break its renderHook unit test. We also don't want its resetOnFocus behavior here: the hook only sees the live isSelected, so re-snapshotting on focus would re-pin the staged selection — the exact jump we're preventing. Without resetOnFocus it's just useInitialValue with extra weight, so useInitialValue (which SingleSelect/MultiSelect already use) is the lighter, correct fit — freeze once at mount.

@marufsharifi

Copy link
Copy Markdown
Contributor Author

@robertjchen, could you please feel free to review the code, but hold off on merging for now? I’m doing another round of testing. Thanks!

robertjchen
robertjchen previously approved these changes Oct 6, 2026
@marufsharifi

Copy link
Copy Markdown
Contributor Author

@linhvovan29546, could you please take another look, this is done from my side. thanks.

@melvin-bot
melvin-bot Bot requested a review from robertjchen October 19, 2026 14:31

@robertjchen robertjchen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@robertjchen
robertjchen merged commit 45d395e into Expensify:main Oct 20, 2026
36 of 39 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🚧 robertjchen has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/robertjchen in version: 9.5.15-1 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

Copy link
Copy Markdown
Contributor

No help site update needed. This PR only changes list order, so the pre-selected option now appears at the top of long, searchable accounting selectors. No article under docs/articles describes the order of these lists, so no sentence is now wrong.


view run

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/grgia in version: 9.5.15-3 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants