diff --git a/packages/tuono-router/src/components/Link.spec.tsx b/packages/tuono-router/src/components/Link.spec.tsx index a8c3f6ca..19ac00b8 100644 --- a/packages/tuono-router/src/components/Link.spec.tsx +++ b/packages/tuono-router/src/components/Link.spec.tsx @@ -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( + + Test 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(Anchor Link) const link = screen.getByRole('link') diff --git a/packages/tuono-router/src/components/Link.tsx b/packages/tuono-router/src/components/Link.tsx index a5890b1c..34b87691 100644 --- a/packages/tuono-router/src/components/Link.tsx +++ b/packages/tuono-router/src/components/Link.tsx @@ -16,6 +16,12 @@ interface TuonoLinkProps extends React.AnchorHTMLAttributes { * @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 ( diff --git a/packages/tuono-router/src/hooks/useRouter.ts b/packages/tuono-router/src/hooks/useRouter.ts index 1ec37f63..bd34af92 100644 --- a/packages/tuono-router/src/hooks/useRouter.ts +++ b/packages/tuono-router/src/hooks/useRouter.ts @@ -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, }