Skip to content

[CFI] Open the Category list in a container anchored to its row - #102665

Merged
puneetlath merged 14 commits into
Expensify:mainfrom
DylanDylann:dylann/101573-category-anchored-dropdown
Oct 5, 2026
Merged

puneetlath merged 14 commits into
Expensify:mainfrom
DylanDylann:dylann/101573-category-anchored-dropdown

Conversation

@DylanDylann

@DylanDylann DylanDylann commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

Fixed Issues

$ #101573
PROPOSAL:

Tests

  1. Override anchoredFieldDropdowns beta to true
  2. Sign in and open a workspace chat on a workspace that has categories enabled with at least one enabled category.
  3. Start a manual expense, enter an amount and continue to the confirmation form.
  4. Click the Category row.
  5. Verify a pop-over opens directly below the row, is the same width as the row, and is aligned to the row's left edge.
  6. Verify the list inside it is the same list the full page showed: same options, same ordering, same recently-used section, and the search input appears under the same condition (a workspace with fewer than 8 categories shows no search input).
  7. Pick a category. Verify the pop-over closes and the row shows the picked category.
  8. Click the row again and pick the same category. Verify it is cleared, matching the full page's behavior.
  9. Scroll the form so the Category row sits near the bottom of the window, then click it. Verify the pop-over opens above the row instead, and is not clipped by the window.
  10. With the pop-over open, verify the form behind it does not scroll.
  • Verify that no errors appear in the JS console

Offline tests

Same above

QA Steps

Same above

  • 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. No new copy was added in this PR.
  • 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: no assets were added or modified.
  • If the PR modifies code that runs when editing or sending messages: not applicable.
  • 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 — CategoryPickerModal is shared with the Search table's inline category edit and needs re-verifying there.
  • If the PR modifies a component related to any of the existing Storybook stories: not applicable.
  • 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 — the full-page category route is unchanged and still reachable directly.
  • 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.
  • If the PR adds or modifies the UI:
    • I asked an AI agent to review the changes for accessibility issues and addressed its findings.
    • I tested with a screen reader (VoiceOver on macOS) and verified all new/changed elements are reachable with a logical focus order.
    • I verified all new/changed elements have meaningful accessible names and roles.
    • I verified state changes are announced (e.g. checked/unchecked, expanded/collapsed, selected).
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow. tests/unit/components/ExpenseFieldDropdown.test.tsx covers lazy mounting, opening in place vs. navigating, the row-width/below-the-row geometry, the flip above the row, dismissing by pressing the row again, and the full-page fallback.
  • 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

iOS: Native
Screen.Recording.2026-10-01.at.15.57.58.mov
MacOS: Chrome / Safari
Screen.Recording.2026-10-01.at.15.42.30.mov

🤖 Generated with Claude Code

The create expense form renders Category as a bordered field with a down
caret, but the caret was cosmetic: tapping the row pushed a full-page
selector. This makes the caret real for Category.

- Adds ExpenseFieldDropdown, which pairs an ExpenseFieldRow with a
  container anchored to it. On a wide layout the container matches the
  row's width and opens directly below the row, flipping above it when
  there isn't room below, and is capped so it is never clipped. On a
  narrow layout PopoverWithMeasuredContent docks it to the bottom, so the
  bottom sheet is the existing pattern with no new component.
- Reuses CategoryPickerModal, which already wraps CategoryPicker for the
  Search table's inline edit, rather than building a second list. It now
  takes the pop-over's width and height so a field row can hand it its
  own, and spans the screen when it renders as a bottom sheet.
- Extracts the category write out of DynamicIOURequestStepCategory into
  useUpdateTransactionCategory, so a category picked in the dropdown
  lands exactly where the same pick on the full page lands: the split
  draft, the saved expense, or the money request draft.
- Keeps the full-page selector for every case the container can't answer
  on its own: no workspace picked yet, an upgrade in the way, no list
  loaded, or a saved expense being edited from this form.

The list is mounted only after the row is first opened, so a field nobody
touches costs the form nothing.

Fields other than Category, and the expense detail and bulk edit
surfaces, are unchanged and follow separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 30, 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/CONST/index.ts 95.83% <ø> (ø)
src/components/CategoryPicker/index.tsx 97.61% <100.00%> (+0.39%) ⬆️
src/components/MenuItem/layout/MenuItemRoot.tsx 96.96% <100.00%> (ø)
...ConfirmationList/sections/ExpenseFieldDropdown.tsx 100.00% <100.00%> (ø)
...questConfirmationList/sections/ExpenseFieldRow.tsx 100.00% <100.00%> (ø)
src/hooks/useUpdateTransactionCategory.ts 100.00% <100.00%> (ø)
src/libs/getSelectionListPopoverContentHeight.ts 100.00% <100.00%> (ø)
src/styles/index.ts 64.73% <100.00%> (ø)
src/styles/variables.ts 100.00% <ø> (ø)
...RequestConfirmationList/sections/CategoryField.tsx 91.07% <91.66%> (-0.04%) ⬇️
... and 3 more
... and 17 files with indirect coverage changes

DylanDylann and others added 6 commits September 30, 2026 21:45
The five list fields on the expense form land one at a time. A form where
Category opens its list in place while the four rows under it still push a
full page reads as broken, so the whole set stays dark until it is complete.

canUseAnchoredFieldDropdowns follows the canUseTrackFlows / canUsePrivateNotes
pattern already in Permissions: a plain client gate, no beta name and no Auth
change, since nothing about this needs the server. It returns a widened boolean
rather than a literal so the gated branches are not statically dead, which is
what keeps the container reachable to test.

It is on everywhere but production, so design can judge the container from an
adhoc build while nobody else sees a half-converted form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Knip flagged it as a new unused exported type. Only the render props are
consumed outside the module, by CategoryFieldDropdown and the test; the props
type is a leftover from an earlier draft of the test that reached for it
through React.ComponentProps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ight

Two things made the container taller than it had any need to be, which showed
up as it running past the RHP the expense form sits in.

The container asked for a minimum height even when the row left it less room
than that. Math.max(availableHeight, MIN_CONTAINER_HEIGHT) turned a 40px gap
into a 180px request, and the popover's overflow shift then dragged it back
over the row. The space the row leaves is a ceiling, not a target, so the
floor is gone.

The list itself was then stretched to fill whatever height came out of that:
CategoryPickerModal draws its content at a fixed height, so a three-category
workspace got the full 416px with most of it empty. It now estimates the
height its list needs from the option count, the same way
getSelectionListPopoverHeight estimates the Spend filters' popovers, and
treats the height it was handed as the cap. Opt-in via shouldFitContentHeight
so the Search table's inline edit keeps the size it has today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing about this feature needs the environment, so the gate does not need to
read it. A plain false matches canUseTrackFlows and canUsePrivateNotes exactly,
and the : boolean return type still widens it at the call sites, so the gated
branches stay reachable to compile and test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two changes to how the container picks its side.

It now flips above the row as soon as below cannot hold the whole container,
rather than waiting until below is nearly unusable. Below stays the default
while it fits, so the common case is unchanged, but a row low down in a panel
now opens upwards into the room it has instead of downwards into a sliver.

Opening above also stops short of the page header. The header holds the back
button, and the container was free to run over it, covering the way out of the
page it belongs to. The space above the row is now measured from the bottom of
the header rather than from the top of the window, using the same
useContentHeaderHeight the header itself is drawn with, plus the top safe area
inset.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PopoverWithMeasuredContent can move itself to the other side of its anchor when
it decides it overflows, and it does so by shifting a whole popover height.
CategoryPickerModal had that switched on, so the container picked a side twice:
once here from the row's measurements, then again inside the popover. The
second decision had no idea the first had been made and landed the list on the
row. The caller now turns it off, since it has already chosen the side.

Two smaller holes fed the same symptom. The container's border sits outside the
height it is given, so the height is now taken from the available space less
that border. And a side with no usable room left the container with a height of
zero or less, which reads as "no static dimensions" and makes the popover
measure its own content instead, ignoring the ceiling entirely and then
clamping itself back over the row to fit the window. A row with too little room
on either side now opens its full-page selector instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@DylanDylann
DylanDylann marked this pull request as ready for review September 30, 2026 17:19
@DylanDylann
DylanDylann requested review from a team as code owners September 30, 2026 17:19
@melvin-bot
melvin-bot Bot requested review from JmillsExpensify, ikevin127 and thelullabyy and removed request for a team September 30, 2026 17:19
@melvin-bot

melvin-bot Bot commented Sep 30, 2026

Copy link
Copy Markdown

@ikevin127 @thelullabyy One of you needs to 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 September 30, 2026 17:19
@DylanDylann DylanDylann changed the title [CFI] Open the Category list in a container anchored to its row [NO QA] [CFI] Open the Category list in a container anchored to its row Sep 30, 2026
@trjExpensify
trjExpensify removed the request for review from thelullabyy September 30, 2026 20:08
@trjExpensify

Copy link
Copy Markdown
Contributor

Looks like you have some ESLint failures and the OP needs some vids, @DylanDylann. @ikevin127 if you want to begin a review in the meantime, please do. 👍

@ikevin127

ikevin127 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible 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 checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (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 verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • 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 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.
  • If the PR adds or modifies the UI:
    • I asked an AI agent to review the changes for accessibility issues and addressed its findings.
    • I tested with a screen reader (VoiceOver on macOS) and verified all new/changed elements are reachable with a logical focus order.
    • I verified all new/changed elements have meaningful accessible names and roles.
    • I verified state changes are announced (e.g. checked/unchecked, expanded/collapsed, selected).
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this 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.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

iOS: Native
Screen.Recording.iPhone.17.Pro.Max.09-30-2026.at.20.22.21.mp4
MacOS: Chrome / Safari
Screen.Recording.2026-09-30.at.20.17.52.mov

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

Tests well generally, but I found 6 actionable issues in this PR: 2 🟠, 3 🟡 and 1 🟢. ESLint is the only failing CI check, because usePermissions is imported but never used in CategoryField.tsx.

🔄 I left out what the bots and the author already raised: the em-dash comments, the duplicated SEARCH_INPUT_HEIGHT / CONTENT_VERTICAL_PADDING constants, and the author's plan to flip the flag before merge.

@OSBotify

OSBotify commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/puneetlath in version: 9.5.3-0 🚀

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

@MelvinBot

Copy link
Copy Markdown
Contributor

No help site update needed. The new anchored Category list only appears behind the new anchoredFieldDropdowns beta, and the shared category picker keeps its existing defaults for everyone else, so no help article sentence becomes incorrect.


view run

@mitarachim

Copy link
Copy Markdown

Deploy Blocker #103124 was identified to be related to this PR.

@mitarachim

Copy link
Copy Markdown

Deploy Blocker #103125 was identified to be related to this PR.

@mitarachim

mitarachim commented Oct 6, 2026 •

Copy link
Copy Markdown

Deploy Blocker #103129 was identified to be related to this PR.

@mitarachim

Copy link
Copy Markdown

Deploy Blocker #103132 was identified to be related to this PR.

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #103131 was identified to be related to this PR.

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #103133 was identified to be related to this PR.

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #103136 was identified to be related to this PR.

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #103137 was identified to be related to this PR.

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #103140 was identified to be related to this PR.

@nlemma

nlemma commented Oct 6, 2026

Copy link
Copy Markdown

Deploy Blocker #103171 was identified to be related to this PR.

@OSBotify

OSBotify commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/puneetlath in version: 9.5.3-4 🚀

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.