Skip to content

feat: fix scroll behavior on asset lists [PERA-3277]#394

Open
wjbeau wants to merge 5 commits intomainfrom
wjbeau/PERA-3277
Open

feat: fix scroll behavior on asset lists [PERA-3277]#394
wjbeau wants to merge 5 commits intomainfrom
wjbeau/PERA-3277

Conversation

@wjbeau
Copy link
Copy Markdown
Contributor

@wjbeau wjbeau commented Apr 28, 2026

Pull Request Template

Description

This PR fixes 4 issues:

  1. The UX of the search bar on the account home page where it should snap to the top when focused but still be scrollable. This improves the UX of the native app by pinning the search to the top of the screen and remains so when scrolling.
  2. It fixes the scroll behavior when re-sorting the list to scroll back to the top of the screen.
  3. It fixes the opt-out of assets to correctly show a toast and clean up the scroll behavior afterwards
  4. It allows for async opt ins so you can kick off multiple opt ins at once if desired

Related Issues

Closes https://algorandfoundation.atlassian.net/browse/PERA-3277

Checklist

  • Have you tested your changes locally?
  • Have you reviewed the code for any potential issues?
  • Have you documented any necessary changes in the project's documentation?
  • Have you added any necessary tests for your changes?
  • Have you updated any relevant dependencies?

Additional Notes

  • Add any additional notes or comments that may be helpful for reviewers.

@wjbeau wjbeau requested a review from a team April 28, 2026 11:31
Comment on lines +28 to +39
const renderHeaderNode = (
component: React.ComponentType | React.ReactElement | null | undefined,
): React.ReactNode => {
if (component == null) {
return null
}
if (typeof component === 'function') {
const Component = component
return <Component />
}
return component
}
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.

nit: I don't like much this pattern because this is basically returning a React node every time, which means it's a component. But it's not written as a component. And since it's not a component, you cannot use react features which might be interesting sometimes like React.memo, etc.

I'd rather design that as a sub-component instead and use it in the more React idiomatic way.

But there's no issue here since you already mitigated the risks of recalculating the result unnecessarily with the useMemo down the line. So I'm really just expressing an personal opinion.

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.

I think this is required because of the way the type for this prop is defined on LegendList - it's not really up to us.


type UseSearchableListParams<T> = {
forwardedRef: React.ForwardedRef<PWFlatListRef>
data: readonly T[] | null | undefined
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.

maybe?

Suggested change
data: readonly T[] | null | undefined
data: readonly Maybe<T[]>

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.

We have to leave it as is on this occasion because it must be readonly to comply with the PWFlatList prop requirements (inherited from LegendList) and you can't make a custom type readonly

Comment thread apps/mobile/src/components/SearchableList/useSearchableList.ts Outdated
type UseSearchableListParams<T> = {
forwardedRef: React.ForwardedRef<PWFlatListRef>
data: readonly T[] | null | undefined
keyExtractor?: (item: T, index: number) => string
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.

What do you think about using the Maybe type in such cases as well just for the sake of consistency?

Suggested change
keyExtractor?: (item: T, index: number) => string
keyExtractor?: Maybe<(item: T, index: number) => string>

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.

I think the ? optionality already covers what we need, no? Do we need to double that up with a type wrapper too?

// Defer the scroll so it runs after FlashList re-renders with the
// shrunken dataset; scrolling synchronously while cells are being
// recycled produces a jittery animation.
requestAnimationFrame(() => {
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.

I think this serves the exact same purpose as the deferToNextCycle function we have in the shared package, right? Should we do something about it?

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.

They are subtly different things I think. setTimeout(0) (which is what deferToNextCycle) will add the funciton call to the end of the event loop, so it will do all other async/await stuff already queued up first, then this thing.

requestAnimationFrame will defer to the next render cycle (i.e. run on the next screen refresh). Because we're trying to prevent animation jitter here, I think the animation hook is more appropriate.

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.

2 participants