Stop scroll jump on search filter participant selection#92045
Stop scroll jump on search filter participant selection#92045MelvinBot wants to merge 25 commits into
Conversation
Co-authored-by: mkhutornyi <mkhutornyi@users.noreply.github.com>
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppAndroid: mWeb ChromeiOS: HybridAppiOS: mWeb SafariMacOS: Chrome / Safari |
| chatOptions.recentReports = chatOptions.recentReports.filter((report) => report.accountID !== currentUserOption.accountID); | ||
| } | ||
|
|
||
| const formattedResults = formatSectionsFromSearchTerm( |
There was a problem hiding this comment.
@MelvinBot formatSectionsFromSearchTerm is no longer used. Is this safe?
There was a problem hiding this comment.
Yes, safe.
-
formatSectionsFromSearchTermis no longer called from this file, but it's still used elsewhere (e.g.SearchFiltersChatsSelector.tsx:111,InSelector.tsx:103,MoneyRequestAttendeeSelector.tsx:211,ParticipantSearchResults.tsx:271) and exported fromOptionsListUtils/index.ts:3341. We're only dropping the call from this component, not the function itself. -
Its previous role here (extracting selected items into a dedicated top section) is replaced by:
shouldKeepSelectedInAvailableOptions: true→ selected items stay in Recents / Contacts with theirisSelectedflag instead of being yanked into a separate section.shouldSeparateNonExistingSelectedOptions: true+ theselectedNonExistingOptionssection → covers the only previously-covered case where a selected item wouldn't appear in Recents/Contacts (name-only attendees).- The dedicated
currentUserOptionsection at the top covers the current-user-at-top case.
-
Same pattern was already applied in Stop scroll jump on room invite member selection #91387 (
RoomInvitePage) and Stop scroll jump on workspace invite member selection #91250 (DynamicWorkspaceInvitePage), so the swap is well-trodden.
| // Selected options not present in personalDetails / recentReports (e.g. name-only attendees | ||
| // for the attendee filter). These need their own section so they stay visible. The current | ||
| // user is excluded since they already have a dedicated section above. | ||
| const extraSelectedOptions = selectedNonExistingOptions.filter((option) => !option.accountID || option.accountID !== currentUserAccountID); | ||
| if (extraSelectedOptions.length > 0) { | ||
| newSections.push({ | ||
| title: '', | ||
| data: extraSelectedOptions, | ||
| sectionIndex: 1, | ||
| }); | ||
| } |
There was a problem hiding this comment.
@MelvinBot I think selectedNonExistingOptions won't exist as server side search is never performed on this page. Can you confirm? If so, we can completely remove this section.
There was a problem hiding this comment.
Can't fully confirm — selectedNonExistingOptions is not tied to server-side search; it's populated whenever a selected option's login is not present in availableOptions.personalDetails. See useSearchSelector/base.ts:429-434:
const selectedNonExistingOptions = shouldSeparateNonExistingSelectedOptions
? (() => {
const personalDetailLogins = new Set(filteredPersonalDetails.map((option) => option.login).filter(Boolean));
return selectedOptionsForDisplay.filter((option) => !personalDetailLogins.has(option.login));
})()
: [];This component is shared by From / To / Assignee / Attendee filters:
- For From / To / Assignee (
shouldAllowNameOnlyOptions: false): pre-selected options are looked up byaccountIDinpersonalDetailsand dropped if not found (SearchFiltersParticipantsSelector.tsx:254-266), soselectedNonExistingOptionswill be empty in practice. You're right for this case. - For Attendee (
shouldAllowNameOnlyOptions: true): name-only attendees haveaccountID = CONST.DEFAULT_NUMBER_IDand aren't inpersonalDetails(SearchFiltersParticipantsSelector.tsx:26-50,:230-251). They WILL show up inselectedNonExistingOptions, and without this section they'd disappear from the list when selected.
So this section is load-bearing for the Attendee filter and shouldn't be removed. If you'd prefer, we could gate it behind shouldAllowNameOnlyOptions to make the intent explicit — happy to push that if you want.
There was a problem hiding this comment.
@MelvinBot I confirmed there's no case of selectedNonExistingOptions in Attendees filter. Let's remove this section
There was a problem hiding this comment.
Done — removed the selectedNonExistingOptions section, the shouldSeparateNonExistingSelectedOptions: true config flag, and the now-unused getEmptyArray import in 67b72ec.
There was a problem hiding this comment.
Done in 2bd5dcb1. Added a local extraSelectedOptions section (login-keyed, mirroring the extraSelectedReports pattern from SearchFiltersChatsSelector.tsx:111-123) above the Recents / Contacts sections so name-only attendees stay visible when selected. No new dependency on the hook's selectedNonExistingOptions.
AI test results
| Test | Result |
|---|---|
prettier --check |
✅ Passed |
eslint (changed file) |
✅ Passed |
typecheck-tsgo |
✅ No new errors on changed file (pre-existing errors in MapView/utils.ts, ConvertGpsPointsTo2DArray.ts, useClearSelectedDomainMembersOnMoveComplete.ts, TryNewDotUtilsTest.ts are unrelated) |
react-compiler-compliance-check check-changed |
getMainBranchCommitHash failed on the Melvin sandbox (origin/ resolves to empty in this shallow-clone setup). Change is a small computation inside an existing useMemo; no new components or hooks introduced. |
jest tests/unit/useSearchSelectorTest.tsx / useAdvancedSearchFilters.test.ts |
Not re-run — hook and useAdvancedSearchFilters aren't touched by this change. |
Co-authored-by: mkhutornyi <mkhutornyi@users.noreply.github.com>
| sectionIndex: 1, | ||
| data: chatOptions.recentReports, | ||
| sectionIndex: 2, | ||
| }); | ||
|
|
||
| newSections.push({ | ||
| title: '', | ||
| data: chatOptions.personalDetails, | ||
| sectionIndex: 2, | ||
| sectionIndex: 3, |
There was a problem hiding this comment.
@MelvinBot keep original sectionIndex as section 1 was removed
Co-authored-by: mkhutornyi <mkhutornyi@users.noreply.github.com>
|
Done — kept the original The |
|
@MelvinBot let's also fix SearchFiltersChatsSelector as similar component |
Apply the same fix as SearchFiltersParticipantsSelector to the chats selector — keep selected items in their natural position with isSelected instead of moving them into a top section, and prevent auto-scroll on toggle via shouldUpdateFocusedIndex and shouldPreventAutoScrollOnSelect. Co-authored-by: mkhutornyi <mkhutornyi@users.noreply.github.com>
|
Applied the same fix pattern to
AI Tests
|
|
The failing test check is unrelated to this PR — it's a flaky/pre-existing failure on Failure: Why it's unrelated:
The other failing check ( Per MelvinBot policy I do not push fixes for unrelated/flaky failures — please re-run the failed Jest shard. |
Code ReviewOverviewThis PR stops the scroll jump and section reshuffle that happened when toggling rows in the Search advanced-filters participant and chat selectors. Instead of pulling selected rows into a dedicated "selected" section at the top (via What changed
Findings1. Potential regression for name-only attendees (Attendee filter) — medium 2. PR description out of sync with diff — low 3. 4. Missing Tests
Security / perf
SummaryThe simplification looks correct for the common path and the scroll-position fix is well-targeted. The main thing I'd want confirmed before merge is the name-only attendee case (finding #1) — if that path is broken, the fix needs a small extras-section equivalent like the Chats selector has. The PR description also needs to be brought in line with the current diff. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3a19cd57fb
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
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".
| @@ -180,26 +145,14 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate, | |||
| sectionIndex: 2, | |||
There was a problem hiding this comment.
Preserve selected participants outside visible options
When this selector opens with existing filters whose participants are not in the current availableOptions page/search result, the selected rows disappear from the list because the selected section was removed and the rendered data is now only chatOptions.recentReports plus chatOptions.personalDetails. shouldKeepSelectedInAvailableOptions only keeps selected items that getValidOptions already returned; it does not append selectedOptions initialized from initialAccountIDs. In that scenario the filter remains applied when pressing Apply, but the user cannot see or deselect it unless they know to search for that exact person, which regresses the previous selected section behavior.
Useful? React with 👍 / 👎.
@MelvinBot restore |
|
The failing checks are unrelated to this PR.
No code changes pushed — re-running the job should clear the flaky test failure. |
|
@MelvinBot when list no less than |
Restores the extraSelectedReports-style local pattern from SearchFiltersChatsSelector to keep selected items (notably name-only attendees in the Attendee filter) visible when they aren't present in the personalDetails / recentReports sections. Co-authored-by: mkhutornyi <mkhutornyi@users.noreply.github.com>
|
MelvinBot has made 5 consecutive commits trying to fix CI failures. Automatic CI fix dispatches are now paused for this PR. Next steps: A human should review the recent commits and resolve the conflicting CI errors manually. |
|
Before I push, want to confirm I have the intent right since this is a behavior change and there's been significant churn already. My interpretation:
Open questions before I push:
If those three match your intent, I'll implement and push. If any differ, lmk and I'll adjust before writing code. |
|
@MelvinBot do this |
- Capture pre-selected options into a frozen 'selected' section at the top on the first render with data when Recents + Contacts >= 12 items. - Selecting a row in the captured section keeps it in place; only its isSelected flag updates. - New selections stay in their natural Recents/Contacts position. - Captured items are filtered out of Recents/Contacts to avoid duplicates. - Apply the same pattern to SearchFiltersChatsSelector. Order: pre-selected -> extra-selected (non-existing) -> current user -> recents -> contacts. Co-authored-by: mkhutornyi <mkhutornyi@users.noreply.github.com>
|
Pushed in d6f84ac. Implemented all three confirmed answers:
Implementation notes for review:
Initial AI tests passed locally (prettier, lint on changed, react-compiler COMPILED for both files, |
|
@MelvinBot selected items should be filtered out from search. |
Co-authored-by: mkhutornyi <mkhutornyi@users.noreply.github.com>
|
Done in AI test results
|
|
@MelvinBot is it possible to extract common hook and utils for putting pre-selected items at the top on first render and then lock? |
- Hook now takes a sections array + snapshot source + getKey callback, returns
{frozenSections, listSections, isFrozen}. Caller no longer has to dedupe
frozen rows out of Recents/Contacts or rebuild the frozen section with
isSelected — the hook walks input sections to take the live row, so toggles
refresh in place automatically.
- Identity is now controlled by a getKey callback (no longer constrained to
keyForList). Chats keys by reportID; participants prefers accountID and
falls back to login for name-only attendees.
- buildFrozenSection and excludeFrozenItems helpers removed — encapsulated
inside the hook. SelectionListOrderUtils only exports moveInitialSelectionToTop.
- Tests rewritten for the new shape.
Co-authored-by: mkhutornyi <mkhutornyi@users.noreply.github.com>
|
Done — pushed Implemented the shape from Hook API ( useFrozenPreSelection<TItem>({
sections, // main list (Recents / Contacts)
snapshotSource, // items considered pre-selected at first ready render
getKey, // identity callback — no keyForList dependency
isReady,
visibleCount,
canCapture?,
threshold?,
frozenSectionIndex?,
}) → {frozenSections, listSections, isFrozen}The hook walks Callers (
AI test results
|
|
@MelvinBot Remove all unnecessary params. As I said, keep this simple. REVERT all changes and rework on this New params:
No need return values:
|
…canCapture Co-authored-by: mkhutornyi <mkhutornyi@users.noreply.github.com>
|
Done — pushed The hook is now exactly the shape from your spec: 3 positional params, single return value. function useFrozenPreSelection<TItem extends ListItem>(
sections: Array<Section<TItem>>,
initialSelectedValues: string[],
canCapture: boolean,
): Array<Section<TItem>>
Removed Files
One naming note: I kept the returned array named |
| */ | ||
| function useFrozenPreSelection<TItem extends ListItem>(sections: Array<Section<TItem>>, initialSelectedValues: string[], canCapture: boolean): Array<Section<TItem>> { | ||
| // null = not captured yet; [] = captured but list was too short to pin. | ||
| const [frozenKeys, setFrozenKeys] = useState<string[] | null>(null); |
There was a problem hiding this comment.
@MelvinBot convert frozenKeys, captured to Set.
Then no need liveByKey, seen as redundant
There was a problem hiding this comment.
Done in a3c25bd — frozenKeys and captured are now Set<string>, and seen + liveByKey are gone. Set.add dedupes during capture, and the emit loop now walks sections directly and pushes any item whose key is in frozenKeys, since the input sections don't have cross-section dupes (the consumers already filter extraSelectedReports / extraSelectedOptions out of Recents/Contacts before calling the hook).
AI test results
| Test | Result |
|---|---|
prettier --check |
✅ Passed |
eslint (changed file) |
✅ Passed |
typecheck-tsgo |
✅ No new errors on changed file (pre-existing errors in MapView/utils.ts, ConvertGpsPointsTo2DArray.ts, useClearSelectedDomainMembersOnMoveComplete.ts, TryNewDotUtilsTest.ts are unrelated) |
react-compiler-compliance-check check src/hooks/useFrozenPreSelection.ts |
✅ COMPILED |
jest tests/unit/useSearchSelectorTest.tsx |
✅ Passed (15 tests) |
jest tests/unit/hooks/useAdvancedSearchFilters.test.ts |
✅ Passed (18 tests) |
Co-authored-by: mkhutornyi <mkhutornyi@users.noreply.github.com>
| const initialSet = new Set(initialSelectedValues); | ||
| const captured = new Set<string>(); | ||
| for (const section of sections) { | ||
| for (const item of section.data) { | ||
| const key = item.keyForList; | ||
| if (key && initialSet.has(key)) { | ||
| captured.add(key); | ||
| } | ||
| } | ||
| } | ||
| setFrozenKeys(captured); |
There was a problem hiding this comment.
@MelvinBot is this logic needed? Why not just set setFrozenKeys(new Set(initialSelectedValues))?
There was a problem hiding this comment.
Done in a2b945b — collapsed the capture to setFrozenKeys(totalCount < STANDARD_LIST_ITEM_LIMIT ? new Set() : new Set(initialSelectedValues)). The downstream "walk sections" loop already drops captured keys missing from sections, so the existing test "drops frozen items that are not present in any input section" still passes — covering the case the removed intersection used to guard against. All 6 hook tests + 33 consumer tests pass.
|
It's needed. This matters in practice for the chats selector: |
@MelvinBot Why would search happen when first render? It's bug. Search input value is empty as default. |
|
You're right — my earlier reasoning was wrong on this point.
|
@MelvinBot ok then let's do this |
Co-authored-by: mkhutornyi <mkhutornyi@users.noreply.github.com>
| // Walk sections in order to collect the latest live rows for frozen keys so toggles refresh in place. | ||
| const frozenData: TItem[] = []; | ||
| for (const section of sections) { | ||
| for (const item of section.data) { | ||
| if (item.keyForList && frozenKeys.has(item.keyForList)) { | ||
| frozenData.push(item); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const filteredSections = sections.map((section) => ({ | ||
| ...section, | ||
| data: section.data.filter((item) => !item.keyForList || !frozenKeys.has(item.keyForList)), | ||
| })); |
There was a problem hiding this comment.
@MelvinBot we can avoid double loop through sections.
How about simplifying like this?
const filteredSections = sections.map((section) => {
const data = [];
for (const item of section.data) {
if (item.keyForList && frozenKeys.has(item.keyForList)) {
frozenData.push(item);
} else {
data.push(item);
}
}
return {...section, data};
});
There was a problem hiding this comment.
Applied in bf634e9. Folded the collect-frozen pass into the section-filter .map so we only walk sections once instead of twice.
Tests:
jest tests/unit/hooks/useFrozenPreSelection.test.ts— 6/6 passedjest tests/unit/SelectionListOrderUtilsTest.ts tests/unit/useSearchSelectorTest.tsx— 17/17 passedprettier,eslint,react-compiler-compliance-check— clean
|
Yes — that's a nicer shape than what I sketched, and Two small things to be aware of so the behavior stays equivalent:
Roughly: const frozenData: T[] = [];
const seen = new Set<string>();
const filteredSections = sections.map((section) => {
const data: T[] = [];
for (const item of section.data) {
if (item.keyForList && frozenKeys.has(item.keyForList)) {
frozenData.push(item);
seen.add(item.keyForList);
} else {
data.push(item);
}
}
return {...section, data};
});
// Backfill snapshot items that weren't in any section
for (const item of frozenSnapshot) {
if (item.keyForList && !seen.has(item.keyForList)) {
frozenData.push(item);
}
}Happy to roll this into the follow-up PR shape — |
@MelvinBot It's fine
@MelvinBot I don't see any case where pre-selected items don't exist in |
|
Good point — you're right. With The only edge case is a pre-selected item that the user deselects during the session AND that no longer matches the current search term. Today's code would still pin it (with Dropping the snapshot backfill. The follow-up hook shape becomes:
Caller still owns the "extra selected" section since Chats vs. Participants differ on inclusion rules. |
Co-authored-by: mkhutornyi <mkhutornyi@users.noreply.github.com>
| */ | ||
| function useFrozenPreSelection<TItem extends ListItem>(sections: Array<Section<TItem>>, initialSelectedValues: string[], canCapture: boolean): Array<Section<TItem>> { | ||
| // null = not captured yet; empty Set = captured but list was too short to pin. | ||
| const [frozenKeys, setFrozenKeys] = useState<Set<string> | null>(null); |
There was a problem hiding this comment.
@MelvinBot when pre-selected items are not in paginated list, they disappear when unselect.
To prevent this, let's store full items (<key, item>) instead of just keys.
Pseudocode:
const [frozenData, setFrozenData] = useState(null);
if (frozenData === null && canCapture) {
const totalCount = sections.reduce((sum, section) => sum + section.data.length, 0);
if (totalCount < CONST.STANDARD_LIST_ITEM_LIMIT) {
setFrozenData(empty);
} else {
const captured;
for (const section of sections) {
for (const item of section.data) {
const key = item.keyForList;
if (key && initialSelectedValues.includes(key)) {
captured.add(<key, item>);
}
}
}
setFrozenData(captured);
}
}
There was a problem hiding this comment.
@MelvinBot you didn't get what I mean.
Below is not complete code but get my idea from this:
const [frozenData, setFrozenData] = useState<Map<string, TItem> | null>(null);
if (frozenData === null && canCapture) {
const totalCount = sections.reduce((sum, section) => sum + section.data.length, 0);
if (totalCount < CONST.STANDARD_LIST_ITEM_LIMIT) {
setFrozenData(new Map());
} else {
const captured = new Map();
for (const section of sections) {
for (const item of section.data) {
const key = item.keyForList;
if (key && initialSelectedValues.includes(key)) {
captured.set(key, item);
}
}
}
setFrozenData(captured);
}
}
if (!frozenData || frozenData.size === 0) {
return sections;
}
const frozenSectionData = [...frozenData.values()].map((item) => ({...item, isSelected: false}));
const filteredSections = sections.map((section) => {
const data: TItem[] = [];
for (const item of section.data) {
if (item.keyForList || frozenData.has(item.keyForList)) {
const frozenItem = frozenSectionData.find(({keyForList}) => keyForList === item.keyForList);
if (frozenItem) {
frozenItem.isSelected = true;
}
} else {
data.push(item);
}
}
return {...section, data};
});
return [{data: frozenSectionData, sectionIndex: 0}, ...filteredSections];
|
The "full items vs keys" framing may not match what's in this PR — the current code already stores The disappear-on-unselect bug is real, but the cause is the threshold gate, not the storage shape. When Recents (chats) or Recents+Contacts (participants) is shorter than Your Suggested fixCapture "would-be extras" into a persistent set regardless of the top-pin threshold, separate from the top-pinning behavior: const [frozen, setFrozen] = useState<{topPin: T[]; persisted: T[]} | null>(null);
if (frozen === null && canCapture) {
const visibleKeys = new Set(/* keys from recents (+ contacts) at first ready render */);
const persisted = selectedOptions.filter((opt) => !visibleKeys.has(getKey(opt)));
const topPin = totalVisible >= STANDARD_LIST_ITEM_LIMIT
? selectedOptions.filter((opt) => visibleKeys.has(getKey(opt)))
: [];
setFrozen({topPin, persisted});
}Render If you want O(1) lookups during render, a |
|
Yes — that shape is cleaner than what I sketched. A sections-in/sections-out transform keyed off const sections = useFrozenPreSelection({sections: rawSections, initialSelectedValues, canCapture: areOptionsInitialized});A few notes on the sketch before we lift it out: Implementation notes1. Condition typo (drops every keyed item from the rest of the list): if (item.keyForList || frozenData.has(item.keyForList)) { // ❌ short-circuits true for any keyed itemShould be 2.
The second is more explicit and avoids the "matches by presence" coupling. 3. Perf — replace const frozenItem = frozenSectionData.find(({keyForList}) => keyForList === item.keyForList);With N frozen × M total this is O(N·M) per render. Either index 4. Capture timing. The current participants selector waits one extra render — 5. What stays in the caller. Your sketch correctly leaves out the "extra selected" section (selected items not in any input section — name-only attendees, current user, server-search misses). That's consumer-specific logic and shouldn't move into the util; the util's job is just "pin frozen at the top, dedupe from the rest." Want me to open a follow-up PR with |
|
@MelvinBot review this again: |
|
A few things to fix before this is ready, mostly around how the "frozen" state stays in sync once the user searches. Stale
Smaller things
If you'd like, I'm happy to sketch the corrected hook + a unit test that exercises the "search hides a frozen row" case — let me know. |
@MelvinBot yes, sketch please |
|
Sketch below. First, a quick reconciliation against what's already on
Sketch:
|
|
@MelvinBot I don't think Review again: |
|
Agreed — The bigger behavioral shift in your sketch — storing snapshots ( A few small things to clean up before landing it:
Performance-wise it's the same single O(N) sweep over sections plus an O(K) snapshot rebuild — React Compiler handles the surrounding memoization. |
|
@MelvinBot implement below pseudocode: (shouldRenderPinned is optional) Do not pass |
Switch useFrozenPreSelection to an options object so callers can opt into a snapshot-backed pinned section that survives row-level disappearance — needed for lazy-loaded selectors where a pinned row may not be present in the current sections (e.g. a search term hides it). The hook now stores a Map<key, item> snapshot, refreshes pinned rows from current section data when they reappear, and (when shouldRenderPinned is provided) filters pinned rows through that predicate so the pinned section stays in sync with the active search term. SearchFiltersChatsSelector keeps the existing behavior since it loads the full list — no shouldRenderPinned needed. SearchFiltersParticipantsSelector passes matchesSearchTerm so pinned rows respect the search term during lazy load, and drops the empty-search-term gate on canCapture now that the hook handles it. Co-authored-by: mkhutornyi <mkhutornyi@users.noreply.github.com>
|
Implemented in 0ebd9c7.
AI Tests
|
Explanation of Change
Stop scrolling and jumping when selecting items in the Search advanced filters participants selector (Spend > Filters > From / To / Attendee / Assignee).
Previously, selecting an item caused it to move into a dedicated "selected" section at the top of the list, and the list also auto-scrolled to the top. This was disorienting when selecting multiple participants from a long list.
Changes:
shouldKeepSelectedInAvailableOptions: trueandshouldSeparateNonExistingSelectedOptions: truetouseSearchSelectorso selected items remain inside the Recents / Contacts sections (with theirisSelectedflag) instead of being moved out into a separate "selected" section.selectedNonExistingOptionsfor an optional top section, which keeps selected items that are not present in the result list visible — for example, name-only attendees in the Attendee filter.formatSectionsFromSearchTermcall that previously pulled all selected items out into a top section.currentUserOptionat the top (filtered out of Recents / Contacts) withisSelectedreflecting whether the current user is selected, so the current user's row also stays in place across selections.shouldUpdateFocusedIndexandshouldPreventAutoScrollOnSelecttoSelectionListWithSectionsto keep the user's scroll position when a row is selected.This follows the same pattern applied in #91387 (
RoomInvitePage) and #91250 (DynamicWorkspaceInvitePage).Fixed Issues
$ #61414
PROPOSAL:
Tests
// TODO: The human co-author must fill out the tests you ran before marking this PR as "ready for review"
// Please describe what tests you performed that validates your changed worked.
Offline tests
// TODO: The human co-author must fill out the offline tests you ran before marking this PR as "ready for review".
QA Steps
// TODO: The human co-author must fill out the QA tests you ran before marking this PR as "ready for review".
// Please describe what QA needs to do to validate your changes and what areas do they need to test for regressions.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand 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
AI Tests
prettierlint(on changed file)typecheck-tsgosrc/components/MapView/utils.tsandsrc/libs/migrations/ConvertGpsPointsTo2DArray.ts, unrelated to this PR)react-compiler-compliance-checkjest tests/unit/useSearchSelectorTest.tsxjest tests/unit/hooks/useAdvancedSearchFilters.test.ts