diff --git a/babel.config.js b/babel.config.js index eca2870ce49edb..754eaac1536070 100644 --- a/babel.config.js +++ b/babel.config.js @@ -31,6 +31,7 @@ module.exports = function getBabelConfig(api) { '@mui/base': resolveAliasPath('./packages/mui-base/src'), '@mui/utils': resolveAliasPath('./packages/mui-utils/src'), '@mui/material-next': resolveAliasPath('./packages/mui-material-next/src'), + '@mui/material-nextjs': resolveAliasPath('./packages/mui-material-nextjs/src'), '@mui/joy': resolveAliasPath('./packages/mui-joy/src'), '@mui/zero-runtime': resolveAliasPath('./packages/zero-runtime/src'), docs: resolveAliasPath('./docs'), diff --git a/docs/data/material/guides/nextjs/nextjs.md b/docs/data/material/guides/nextjs/nextjs.md index 08ff785fc1b687..2d724034821d0a 100644 --- a/docs/data/material/guides/nextjs/nextjs.md +++ b/docs/data/material/guides/nextjs/nextjs.md @@ -59,6 +59,35 @@ This option ensures that the styles generated by Material UI will be wrapped in To learn more about it, see [the MDN CSS layer documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer). +### Link + +The package provides a thin wrapper Link component that combines the functionality of the Next.js [Link](https://nextjs.org/docs/api-reference/next/link) and the styles of Material UI [Link](https://mui.com/components/links/). + +You can pass any props supported by both components to the Link. + +```js +import { Link } from '@mui/material-nextjs/v13-appRouter'; // or `v14-appRouter` if you are using Next.js v14 + +function Nav() { + return ( +
+ + Dashboard + +
+ ); +} +``` + +To learn more about the supported props, visit: + +- Next.js related props: [Next.js Link](https://nextjs.org/docs/app/api-reference/components/link#props) +- Material UI related props: [Material UI Link](/material-ui/react-link/) + +:::warning +Do not use the Link from `@mui/material-nextjs/*-pagesRouter` for App Router. +::: + ## Pages Router This section walks through the Material UI integration with the Next.js [Pages Router](https://nextjs.org/docs/pages/building-your-application), for both [Server Side Rendering](https://nextjs.org/docs/pages/building-your-application/rendering/server-side-rendering) (SSR) and [Static Site Generation](https://nextjs.org/docs/pages/building-your-application/rendering/static-site-generation) (SSG). @@ -149,3 +178,30 @@ If you are using TypeScript, add `DocumentHeadTagsProps` to the Document's props ... } ``` + +### Link + +For pages router, use the Link only from `@mui/material-nextjs/*-pagesRouter`: + +```js +import { Link } from '@mui/material-nextjs/v13-pagesRouter'; // or `v14-pagesRouter` if you are using Next.js v14 + +function Nav() { + return ( +
+ + Dashboard + +
+ ); +} +``` + +To learn more about the supported props, visit: + +- Next.js related props: [Next.js Link](https://nextjs.org/docs/pages/api-reference/components/link#props) +- Material UI related props: [Material UI Link](/material-ui/react-link/) + +:::warning +Do not use the Link from `@mui/material-nextjs/*-appRouter` for Pages Router. +::: diff --git a/packages/mui-material-nextjs/package.json b/packages/mui-material-nextjs/package.json index 1daef8f6a664b6..75b17a20f12763 100644 --- a/packages/mui-material-nextjs/package.json +++ b/packages/mui-material-nextjs/package.json @@ -36,6 +36,9 @@ "test": "cd ../../ && cross-env NODE_ENV=test mocha 'packages/mui-utils/**/*.test.{js,ts,tsx}'", "typescript": "tsc -p tsconfig.json" }, + "dependencies": { + "clsx": "^2.1.0" + }, "devDependencies": { "@emotion/cache": "^11.11.0", "@emotion/react": "^11.5.0", diff --git a/packages/mui-material-nextjs/src/v13-appRouter/Link/Link.spec.tsx b/packages/mui-material-nextjs/src/v13-appRouter/Link/Link.spec.tsx new file mode 100644 index 00000000000000..7d581136f7df5f --- /dev/null +++ b/packages/mui-material-nextjs/src/v13-appRouter/Link/Link.spec.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { Link } from '@mui/material-nextjs/v13-appRouter'; + +// @ts-expect-error href is required by Next.js +; +; +; + + Dashboard +; + + Dashboard +; + + Dashboard +; + +; +; +; diff --git a/packages/mui-material-nextjs/src/v13-appRouter/Link/Link.tsx b/packages/mui-material-nextjs/src/v13-appRouter/Link/Link.tsx new file mode 100644 index 00000000000000..9155fcffedfd53 --- /dev/null +++ b/packages/mui-material-nextjs/src/v13-appRouter/Link/Link.tsx @@ -0,0 +1,63 @@ +'use client'; +import * as React from 'react'; +import clsx from 'clsx'; +import { usePathname } from 'next/navigation'; +import NextLink, { LinkProps as NextLinkProps } from 'next/link'; +import MuiLink, { LinkOwnProps as MuiLinkProps } from '@mui/material/Link'; +import linkClasses from './linkClasses'; + +export interface LinkProps + extends Omit, + MuiLinkProps { + /** + * Extra class name to apply to the link. + */ + className?: string; + /** + * The active class name to apply when the current page is the same as the link's href. + * @default 'Mui-active' + */ + activeClassName?: string; +} + +const Link = React.forwardRef(function Link( + { + href, + replace, + scroll, + shallow, + prefetch, + locale, + as, + className: classNameProp, + activeClassName = linkClasses.active, + ...muiLinkProps + }, + ref, +) { + const nextPathname = usePathname(); + const pathname = typeof href === 'string' ? href : href.pathname; + const className = clsx(classNameProp, { + [activeClassName]: nextPathname === pathname && activeClassName, + }); + return ( + + + + ); +}); + +export default Link; diff --git a/packages/mui-material-nextjs/src/v13-appRouter/Link/index.ts b/packages/mui-material-nextjs/src/v13-appRouter/Link/index.ts new file mode 100644 index 00000000000000..b0958cdbcfbb49 --- /dev/null +++ b/packages/mui-material-nextjs/src/v13-appRouter/Link/index.ts @@ -0,0 +1,4 @@ +export { default as Link } from './Link'; +export { default as linkClasses, getLinkUtilityClass } from './linkClasses'; +export type { LinkProps } from './Link'; +export type { LinkClasses } from './linkClasses'; diff --git a/packages/mui-material-nextjs/src/v13-appRouter/Link/linkClasses.ts b/packages/mui-material-nextjs/src/v13-appRouter/Link/linkClasses.ts new file mode 100644 index 00000000000000..6c2438e043732a --- /dev/null +++ b/packages/mui-material-nextjs/src/v13-appRouter/Link/linkClasses.ts @@ -0,0 +1,26 @@ +import generateUtilityClasses from '@mui/utils/generateUtilityClasses'; +import generateUtilityClass from '@mui/utils/generateUtilityClass'; +import { LinkClasses as MuiLinkClasses } from '@mui/material/Link'; + +export interface LinkClasses extends MuiLinkClasses { + /** + * Styles applied to the root element when the link is active. + */ + active: string; +} + +export function getLinkUtilityClass(slot: string): string { + return generateUtilityClass('MuiLink', slot); +} + +const linkClasses: LinkClasses = generateUtilityClasses('MuiLink', [ + 'root', + 'underlineNone', + 'underlineHover', + 'underlineAlways', + 'button', + 'focusVisible', + 'active', +]); + +export default linkClasses; diff --git a/packages/mui-material-nextjs/src/v13-appRouter/index.ts b/packages/mui-material-nextjs/src/v13-appRouter/index.ts index 3b153dde004ec4..80eb6c9856903a 100644 --- a/packages/mui-material-nextjs/src/v13-appRouter/index.ts +++ b/packages/mui-material-nextjs/src/v13-appRouter/index.ts @@ -1,3 +1,4 @@ 'use client'; export { default as AppRouterCacheProvider } from './appRouterV13'; export * from './appRouterV13'; +export * from './Link'; diff --git a/packages/mui-material-nextjs/src/v13-pagesRouter/Link/Link.spec.tsx b/packages/mui-material-nextjs/src/v13-pagesRouter/Link/Link.spec.tsx new file mode 100644 index 00000000000000..da130376674539 --- /dev/null +++ b/packages/mui-material-nextjs/src/v13-pagesRouter/Link/Link.spec.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { Link } from '@mui/material-nextjs/v13-pagesRouter'; + +// @ts-expect-error href is required by Next.js +; +; +; + + Dashboard +; + + Dashboard +; + + Dashboard +; + +; +; +; diff --git a/packages/mui-material-nextjs/src/v13-pagesRouter/Link/Link.tsx b/packages/mui-material-nextjs/src/v13-pagesRouter/Link/Link.tsx new file mode 100644 index 00000000000000..c6e947b810cf0a --- /dev/null +++ b/packages/mui-material-nextjs/src/v13-pagesRouter/Link/Link.tsx @@ -0,0 +1,63 @@ +'use client'; +import * as React from 'react'; +import clsx from 'clsx'; +import { useRouter } from 'next/router'; +import NextLink, { LinkProps as NextLinkProps } from 'next/link'; +import MuiLink, { LinkOwnProps as MuiLinkProps } from '@mui/material/Link'; +import linkClasses from './linkClasses'; + +export interface LinkProps + extends Omit, + MuiLinkProps { + /** + * Extra class name to apply to the link. + */ + className?: string; + /** + * The active class name to apply when the current page is the same as the link's href. + * @default 'Mui-active' + */ + activeClassName?: string; +} + +const Link = React.forwardRef(function Link( + { + href, + replace, + scroll, + shallow, + prefetch, + locale, + as, + className: classNameProp, + activeClassName = linkClasses.active, + ...muiLinkProps + }, + ref, +) { + const router = useRouter(); + const pathname = typeof href === 'string' ? href : href.pathname; + const className = clsx(classNameProp, { + [activeClassName]: router.pathname === pathname && activeClassName, + }); + return ( + + + + ); +}); + +export default Link; diff --git a/packages/mui-material-nextjs/src/v13-pagesRouter/Link/index.ts b/packages/mui-material-nextjs/src/v13-pagesRouter/Link/index.ts new file mode 100644 index 00000000000000..b0958cdbcfbb49 --- /dev/null +++ b/packages/mui-material-nextjs/src/v13-pagesRouter/Link/index.ts @@ -0,0 +1,4 @@ +export { default as Link } from './Link'; +export { default as linkClasses, getLinkUtilityClass } from './linkClasses'; +export type { LinkProps } from './Link'; +export type { LinkClasses } from './linkClasses'; diff --git a/packages/mui-material-nextjs/src/v13-pagesRouter/Link/linkClasses.ts b/packages/mui-material-nextjs/src/v13-pagesRouter/Link/linkClasses.ts new file mode 100644 index 00000000000000..6c2438e043732a --- /dev/null +++ b/packages/mui-material-nextjs/src/v13-pagesRouter/Link/linkClasses.ts @@ -0,0 +1,26 @@ +import generateUtilityClasses from '@mui/utils/generateUtilityClasses'; +import generateUtilityClass from '@mui/utils/generateUtilityClass'; +import { LinkClasses as MuiLinkClasses } from '@mui/material/Link'; + +export interface LinkClasses extends MuiLinkClasses { + /** + * Styles applied to the root element when the link is active. + */ + active: string; +} + +export function getLinkUtilityClass(slot: string): string { + return generateUtilityClass('MuiLink', slot); +} + +const linkClasses: LinkClasses = generateUtilityClasses('MuiLink', [ + 'root', + 'underlineNone', + 'underlineHover', + 'underlineAlways', + 'button', + 'focusVisible', + 'active', +]); + +export default linkClasses; diff --git a/packages/mui-material-nextjs/src/v13-pagesRouter/index.ts b/packages/mui-material-nextjs/src/v13-pagesRouter/index.ts index ff4f92e5707270..b8fa76c3417865 100644 --- a/packages/mui-material-nextjs/src/v13-pagesRouter/index.ts +++ b/packages/mui-material-nextjs/src/v13-pagesRouter/index.ts @@ -1,2 +1,3 @@ export * from './pagesRouterV13Document'; export * from './pagesRouterV13App'; +export * from './Link'; diff --git a/packages/mui-material-nextjs/tsconfig.build.json b/packages/mui-material-nextjs/tsconfig.build.json index 405e223d9a9bad..e40d6e52ebaa3d 100644 --- a/packages/mui-material-nextjs/tsconfig.build.json +++ b/packages/mui-material-nextjs/tsconfig.build.json @@ -11,5 +11,6 @@ "rootDir": "./src" }, "include": ["./src/**/*.ts*"], - "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*"] + "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*"], + "references": [{ "path": "../mui-material/tsconfig.build.json" }] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 899327c1ce4300..9737a65f0efd2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1708,6 +1708,9 @@ importers: '@mui/material': specifier: ^5.0.0 version: link:../mui-material/build + clsx: + specifier: ^2.1.0 + version: 2.1.0 devDependencies: '@emotion/cache': specifier: ^11.11.0