Skip to content
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

[material-ui-nextjs] Add Link integration component #40265

Closed
wants to merge 9 commits into from
Closed
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
1 change: 1 addition & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
56 changes: 56 additions & 0 deletions docs/data/material/guides/nextjs/nextjs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

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

This creates a horizontal scrollbar, and I think better if set to be generic.

Suggested change
import { Link } from '@mui/material-nextjs/v13-appRouter'; // or `v14-appRouter` if you are using Next.js v14
import { Link } from '@mui/material-nextjs/v13-appRouter';
// or `v1X-appRouter` if you are using Next.js v1X


function Nav() {
return (
<div>
<Link href="/dashboard" variant="caption" sx={{ bgcolor: 'grey.100' }}>
Dashboard
</Link>
</div>
);
}
```

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 <b>`@mui/material-nextjs/*-pagesRouter`</b> for App Router.
:::

Comment on lines +87 to +90
Copy link
Member

Choose a reason for hiding this comment

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

Is this needed?

Suggested change
:::warning
Do not use the Link from <b>`@mui/material-nextjs/*-pagesRouter`</b> for App Router.
:::

Because we import the router, it will crash if it uses the wrong API

Screenshot 2024-01-16 at 01 21 57

## 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).
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

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

I think we should also document how to use a Link with a Button.

import Button from '@mui/material/Button';
import NextLink from 'next/link';

<Button component={NextLink} href="/foo">

Actually, I don't know if this works (maybe types issue, maybe href prop conflict) but this is definitely an important use case.

Copy link

Choose a reason for hiding this comment

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

Also it should be possible to pass in NextLink via a theme, right? Should we have that example as well?


function Nav() {
return (
<div>
<Link href="/dashboard" variant="caption" sx={{ bgcolor: 'grey.100' }}>
Dashboard
</Link>
</div>
);
}
```

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 <b>`@mui/material-nextjs/*-appRouter`</b> for Pages Router.
:::
3 changes: 3 additions & 0 deletions packages/mui-material-nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
25 changes: 25 additions & 0 deletions packages/mui-material-nextjs/src/v13-appRouter/Link/Link.spec.tsx
Original file line number Diff line number Diff line change
@@ -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
<Link />;
<Link href="" />;
<Link
href={{
pathname: '/about',
query: { name: 'test' },
}}
/>;
<Link href="/dashboard" replace>
Dashboard
</Link>;
<Link href="/dashboard" scroll={false}>
Dashboard
</Link>;
<Link href="/dashboard" prefetch={false}>
Dashboard
</Link>;

<Link href="" sx={{ color: 'secondary.dark' }} />;
<Link href="" variant="caption" />;
<Link href="" underline="always" />;
63 changes: 63 additions & 0 deletions packages/mui-material-nextjs/src/v13-appRouter/Link/Link.tsx
Original file line number Diff line number Diff line change
@@ -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<NextLinkProps, 'passHref' | 'legacyBehavior'>,
MuiLinkProps {
/**
* Extra class name to apply to the link.
*/
Comment on lines +12 to +14
Copy link
Member

Choose a reason for hiding this comment

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

What we usually use for the className:

Suggested change
/**
* Extra class name to apply to the link.
*/

Did this mui/mui-x#11693 accordingly.

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<HTMLAnchorElement, LinkProps>(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 (
<NextLink
{...{
href,
replace,
scroll,
shallow,
prefetch,
locale,
as,
}}
// below props are required for NextLink to work with MUI Link
passHref
legacyBehavior
Copy link
Member

@oliviertassinari oliviertassinari Jan 16, 2024

Choose a reason for hiding this comment

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

I think we should update to the latest stable API, part of is:

Suggested change
legacyBehavior

I don't see why it's needed, I would expect <MdLink component={NextLink}> to work fine.

Copy link
Member

Choose a reason for hiding this comment

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

>
<MuiLink ref={ref} className={className} {...muiLinkProps} />
</NextLink>
);
});

export default Link;
4 changes: 4 additions & 0 deletions packages/mui-material-nextjs/src/v13-appRouter/Link/index.ts
Original file line number Diff line number Diff line change
@@ -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';
26 changes: 26 additions & 0 deletions packages/mui-material-nextjs/src/v13-appRouter/Link/linkClasses.ts
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions packages/mui-material-nextjs/src/v13-appRouter/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client';
export { default as AppRouterCacheProvider } from './appRouterV13';
export * from './appRouterV13';
export * from './Link';
Original file line number Diff line number Diff line change
@@ -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
<Link />;
<Link href="" />;
<Link
href={{
pathname: '/about',
query: { name: 'test' },
}}
/>;
<Link href="/dashboard" replace>
Dashboard
</Link>;
<Link href="/dashboard" scroll={false}>
Dashboard
</Link>;
<Link href="/dashboard" prefetch={false}>
Dashboard
</Link>;

<Link href="" sx={{ color: 'secondary.dark' }} />;
<Link href="" variant="caption" />;
<Link href="" underline="always" />;
63 changes: 63 additions & 0 deletions packages/mui-material-nextjs/src/v13-pagesRouter/Link/Link.tsx
Original file line number Diff line number Diff line change
@@ -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<NextLinkProps, 'passHref' | 'legacyBehavior'>,
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<HTMLAnchorElement, LinkProps>(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 (
<NextLink
{...{
href,
replace,
scroll,
shallow,
prefetch,
locale,
as,
}}
// below props are required for NextLink to work with MUI Link
passHref
legacyBehavior
>
<MuiLink ref={ref} className={className} {...muiLinkProps} />
</NextLink>
);
});

export default Link;
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions packages/mui-material-nextjs/src/v13-pagesRouter/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './pagesRouterV13Document';
export * from './pagesRouterV13App';
export * from './Link';
3 changes: 2 additions & 1 deletion packages/mui-material-nextjs/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" }]
}
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.