Skip to content

Commit

Permalink
✨ heading anchor feature
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnsonMao committed Jul 13, 2023
1 parent 95b68da commit 2b6ae05
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 13 deletions.
2 changes: 1 addition & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const metadata: Metadata = {

function RootLayout({ children }: React.PropsWithChildren) {
return (
<html lang="en" suppressHydrationWarning>
<html lang="en" className="scroll-smooth" suppressHydrationWarning>
<body className="dark:bg-slate-800">
<Providers>
<Navbar {...navbar} />
Expand Down
4 changes: 4 additions & 0 deletions src/assets/css/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@

@import "./prism-plus.css";
@import "./prism-vsc-dark-plus.css";

:root {
scroll-padding-top: 72px;
}
52 changes: 46 additions & 6 deletions src/components/common/Heading/Heading.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,53 @@
import type { HTMLAttributes } from 'react';
import cn from '@/utils/cn';

type HeadingProps = {
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
} & HTMLAttributes<HTMLHeadingElement>;
type HTMLHeadingProps = HTMLAttributes<HTMLHeadingElement>;

function Heading({ as = 'h2', ...attributes }: HeadingProps) {
const HeadingTag = as;
type HeadingProps = {
Component?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
} & HTMLHeadingProps;

return <HeadingTag {...attributes} />;
function Heading({
Component = 'h2',
id,
className,
children,
...attributes
}: HeadingProps) {
return (
<Component
id={id}
className={cn('group relative', className)}
{...attributes}
>
<a
href={id}
className="absolute -left-6 no-underline opacity-0 group-hover:opacity-100"
>
#
</a>
{children}
</Component>
);
}

export const H1 = (props: HTMLHeadingProps) => (
<Heading Component="h1" {...props} />
);
export const H2 = (props: HTMLHeadingProps) => (
<Heading Component="h2" {...props} />
);
export const H3 = (props: HTMLHeadingProps) => (
<Heading Component="h3" {...props} />
);
export const H4 = (props: HTMLHeadingProps) => (
<Heading Component="h4" {...props} />
);
export const H5 = (props: HTMLHeadingProps) => (
<Heading Component="h5" {...props} />
);
export const H6 = (props: HTMLHeadingProps) => (
<Heading Component="h6" {...props} />
);

export default Heading;
24 changes: 21 additions & 3 deletions src/components/common/Heading/heading.test.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
import { render, screen } from '@testing-library/react';

import Heading from '.';
import Heading, { H1, H2, H3, H4, H5, H6 } from '.';

describe('Heading component', () => {
it('should render correct element', () => {
const name = 'The heading text';

render(<Heading>{name}</Heading>);

const heading = screen.getByRole('heading', { name });
const heading = screen.getByRole('heading');

expect(heading).toBeInTheDocument();
expect(heading).toHaveTextContent(name);
expect(heading).toHaveTextContent(`#${name}`);
expect(heading.tagName).toBe('H2');
});

it.each([
[H1, 'H1'],
[H2, 'H2'],
[H3, 'H3'],
[H4, 'H4'],
[H5, 'H5'],
[H6, 'H6'],
])('should render correct tag name', (Component, expected) => {
const name = `The ${expected} tag`;

render(<Component>{name}</Component>);

const heading = screen.getByRole('heading');

expect(heading).toHaveTextContent(`#${name}`);
expect(heading.tagName).toBe(expected);
});
});
2 changes: 2 additions & 0 deletions src/components/common/Heading/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Heading from './Heading';

export * from './Heading';

export default Heading;
11 changes: 8 additions & 3 deletions src/utils/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import rehypePrismPlus from 'rehype-prism-plus';
import rehypeCodeTitles from 'rehype-code-titles';

import { POSTS_DIRECTORY } from '@/configs/path';
import Heading from '@/components/common/Heading';
import { H1, H2, H3, H4, H5, H6 } from '@/components/common/Heading';
import CodeBox from '@/components/common/CodeBox';

export interface IPost {
Expand Down Expand Up @@ -67,8 +67,13 @@ export async function getPostDataById(
const { content, frontmatter } = await compileMDX<IPost>({
source: fileContents,
components: {
h1: Heading,
pre: CodeBox
h1: H1,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
h6: H6,
pre: CodeBox,
},
options: {
parseFrontmatter: true,
Expand Down

0 comments on commit 2b6ae05

Please sign in to comment.