diff --git a/docs/data/toolpad/core/components/page-container/CustomPageContainer.js b/docs/data/toolpad/core/components/page-container/CustomPageContainer.js index 43529696937..d9a681669d6 100644 --- a/docs/data/toolpad/core/components/page-container/CustomPageContainer.js +++ b/docs/data/toolpad/core/components/page-container/CustomPageContainer.js @@ -42,6 +42,7 @@ function Content({ router }) { Content.propTypes = { router: PropTypes.shape({ + Link: PropTypes.func, navigate: PropTypes.func.isRequired, pathname: PropTypes.string.isRequired, searchParams: PropTypes.instanceOf(URLSearchParams).isRequired, diff --git a/docs/pages/toolpad/core/api/app-provider.json b/docs/pages/toolpad/core/api/app-provider.json index 6918852f106..b2ab0e72fbb 100644 --- a/docs/pages/toolpad/core/api/app-provider.json +++ b/docs/pages/toolpad/core/api/app-provider.json @@ -22,7 +22,7 @@ "router": { "type": { "name": "shape", - "description": "{ navigate: func, pathname: string, searchParams: URLSearchParams }" + "description": "{ Link?: func, navigate: func, pathname: string, searchParams: URLSearchParams }" }, "default": "null" }, diff --git a/packages/toolpad-core/src/AppProvider/AppProvider.tsx b/packages/toolpad-core/src/AppProvider/AppProvider.tsx index 0de8c74eec7..57a97eba5f8 100644 --- a/packages/toolpad-core/src/AppProvider/AppProvider.tsx +++ b/packages/toolpad-core/src/AppProvider/AppProvider.tsx @@ -10,6 +10,7 @@ import { RouterContext, WindowContext, } from '../shared/context'; +import type { LinkProps } from '../shared/Link'; import { AppThemeProvider } from './AppThemeProvider'; export interface NavigateOptions { @@ -27,6 +28,7 @@ export interface Router { pathname: string; searchParams: URLSearchParams; navigate: Navigate; + Link?: React.ComponentType; } export interface Branding { @@ -242,6 +244,7 @@ AppProvider.propTypes /* remove-proptypes */ = { * @default null */ router: PropTypes.shape({ + Link: PropTypes.func, navigate: PropTypes.func.isRequired, pathname: PropTypes.string.isRequired, searchParams: PropTypes.instanceOf(URLSearchParams).isRequired, diff --git a/packages/toolpad-core/src/nextjs/NextAppProvider.test.tsx b/packages/toolpad-core/src/nextjs/NextAppProvider.test.tsx index ba23bedc3d0..c49396090de 100644 --- a/packages/toolpad-core/src/nextjs/NextAppProvider.test.tsx +++ b/packages/toolpad-core/src/nextjs/NextAppProvider.test.tsx @@ -24,6 +24,8 @@ vi.mock('next/router', () => ({ useRouter: () => null })); vi.mock('next/compat/router', () => ({ useRouter: () => null })); +vi.mock('next/link', () => ({ default: () => null })); + interface RouterTestProps { children: React.ReactNode; } diff --git a/packages/toolpad-core/src/nextjs/NextAppProviderApp.tsx b/packages/toolpad-core/src/nextjs/NextAppProviderApp.tsx index f01b51a653d..0f64d3a976d 100644 --- a/packages/toolpad-core/src/nextjs/NextAppProviderApp.tsx +++ b/packages/toolpad-core/src/nextjs/NextAppProviderApp.tsx @@ -1,8 +1,14 @@ import * as React from 'react'; +import NextLink from 'next/link'; import { usePathname, useSearchParams, useRouter } from 'next/navigation'; +import { LinkProps } from '../shared/Link'; import { AppProvider } from '../AppProvider'; import type { AppProviderProps, Navigate, Router } from '../AppProvider'; +const Link = React.forwardRef((props, ref) => { + const { href, history, ...rest } = props; + return ; +}); /** * @ignore - internal component. */ @@ -29,6 +35,7 @@ export function NextAppProviderApp(props: AppProviderProps) { pathname, searchParams, navigate, + Link, }), [pathname, navigate, searchParams], ); diff --git a/packages/toolpad-core/src/nextjs/NextAppProviderPages.tsx b/packages/toolpad-core/src/nextjs/NextAppProviderPages.tsx index ac918983c7e..e9a25759c4e 100644 --- a/packages/toolpad-core/src/nextjs/NextAppProviderPages.tsx +++ b/packages/toolpad-core/src/nextjs/NextAppProviderPages.tsx @@ -1,9 +1,16 @@ import * as React from 'react'; +import NextLink from 'next/link'; import { asArray } from '@toolpad/utils/collections'; import { useRouter } from 'next/router'; +import { LinkProps } from '../shared/Link'; import { AppProvider } from '../AppProvider'; import type { AppProviderProps, Navigate, Router } from '../AppProvider'; +const Link = React.forwardRef((props, ref) => { + const { href, history, ...rest } = props; + return ; +}); + /** * @ignore - internal component. */ @@ -41,6 +48,7 @@ export function NextAppProviderPages(props: AppProviderProps) { pathname, searchParams, navigate, + Link, }), [navigate, pathname, searchParams], ); diff --git a/packages/toolpad-core/src/react-router/ReactRouterAppProvider.tsx b/packages/toolpad-core/src/react-router/ReactRouterAppProvider.tsx index afb1fdaf89a..f76ab670257 100644 --- a/packages/toolpad-core/src/react-router/ReactRouterAppProvider.tsx +++ b/packages/toolpad-core/src/react-router/ReactRouterAppProvider.tsx @@ -1,7 +1,13 @@ 'use client'; import * as React from 'react'; -import { useSearchParams, useLocation, useNavigate } from 'react-router'; +import { useSearchParams, useLocation, useNavigate, Link as ReactRouterLink } from 'react-router'; import { AppProvider, type AppProviderProps, Navigate, Router } from '../AppProvider/AppProvider'; +import { LinkProps } from '../shared/Link'; + +const Link = React.forwardRef((props, ref) => { + const { href, history, ...rest } = props; + return ; +}); function ReactRouterAppProvider(props: AppProviderProps) { const { pathname } = useLocation(); @@ -26,6 +32,7 @@ function ReactRouterAppProvider(props: AppProviderProps) { pathname, searchParams, navigate: navigateImpl, + Link, }), [pathname, searchParams, navigateImpl], ); diff --git a/packages/toolpad-core/src/shared/Link.tsx b/packages/toolpad-core/src/shared/Link.tsx index d17d657e2a1..9c4814bf8dc 100644 --- a/packages/toolpad-core/src/shared/Link.tsx +++ b/packages/toolpad-core/src/shared/Link.tsx @@ -6,10 +6,16 @@ import { RouterContext } from './context'; */ export interface LinkProps extends React.AnchorHTMLAttributes { + /* + * "replace" will replace the history stack with the URL being navigated to + * "push" will push the URL being navigated to as a new entry onto the history stack + * "auto" is the default and the mimics the "push" behaviour + */ history?: 'auto' | 'push' | 'replace'; + href: string; } -export const Link = React.forwardRef(function Link( +export const DefaultLink = React.forwardRef(function Link( props: LinkProps, ref: React.ForwardedRef, ) { @@ -34,3 +40,16 @@ export const Link = React.forwardRef(function Link( ); }); + +export const Link = React.forwardRef(function Link( + props: LinkProps, + ref: React.ForwardedRef, +) { + const routerContext = React.useContext(RouterContext); + const LinkComponent = routerContext?.Link ?? DefaultLink; + return ( + + {props.children} + + ); +});