Skip to content

[Design Polish] Center the search router on wide viewports - #100831

Merged
carlosmiceli merged 17 commits into
mainfrom
claude-centerSearchRouter
Sep 14, 2026
Merged

carlosmiceli merged 17 commits into
mainfrom
claude-centerSearchRouter

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

Centers the search router popover on wide (web/desktop) viewports instead of anchoring it to the top-right corner, so it reads as a modern command palette rather than a dropdown hanging off the search icon.

The prototype in #100187 established this treatment; this PR ports only the search-router slice of it, with no other changes from that PR.

Three deltas, all scoped to !shouldUseNarrowLayout so narrow/mobile layout is untouched:

  • Location. popoverAnchorPosition becomes {left: 0, right: 0, top: 40} on wide layout. The POPOVER modal style already applies position: absolute plus alignItems: 'center', so spanning the anchor box full width is what centers the card; the 40px top offset lifts it off the window edge. Narrow layout keeps {right: 6, top: 6}.
  • Height. The router replaces mh85vh (85% of viewport height, so the popover grew with the window) with a fixed maxHeight: 520 plus overflowHidden, so the results list scrolls inside a stable card instead of the card stretching. mh85vh was only used here.
  • Shadow. A new searchRouterPopoverShadow style layers three wider, lighter brand-tinted shadows behind the existing theme.shadow, passed as innerContainerStyle, for stronger separation from the page now that the card floats over content rather than tucking into a corner.

The popover also widens from 512px to 600px, matching the mockup in the issue and the prototype — the measured card in the design is 600px. Say the word if you'd rather keep 512px; it's a one-line revert of searchRouterPopoverWidth.

The input's wide-layout margins go from mv2/mh2 (8px) to mv4/mh4 (16px) so the inset stays proportional at the wider size, as in the prototype.

Fixed Issues

$ #100823
PROPOSAL: #100823 (comment)

Tests

  1. At 1000px+ wide, open search via the icon or Cmd/Ctrl+K.
  2. Verify the popover is centred, sits 40px from the top, has a visible drop shadow, and the input is inset equally left and right.
  3. Type type:expense, verify typed text and the clear button run the full width of the input.
  4. Verify suggestions scroll inside the card and the scrollbar sits at the card edge with rows inset from it.
  5. Shrink the window to under ~600px tall, verify the card shrinks with it instead of clipping.
  6. Switch to dark theme, verify the shadow is still visible.
  7. Open the Help side panel, reopen search, verify it is centred in the remaining area.
  8. Dismiss with Escape and by clicking outside, verify no overlay remains.
  9. On mobile and below the narrow breakpoint, verify search still opens full screen as before.
  • Verify that no errors appear in the JS console

Offline tests

QA Steps

Same as Tests

  • Verify that no errors appear in the JS console

AI Tests

Run locally on this branch:

Check Result
npm run typecheck ✅ passed
npm run lint-changed ✅ passed (pre-existing grandfathered no-unsafe-type-assertion at SearchRouter.tsx:246, untouched by this PR)
npm run react-compiler-compliance-check check <changed files> ✅ passed
npm run gh-actions-unused-styles ✅ passed — no unused styles
npm run spell-changed ✅ passed on all 4 changed files
npm test — SearchRouterPageTest, BaseModalTest, SearchAutocompleteListTest ✅ 25 tests passed

Could not run: npm run react-compiler-compliance-check check-changed (its origin/<base> resolution fails in this environment — ran the equivalent check against the changed files instead), and npm run storybook -- --smoke-test --ci. The full npm test suite was not run; CI covers it.

Browser verification of the rendered result is in the MacOS: Chrome / Safari section below.

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
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

Verified on dev NewDot web in a 1600x1000 viewport. Measured from the screenshots: the card spans x=500–1100, so it is exactly 600px wide and centered on the viewport midpoint, with its top edge 40px below the window top.

Search router popover centered below top nav on Home

Corners at 4x — rounded and unclipped, with the layered shadow visible past the card border:

Zoomed top-left corner showing rounded corner and drop shadow Zoomed top-right corner showing rounded corner and drop shadow

Typing type:expense — suggestions render inside the card, which stays under the 520px cap, and the focused input sits ~16px in from each card edge (input border at x=516/1083 vs card at x=500/1100):

Search router popover showing type:expense suggestions

Dismisses cleanly, no leftover overlay:

Home screen after dismissing the search router popover

The test account was freshly created, so the popover has little recent-search content and renders short; the geometry above is what this PR changes.

Co-authored-by: Shawn Borton <shawnborton@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team September 10, 2026 11:43
@melvin-bot melvin-bot Bot added Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web labels Sep 10, 2026
@melvin-bot
melvin-bot Bot requested a review from shawnborton September 10, 2026 11:43
@MelvinBot

MelvinBot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Nothing to fix here — every code check on this PR passes. The one failure, Check independent approval, is a process gate, not a code problem, so there's no commit I can push to clear it.

It fails because the PR has zero approvals and needs one from a reviewer who isn't a commit author:

Insufficient independent peer review
  commitAuthors: [ 'MelvinBot', 'shawnborton' ]
  approvers: []
  independentApprovers: []
  required: 1

Since you're listed as a commit author, your own approval won't count. Someone else needs to review and approve it.

The two perf-tests jobs are still pending, not failed.

Full check status

All 30 other checks pass or skip, including typecheck, ESLint check, Oxfmt check, React Compiler Compliance, spellcheck, Check for unused styles, Storybook tests, Bun tests, all 8 test jobs, and all 4 native builds.

Failing step: Verify peer review.


view run · view recording

@codecov

codecov Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/components/Modal/BaseModal.tsx 96.87% <100.00%> (+0.02%) ⬆️
src/components/Search/SearchAutocompleteInput.tsx 95.83% <100.00%> (-0.06%) ⬇️
src/components/Search/SearchAutocompleteList.tsx 88.37% <ø> (-0.06%) ⬇️
...rc/components/Search/SearchRouter/SearchRouter.tsx 72.25% <100.00%> (+0.48%) ⬆️
...earch/SearchRouter/getSearchRouterPopoverLayout.ts 100.00% <100.00%> (ø)
src/styles/theme/themes/dark.ts 100.00% <ø> (ø)
src/styles/theme/themes/light.ts 100.00% <ø> (ø)
src/styles/variables.ts 100.00% <ø> (ø)
...ts/Search/SearchRouter/SearchRouterModal/index.tsx 0.00% <0.00%> (ø)
... and 188 files with indirect coverage changes

@github-actions

Copy link
Copy Markdown
Contributor

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

@github-actions

This comment has been minimized.

@shawnborton

shawnborton commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

@MelvinBot can you make the search bar appear all the way on the right edge? This might mean that you need to add the right padding/margin to a different view and remove it from the view that is wrapping the search bar.

CleanShot 2026-09-10 at 13 55 30@2x

@MelvinBot

Copy link
Copy Markdown
Contributor Author

⏱️ Ran out of time before I could finish. This usually means the task was too complex or I got stuck exploring. Please try again with a more specific request or post in #melvinbot-ai.

Run details

@grgia
grgia self-requested a review September 10, 2026 12:37
Co-authored-by: shawnborton <shawnborton@users.noreply.github.com>
Co-authored-by: Shawn Borton <shawnborton@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done — pushed in 25b5fa4. On wide layout the view wrapping the search bar now only carries a left margin, so the bar runs to the popover's right edge: SearchRouter.tsx:513. The results list already owns its own right gutter (its own horizontal padding plus the space the scrollbar sits in), so nothing needed the margin re-added to it.

Geometry inside the 600px card: the bar was 16px in from both edges, and is now 16px in on the left and flush on the right.

Two things to flag before you look at the build.

I couldn't screenshot it. The web test session went blank part way through the run, so this change is verified by code and by typecheck, ESLint, React Compiler, spellcheck, unused-styles and the SearchRouterPageTest suite — but not by eye. The adhoc build will refresh with this commit.

The bar has a visible border, so running it flush means its right border now sits exactly on the card edge. If that reads as clipped rather than intentional, say the word — reverting to a symmetric inset is a one-line change, and so is giving it a smaller right inset (8px) that lines up with the left edge of the rows below.

Separately, one thing I found that may actually be what you're seeing. The text input element inside the bar is capped at 375px, while the bar itself is now 568px wide: SearchAutocompleteInput.tsx:196. That 375px is the old popover width, so on the wide layout your typed text and the clear "X" stop roughly 190px short of the bar's right edge even though the border box spans the full width. It predates this PR but the wider card makes it more obvious. Want me to make the input full width here too?

@MelvinBot

MelvinBot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Re-checked after 25b5fa4. Still nothing to fix in code — every code check passes on the new commit, and the only failure is the same process gate as before:

Insufficient independent peer review
  commitAuthors: [ 'MelvinBot', 'shawnborton' ]
  approvers: []
  independentApprovers: []
  required: 1

Check independent approval needs one approval from someone who isn't a commit author. You're a commit author, so your approval won't satisfy it. This isn't something a push can clear — it needs another reviewer.

branch-perf-tests is still running, not failed.

Full check status on 25b5fa4

Passing: typecheck, ESLint check, Oxfmt check, React Compiler Compliance, spellcheck, Check for unused styles, Compare knip issues against main, Storybook tests, Bun tests, all 8 test jobs, all 4 native builds, CLA, verifySignedCommits, baseline-perf-tests.

Failing step: Verify peer review.


view run · view recording

@grgia

grgia commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Findings:

  • Shadow is hard-coded rgba in a one-off style. The extra layers are 0.03 to 0.07 alpha so they vanish on dark theme, and floating RHP needs the same shadow. Should be a theme token.
  • The autocomplete input is capped at 375px (Melvin found this), so at 600 wide typed text stops ~190px short. Fix it here.
  • Fixed 520 max height clips on viewports under ~600px tall; the old 85vh behaviour should remain as a floor.
  • Three comment paragraphs.

@MelvinBot three changes: (1) move the layered shadow into the theme as a token in light.ts/dark.ts instead of hardcoded rgba in searchRouterPopoverShadow, the floating RHP will reuse it and the current layers disappear on dark; (2) fix the 375px cap in SearchAutocompleteInput you found so the input fills the 600px card; (3) keep mh85vh alongside the 520 cap so short viewports don't clip. Drop the three comment blocks while there.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

All four are straightforward, and one of them has a trap in it. Here is the exact shape of each — I can't push them, see the note at the bottom.

1. Shadow as a theme token. Add an elevatedShadow key next to shadow in types.ts:61, light.ts:53 and dark.ts:53 holding the whole layered value, then reduce styles/index.ts:1286-1288 to {boxShadow: theme.elevatedShadow}. Worth renaming it off searchRouterPopoverShadow at the same time, since floating RHP will share it.

For the dark values: the existing shadow token already runs 4x the light alpha (0.24 against 0.06), so applying that same 4x to the new layers gives 0.288 / 0.144 / 0.108 — visible rather than vanishing.

2. The 375px cap. SearchAutocompleteInput.tsx:196 is the only place isFullWidth is read, and SearchRouter.tsx:516 is its only caller, so the 375px branch is reachable only from the router on wide layout. Make line 196 styles.w100 unconditionally and delete the prop that is then dead: the type at line 44, the destructure at line 80, and the caller. variables.popoverWidth itself stays — styles/index.ts:4470 still uses it.

3. The 85vh floor — this one needs care. Putting styles.mh85vh next to {maxHeight: 520} in the same style array does not stack. React Native flattens the array and the last maxHeight wins, so whichever you list second silently erases the other. It has to be one computed value instead: take windowHeight from useWindowDimensions and set Math.min(variables.searchRouterPopoverMaxHeight, windowHeight * 0.85) at SearchRouter.tsx:497. CSS min(520px, 85vh) would also express it, but only on web, and the wide layout renders on native tablets too.

4. The comments. I count five blocks on this branch rather than three — SearchRouterModal:64 and :72, styles/index.ts:1284-1285, variables.ts:248, and the block I added at SearchRouter.tsx:508-512. I'd drop all five unless you want some kept.

I can only make code changes when directed by the PR author, an assignee, or an existing co-author. To have me implement this, the PR assignee or author should reply with a request tagging me.

@shawnborton

Copy link
Copy Markdown
Contributor

@grgia I will ask Melvin to make your changes but then I think I need to take this one to Claude to better implement the search bar stuff I was trying to do

@shawnborton

Copy link
Copy Markdown
Contributor

@MelvinBot implement the changes here please

@grgia

grgia commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

All good @shawnborton. I'll wait for your call to run some reviews

…t, clamp the popover height to the viewport

Co-authored-by: shawnborton <shawnborton@users.noreply.github.com>
Co-authored-by: Shawn Borton <shawnborton@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

All three done in 25b1865, plus the comment cleanup.

1. Shadow is a theme token. New elevatedShadow key in light.ts:54 and dark.ts:54, typed alongside shadow in types.ts. searchRouterPopoverShadow is now just boxShadow: theme.elevatedShadow, so the floating RHP can pick up the same token. For dark I scaled each extra layer's alpha by 4x (0.072/0.036/0.027 → 0.288/0.144/0.108), matching the ratio dark's base shadow already has to light's (0.24 vs 0.06). Both contrast themes inherit it by spread.

2. 375px cap is gone. SearchRouter was the only consumer of isFullWidth, so I dropped the prop rather than flipping it, and the input is now always styles.w100. Also removed the prop from SearchRouter.perf-test.tsx.

3. Height clamps to the viewport. SearchRouter.tsx:496 is now Math.min(520, windowHeight * 0.85). I did not literally keep styles.mh85vh in the array, because two maxHeight entries don't compose — the last one wins, so the 520 cap would still clip a 600px-tall window. Math.min is the min(520px, 85vh) you asked for. mh85vh stays defined in sizing.ts and is now unused, same as its mh45vh/mh65vh siblings.

Comments. I dropped five, not three — the two in SearchRouterModal, the one above searchRouterPopoverShadow, the one on searchRouterPopoverTopOffset, and the block I added over the input wrapper. They were all the same kind of restate-the-code paragraph. Shout if any of those was worth keeping.

Checks. typecheck, ESLint on all nine changed files (counts match the seatbelt exactly — no new violations), React Compiler compliance, unused-styles, and SearchRouterPageTest / SearchAutocompleteListTest / ThemeUtilsTest all pass.

Still no browser screenshots. The web test session went blank again — the page dropped to zero rendered nodes while idle, on a healthy dev server, for the second run in a row. So the dark-theme shadow and the full-width input are verified by code, not by eye. The adhoc build will rebuild on this commit if you want to look.

@MelvinBot

MelvinBot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Still nothing for me to fix. The full suite re-ran on the latest commits and 34 checks now pass, including all 8 test jobs, both perf-test jobs, typecheck, ESLint check, Oxfmt check, React Compiler Compliance, spellcheck, Check for unused styles, Storybook tests, Bun tests, and all 4 native builds.

The single remaining failure is Check independent approval, which is an approval gate rather than a code check — no commit can clear it:

Insufficient independent peer review
  commitAuthors: [ 'MelvinBot', 'shawnborton' ]
  approvers: []
  independentApprovers: []
  required: 1

You count as a commit author, so your approval won't satisfy it. The PR needs one approval from someone who isn't you or me. reviewDecision is REVIEW_REQUIRED and mergeStateStatus is BLOCKED, which matches.

Failing step: Verify peer review.


view run · view recording

@shawnborton

Copy link
Copy Markdown
Contributor

Pushed my changes @grgia feel free to give this another review now

…uter

Co-authored-by: Shawn Borton <shawnborton@users.noreply.github.com>
@MelvinBot

MelvinBot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Fixed and pushed — the branch was one commit behind main, and that commit was the fix.

Two test jobs were failing on stale middleware-count assertions:

● Middleware registration › registers all 15 middlewares with no duplicates
  Expected length: 15
  Received length: 16

Nothing in this PR touches middleware. main had already added a 16th middleware and updated both test files to expect 16 (#100858), but this branch still carried the old 15. Merging main in resolved it — no conflicts, and the PR's own diff is untouched (still the same 10 files).

All 8 test jobs now pass, along with typecheck, ESLint check, Oxfmt check, React Compiler Compliance, spellcheck, Check for unused styles, Storybook tests, Bun tests, the perf tests, and all 4 native builds.

The one remaining failure, Check independent approval, is a process gate I can't clear with code. It needs one approval from a reviewer who isn't a commit author — since you're a commit author, your own approval won't count, so someone else needs to review it.

Verification before pushing

Ran locally on the merged branch:

Check Result
MiddlewareRegistrationTest + MiddlewareEntryPointTest ✅ 4 tests passed
npm run typecheck ✅ passed
SearchRouterPageTest, BaseModalTest, tests/unit/Search ✅ 1573 tests passed across 50 suites

Merge commit: 856910c. The checklist check is now skipping rather than failing.


view run · no recording available

@github-actions

Copy link
Copy Markdown
Contributor

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

@github-actions

This comment has been minimized.

@shawnborton

Copy link
Copy Markdown
Contributor

Testing well on my end, let's do a final review and merge @huult @grgia

grgia
grgia previously approved these changes Sep 14, 2026

@grgia grgia 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.

Nothing blocking code wise, only comment is we could add an automated test:

Add a small unit test for getSearchRouterPopoverLayout at e.g. 1000, 700 and 500 px heights, asserting topOffset and maxHeight (100/520, 90/520, 16/468).

@MelvinBot wdyt

Co-authored-by: Shawn Borton <shawnborton@users.noreply.github.com>
@MelvinBot

MelvinBot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Good call — added in 48dc142: tests/unit/getSearchRouterPopoverLayoutTest.ts.

Your three expected pairs are exactly right — I checked each against the formula before writing them down:

Viewport evenGap topOffset maxHeight
1000 240 min(100, 240) = 100 min(520, 800) = 520
700 90 min(100, 90) = 90 min(520, 520) = 520
500 −10 max(16, −10) = 16 min(520, 468) = 468

I added two things on top of your three cases, both cheap:

  • The two breakpoints, 720px and 552px — the exact heights where topOffset stops being 100 and where it bottoms out at 16. Your 1000/700/500 straddle them; these land on them, which is where an off-by-one would actually hide.
  • The equal-gap invariant the helper's doc comment promises — windowHeight - topOffset - maxHeight === topOffset — asserted across 300/500/552/700/720.

To check the test isn't vacuous I mutated the helper (dropped the * 2 in the maxHeight clamp) and confirmed 3 of the 10 cases go red, then reverted.

AI tests run locally on 48dc142
Check Result
npm run typecheck ✅ passed
eslint on the new file ✅ passed
npm run spell-changed on the new file ✅ passed
npm test -- --silent — getSearchRouterPopoverLayoutTest, SearchRouterPageTest, SearchAutocompleteListTest ✅ 29 tests passed

npm run prettier isn't a script in this repo and prettier isn't a dependency — formatting is covered by ESLint, which passed. I ran the three affected suites rather than the whole Jest suite; CI shards cover the rest.

One thing I deliberately left untested: below a 32px viewport height maxHeight goes negative, since the min offset is 16 on each side and nothing floors the result at 0. Not reachable in a real browser, so I didn't want to lock the current behaviour in — flagging it in case you'd rather it clamped.


view run · no recording available

*/
function getSearchRouterPopoverLayout(windowHeight: number): SearchRouterPopoverLayout {
const evenGap = (windowHeight - variables.searchRouterPopoverMaxHeight) / 2;
const topOffset = Math.min(variables.searchRouterPopoverTopOffset, Math.max(variables.searchRouterPopoverMinTopOffset, evenGap));

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.

@MelvinBot Can we use clamp from NumberUtils for this logic?

@MelvinBot MelvinBot Sep 14, 2026 •

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.

Yes — good catch, applied in ad0b58f.

clamp(value, min, max) is literally Math.min(Math.max(value, min), max) (NumberUtils.ts:68-70), so this is an exact behavioural equivalent — just easier to read:

const topOffset = clamp(evenGap, variables.searchRouterPopoverMinTopOffset, variables.searchRouterPopoverTopOffset);

getSearchRouterPopoverLayout.ts:21. NumberUtils has no imports of its own, so there's no cycle risk pulling it into this helper.

I left maxHeight on line 25 as a plain Math.min — it only has an upper bound, so clamp would need a fake lower bound to fit.

AI tests run locally
Check Result
npm run typecheck ✅ passed
eslint on the changed file ✅ passed
npm run react-compiler-compliance-check check <file> ✅ passed
npm test — getSearchRouterPopoverLayoutTest ✅ 10 tests passed
npm test — BaseModalTest ✅ 7 tests passed

The existing 10 unit tests for this helper pass unchanged, which is the equivalence check.

Couldn't run: npm run spell-changed (its git merge-base against origin/main doesn't resolve in this environment) and npm run prettier (no such script in this repo, and the only resolvable prettier config here isn't App's). The change adds no new words beyond clamp and NumberUtils, both already in the codebase.


view run · no recording available

Co-authored-by: huult <huult@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

⚠️ MelvinBot CI Fix Loop Detected

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.

@huult

huult commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

@shawnborton Could you complete the author checklist?

@shawnborton

Copy link
Copy Markdown
Contributor

Done!

@huult huult 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.

Image

@melvin-bot
melvin-bot Bot requested a review from grgia September 14, 2026 09:04
@shawnborton

Copy link
Copy Markdown
Contributor

All you @carlosmiceli

@grgia

grgia commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Not sure why Melv assigned a second engineer review on these tbh

@carlosmiceli
carlosmiceli merged commit 1b08559 into main Sep 14, 2026
46 of 74 checks passed
@carlosmiceli
carlosmiceli deleted the claude-centerSearchRouter branch September 14, 2026 15:36
@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.

@github-actions

Copy link
Copy Markdown
Contributor

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

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/carlosmiceli in version: 9.4.78-0 🚀

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

@MelvinBot

MelvinBot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 No help site changes are required. I did not create a docs PR.

This PR only changes where the search popover sits and how big it is on wide viewports — centered instead of anchored top-right, a fixed max height, a layered shadow, and a dimmed backdrop. It adds no feature, renames nothing, and changes no user-facing copy, tab name, setting label, or button.

The help site never documents the popover's position or size, so nothing in docs/articles became inaccurate.

What I checked

Diff surface. All 12 changed files are layout, styles, variables, or tests (BaseModal.tsx, Modal/types.ts, the four Search/ files, getSearchRouterPopoverLayout.ts, light.ts/dark.ts, variables.ts, two test files). No language file, no CONST label, no new screen.

Positional references in the docs. Two New Expensify articles describe where the Search icon lives, not where the popover opens:

The icon stays in the top-right — this PR touches no top-bar file — so both lines remain correct. Line 24 of the operators article says operators go in "the search bar that opens from the Search icon," which is position-neutral and still accurate.

Other search articles. Use-Search-Shortcuts.md, Navigating-New-Expensify.md, and Search-and-Download-Expenses.md describe behavior and syntax only. None mentions the popover's placement, width, height, or shadow.

Screenshots. No article embeds a screenshot of the search popover, so none went stale. The two search-named images in docs/assets/images (ExpensifyHelp-SearchFormat.png, search-download.png) are not referenced by any current article.

@shawnborton — since no docs update is needed, there is no help site PR to review. If you'd rather the articles call out the new centered command-palette treatment anyway, reply with @MelvinBot and the wording you want and I'll open the draft PR.


view run · no recording available

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/luacmartins in version: 9.4.78-3 🚀

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

Bundle Size Analysis (Sentry):

Comment on lines 30 to 40
useEffect(() => {
if (!isSearchRouterDisplayed || shouldUseNarrowLayout) {
return;
}

const subscription = Dimensions.addEventListener('change', () => closeSearchRouter());

return () => {
subscription.remove();
};
}, [isSearchRouterDisplayed, closeSearchRouter, shouldUseNarrowLayout]);

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.

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

Labels

Design Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants