Skip to content

Fade transitions between pages #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/widget-react/src/components/form/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ export function useAutoFocus<T extends HTMLInputElement>() {

useEffect(() => {
if (isSmall) return
ref.current?.focus()
// wait 250ms to allow transition to complete
const timeout = setTimeout(() => ref.current?.focus(), 250)
return () => clearTimeout(timeout)
}, [isSmall])

return ref
Expand Down
6 changes: 5 additions & 1 deletion packages/widget-react/src/lib/router/MemoryRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@ interface MemoryRouterProps {

const MemoryRouter = ({ children, initialEntry }: PropsWithChildren<MemoryRouterProps>) => {
const [history, setHistory] = useState<HistoryEntry[]>([initialEntry ?? { path: "/" }])
// we cannot use history to track prevLocation since navigate(-1) will remove the current entry
const [previousLocation, setPreviousLocation] = useState<HistoryEntry | null>(null)
const location = history[history.length - 1]

const navigate = useCallback((to: string | number, state?: object) => {
setHistory((prev) => {
setPreviousLocation(prev[prev.length - 1])

if (typeof to === "string") {
return [...prev, { path: to, state }]
}
Expand Down Expand Up @@ -45,7 +49,7 @@ const MemoryRouter = ({ children, initialEntry }: PropsWithChildren<MemoryRouter
}, [])

return (
<RouterContext.Provider value={{ location, history, navigate, reset }}>
<RouterContext.Provider value={{ location, previousLocation, history, navigate, reset }}>
{children}
</RouterContext.Provider>
)
Expand Down
6 changes: 6 additions & 0 deletions packages/widget-react/src/lib/router/RouterContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface HistoryEntry {

interface RouterContextProps {
location: HistoryEntry
previousLocation: HistoryEntry | null
history: HistoryEntry[]
navigate: (to: string | number, state?: object) => void
reset: (to: string, state?: object) => void
Expand All @@ -23,6 +24,11 @@ export function useLocation() {
return location
}

export function usePreviousPath() {
const { previousLocation } = useRouterContext()
return previousLocation?.path
}

export function usePath() {
const { path } = useLocation()
return path
Expand Down
4 changes: 4 additions & 0 deletions packages/widget-react/src/pages/wallet/tabs/Home.module.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.container {
height: 100%;
}

.nav {
display: flex;
gap: 8px;
Expand Down
70 changes: 46 additions & 24 deletions packages/widget-react/src/pages/wallet/tabs/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,48 @@
import { useMemo } from "react"
import { Tabs } from "radix-ui"
import { IconArrowRight, IconSwap } from "@initia/icons-react"
import { Link, useNavigate, usePath } from "@/lib/router"
import { animated, useTransition } from "@react-spring/web"
import { Link, useNavigate, usePath, usePreviousPath } from "@/lib/router"
import { useClaimableModal } from "@/pages/bridge/op/reminder"
import Scrollable from "@/components/Scrollable"
import Assets from "./assets/Assets"
import Nfts from "./nft/Nfts"
import Activity from "./activity/Activity"
import styles from "./Home.module.css"

const tabs = [
{ label: "Assets", value: "/", component: <Assets /> },
{ label: "NFTs", value: "/nfts", component: <Nfts /> },
{ label: "History", value: "/activity", component: <Activity /> },
]

const Home = () => {
useClaimableModal()

const navigate = useNavigate()
const path = usePath()
const prevPath = usePreviousPath()

const direction = useMemo(() => {
const currentIndex = tabs.findIndex((t) => t.value === path)
const prevIndex = tabs.findIndex((t) => t.value === prevPath)

return currentIndex > prevIndex ? 1 : -1
}, [path, prevPath])
Comment on lines +26 to +31
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle edge case when tab indices are not found.

The direction calculation doesn't handle the case where findIndex returns -1 (when a path is not found in the tabs array). This could lead to incorrect animation direction.

 const direction = useMemo(() => {
   const currentIndex = tabs.findIndex((t) => t.value === path)
   const prevIndex = tabs.findIndex((t) => t.value === prevPath)

+  // Handle case where indices are not found
+  if (currentIndex === -1 || prevIndex === -1) {
+    return 1 // Default to forward direction
+  }
+
   return currentIndex > prevIndex ? 1 : -1
 }, [path, prevPath])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const direction = useMemo(() => {
const currentIndex = tabs.findIndex((t) => t.value === path)
const prevIndex = tabs.findIndex((t) => t.value === prevPath)
return currentIndex > prevIndex ? 1 : -1
}, [path, prevPath])
const direction = useMemo(() => {
const currentIndex = tabs.findIndex((t) => t.value === path)
const prevIndex = tabs.findIndex((t) => t.value === prevPath)
// Handle case where indices are not found
if (currentIndex === -1 || prevIndex === -1) {
return 1 // Default to forward direction
}
return currentIndex > prevIndex ? 1 : -1
}, [path, prevPath])
🤖 Prompt for AI Agents
In packages/widget-react/src/pages/wallet/tabs/Home.tsx around lines 26 to 31,
the direction calculation does not handle cases where findIndex returns -1 if a
path is not found in the tabs array. Update the useMemo logic to check if either
currentIndex or prevIndex is -1 and handle this edge case explicitly, for
example by returning a default direction or skipping the animation, to avoid
incorrect animation direction.


const skipAnimation =
!tabs.find((t) => t.value === prevPath) || !tabs.find((t) => t.value === path)

const transitions = useTransition(path, {
from: { opacity: 0, transform: `translateX(${direction * 100}%)` },
enter: { opacity: 1, transform: "translateX(0%)" },
leave: { opacity: 0, transform: `translateX(${direction * -100}%)` },
config: { tension: 250, friction: 30 },
immediate: skipAnimation,
})

return (
<Scrollable>
<Scrollable className={styles.container}>
<div className={styles.nav}>
<Link to="/send" className={styles.item}>
<IconArrowRight size={16} />
Expand All @@ -30,30 +57,25 @@ const Home = () => {

<Tabs.Root value={path} onValueChange={navigate}>
<Tabs.List className={styles.tabs}>
<Tabs.Trigger className={styles.tab} value="/">
Assets
</Tabs.Trigger>

<Tabs.Trigger className={styles.tab} value="/nfts">
NFTs
</Tabs.Trigger>

<Tabs.Trigger className={styles.tab} value="/activity">
History
</Tabs.Trigger>
{tabs.map((tab) => (
<Tabs.Trigger key={tab.value} className={styles.tab} value={tab.value}>
{tab.label}
</Tabs.Trigger>
))}
</Tabs.List>

<Tabs.Content value="/">
<Assets />
</Tabs.Content>

<Tabs.Content value="/nfts">
<Nfts />
</Tabs.Content>

<Tabs.Content value="/activity">
<Activity />
</Tabs.Content>
<div style={{ position: "relative" }}>
{transitions((style, item) => {
const tab = tabs.find((t) => t.value === item)
return (
<Tabs.Content forceMount key={item} value={item} asChild>
<animated.div style={{ ...style, position: "absolute", width: "100%" }}>
{tab?.component}
</animated.div>
</Tabs.Content>
)
})}
</div>
</Tabs.Root>
</Scrollable>
)
Expand Down
113 changes: 70 additions & 43 deletions packages/widget-react/src/public/app/Routes.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { useEffect } from "react"
import { useNavigate, usePath } from "@/lib/router"
import { useTransition, animated } from "@react-spring/web"
import { useNavigate, usePath, usePreviousPath } from "@/lib/router"
import { useModal } from "./ModalContext"
import { useAddress } from "../data/hooks"
import Connect from "@/pages/connect/Connect"
import Home from "@/pages/wallet/tabs/Home"
import Send from "@/pages/wallet/txs/send/Send"
Expand All @@ -12,71 +15,95 @@ import Withdrawals from "@/pages/bridge/op/Withdrawals"
import BridgePreview from "@/pages/bridge/BridgePreview"
import BridgeHistory from "@/pages/bridge/BridgeHistory"
import TxRequest from "@/pages/tx/TxRequest"
import { useAddress } from "../data/hooks"
import { useModal } from "./ModalContext"

const routes = [
{ path: "/connect", Component: Connect },
{ path: "/", Component: Home },
{ path: "/send", Component: Send, rerender: true },
{ path: "/collection", Component: CollectionDetails },
{ path: "/nft", Component: NftDetails },
{ path: "/nft/send", Component: SendNft, rerender: true },
{ path: "/rollups", Component: ManageChains },
{ path: "/bridge", Component: BridgeForm, renderWithoutAddress: true, rerender: true },
{ path: "/bridge/preview", Component: BridgePreview },
{ path: "/bridge/history", Component: BridgeHistory, renderWithoutAddress: true },
{ path: "/op/withdrawals", Component: Withdrawals },
{ path: "/tx", Component: TxRequest },
]

const Routes = () => {
const rawPath = usePath()
const rawPrevPath = usePreviousPath()
const navigate = useNavigate()
const path = usePath()
const address = useAddress()
const { closeModal } = useModal()

const path = ["/nfts", "/activity"].includes(rawPath) ? "/" : rawPath
const prevPath = ["/nfts", "/activity"].includes(rawPrevPath || "") ? "/" : rawPrevPath

// whenever address changes, navigate to the appropriate path
useEffect(() => {
closeModal()

if (path.startsWith("/bridge/")) {
navigate("/bridge")
}

if (path === "/collection" || path.startsWith("/nft")) {
navigate("/nfts")
}

// Run only on address changes, preventing navigation from triggering on path updates.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [address])

if (path === "/connect") {
if (address) return null
return <Connect />
}
// Compute transition direction
const currentIndex = routes.findIndex((r) => r.path === path)
const prevIndex = routes.findIndex((r) => r.path === prevPath)
const direction = currentIndex >= prevIndex ? 1 : -1
Comment on lines +58 to +61
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Handle edge cases when routes are not found in the array.

The findIndex method returns -1 when a route is not found, which could lead to unexpected animation directions. Consider adding validation to ensure both routes exist before calculating direction.

  // Compute transition direction
  const currentIndex = routes.findIndex((r) => r.path === path)
  const prevIndex = routes.findIndex((r) => r.path === prevPath)
- const direction = currentIndex >= prevIndex ? 1 : -1
+ const direction = currentIndex === -1 || prevIndex === -1 ? 1 : (currentIndex >= prevIndex ? 1 : -1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Compute transition direction
const currentIndex = routes.findIndex((r) => r.path === path)
const prevIndex = routes.findIndex((r) => r.path === prevPath)
const direction = currentIndex >= prevIndex ? 1 : -1
// Compute transition direction
const currentIndex = routes.findIndex((r) => r.path === path)
const prevIndex = routes.findIndex((r) => r.path === prevPath)
const direction = currentIndex === -1 || prevIndex === -1
? 1
: (currentIndex >= prevIndex ? 1 : -1)
🤖 Prompt for AI Agents
In packages/widget-react/src/public/app/Routes.tsx around lines 58 to 61, the
code calculates transition direction using indices from routes.findIndex, but
does not handle cases where a route is not found (findIndex returns -1). To fix
this, add validation to check if both currentIndex and prevIndex are not -1
before computing direction. If either is -1, handle the case appropriately, such
as defaulting direction to 0 or skipping the animation logic.


const transitions = useTransition(path, {
from:
direction > 0
? { opacity: 1, transform: `translateX(100%)` }
: { opacity: 0, transform: `translateX(0%)` },
enter: { opacity: 1, transform: "translateX(0%)" },
leave:
direction < 0
? { opacity: 1, transform: `translateX(100%)` }
: { opacity: 0, transform: `translateX(0%)` },
immediate: !prevPath,
})

if (path === "/connect" && !address) return <Connect />

// FIXME: do we need to block all routes when address is not connected?
if (!address) return null

switch (path) {
case "/bridge":
return <BridgeForm key={address} />
case "/bridge/history":
return <BridgeHistory />
}
return (
<div style={{ position: "relative", height: "100%", overflow: "hidden" }}>
{transitions((style, animatedPath) => {
const route = routes.find((r) => r.path === animatedPath)
if (!route) return null
if (!address && !route.renderWithoutAddress) return null

if (!address) {
return null
}
const { Component, rerender } = route

switch (path) {
case "/":
case "/nfts":
case "/activity":
return <Home />
case "/send":
return <Send key={address} />
case "/collection":
return <CollectionDetails />
case "/nft":
return <NftDetails />
case "/nft/send":
return <SendNft key={address} />
case "/rollups":
return <ManageChains />
case "/bridge/preview":
return <BridgePreview />
case "/op/withdrawals":
return <Withdrawals />
case "/tx":
return <TxRequest />
case "/blank":
return null
}
return (
<animated.div
key={animatedPath}
style={{
...style,
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "var(--bg)",
}}
>
{rerender ? <Component key={address} /> : <Component />}
</animated.div>
)
})}
</div>
)
}

export default Routes