feat(packages/tuono-router): add replace method to router and replace prop to Link (#596)

This commit is contained in:
Jovan Milosevic
2025-02-24 19:53:59 +01:00
committed by GitHub
parent 798f8f1bf8
commit d71185c9cb
3 changed files with 59 additions and 8 deletions
@@ -4,10 +4,14 @@ import { render, fireEvent, screen } from '@testing-library/react'
import Link from './Link'
const pushMock = vi.fn()
const replaceMock = vi.fn()
const preloadMock = vi.fn()
vi.mock('../hooks/useRouter', () => ({
useRouter: (): { push: typeof pushMock } => ({ push: pushMock }),
useRouter: (): { push: typeof pushMock; replace: typeof replaceMock } => ({
push: pushMock,
replace: replaceMock,
}),
}))
vi.mock('../hooks/useRoute', () => ({
@@ -51,6 +55,19 @@ describe('Link Component', () => {
expect(pushMock).toHaveBeenCalledWith('/test', { scroll: true })
})
it('calls router.replace on click when the replace prop is true', () => {
render(
<Link href="/test" replace>
Test Link
</Link>,
)
const link = screen.getByRole('link')
fireEvent.click(link)
expect(replaceMock).toHaveBeenCalledWith('/test', { scroll: true })
expect(pushMock).not.toHaveBeenCalled()
})
it('does not navigate if href starts with "#"', () => {
render(<Link href="#section">Anchor Link</Link>)
const link = screen.getByRole('link')
+10 -1
View File
@@ -16,6 +16,12 @@ interface TuonoLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
* @default true
*/
scroll?: boolean
/**
* If "true" the history entry will be replaced instead of pushed.
* @default false
*/
replace?: boolean
}
function isEventModifierKeyActiveAndTargetDifferentFromSelf(
@@ -39,6 +45,7 @@ export default function Link(
scroll = true,
children,
href,
replace,
onClick,
...rest
} = componentProps
@@ -68,7 +75,9 @@ export default function Link(
event.preventDefault()
router.push(href || '', { scroll })
const method = replace ? 'replace' : 'push'
router[method](href || '', { scroll })
}
return (
+31 -6
View File
@@ -2,7 +2,10 @@ import React from 'react'
import { useRouterContext } from '../components/RouterContext'
interface PushOptions {
type NavigationType = 'pushState' | 'replaceState'
type NavigationFn = (path: string, opts?: NavigationOptions) => void
interface NavigationOptions {
/**
* If "false" the scroll offset will be kept across page navigation. Default "true"
*/
@@ -13,7 +16,13 @@ interface UseRouterResult {
/**
* Redirects to the path passed as argument updating the browser history.
*/
push: (path: string, opt?: PushOptions) => void
push: NavigationFn
/**
* Redirects to the path passed as argument replacing the current history
* entry.
*/
replace: NavigationFn
/**
* This object contains all the query params of the current route
@@ -29,9 +38,9 @@ interface UseRouterResult {
export const useRouter = (): UseRouterResult => {
const { location, updateLocation } = useRouterContext()
const push = React.useCallback(
(path: string, opt?: PushOptions): void => {
const { scroll = true } = opt || {}
const navigate = React.useCallback(
(type: NavigationType, path: string, opts?: NavigationOptions): void => {
const { scroll = true } = opts || {}
const url = new URL(path, window.location.origin)
updateLocation({
@@ -41,7 +50,8 @@ export const useRouter = (): UseRouterResult => {
searchStr: url.search,
hash: url.hash,
})
history.pushState(path, '', path)
history[type](path, '', path)
if (scroll) {
window.scroll(0, 0)
@@ -50,8 +60,23 @@ export const useRouter = (): UseRouterResult => {
[updateLocation],
)
const push = React.useCallback(
(path: string, opts?: NavigationOptions): void => {
navigate('pushState', path, opts)
},
[navigate],
)
const replace = React.useCallback(
(path: string, opts?: NavigationOptions): void => {
navigate('replaceState', path, opts)
},
[navigate],
)
return {
push,
replace,
query: location.search,
pathname: location.pathname,
}