├── .eslintignore
├── .eslintrc
├── .github
└── workflows
│ ├── ci-and-publish.yml
│ ├── ci.yml
│ ├── codeql-analysis.yml
│ └── stale.yml
├── .gitignore
├── .prettierignore
├── .releaserc.json
├── .size-limit.json
├── .vscode
└── extensions.json
├── LICENSE
├── README.md
├── assets
└── react-headings.png
├── codecov.yml
├── package-lock.json
├── package.json
├── src
├── index.test.tsx
└── index.tsx
├── tsconfig.json
└── vitest.config.ts
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | coverage
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "plugins": ["@typescript-eslint"],
4 | "extends": [
5 | "plugin:@typescript-eslint/eslint-recommended",
6 | "plugin:@typescript-eslint/recommended",
7 | "prettier"
8 | ],
9 | "env": {
10 | "es6": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.github/workflows/ci-and-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package when a release is created
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3 |
4 | name: ci-and-publish
5 |
6 | on:
7 | push:
8 | branches: [master, next]
9 |
10 | jobs:
11 | test:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | node-version: [12.x, 14.x, 16.x, 18.x]
16 | steps:
17 | - uses: actions/checkout@v3
18 | - name: Use Node.js ${{ matrix.node-version }}
19 | uses: actions/setup-node@v2
20 | with:
21 | node-version: ${{ matrix.node-version }}
22 | cache: npm
23 | - run: npm ci
24 | - run: npm run lint
25 | - run: npm test
26 |
27 | publish-npm:
28 | needs: test
29 | runs-on: ubuntu-latest
30 | steps:
31 | - uses: actions/checkout@v3
32 | - uses: actions/setup-node@v2
33 | with:
34 | node-version: 18
35 | registry-url: https://registry.npmjs.org/
36 | cache: npm
37 | - run: npm ci
38 | - run: npm run build
39 | - run: npx semantic-release
40 | env:
41 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
42 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
43 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | pull_request:
5 | branches: [master, next]
6 |
7 | jobs:
8 | build-and-test:
9 | runs-on: ubuntu-latest
10 | strategy:
11 | matrix:
12 | node-version: [12.x, 14.x, 16.x, 18.x]
13 | steps:
14 | - uses: actions/checkout@v3
15 | - name: Use Node.js ${{ matrix.node-version }}
16 | uses: actions/setup-node@v2
17 | with:
18 | node-version: ${{ matrix.node-version }}
19 | cache: npm
20 | - run: npm ci
21 | - run: npm run lint
22 | - run: npm test
23 | - run: npm run build
24 | - name: Upload coverage to Codecov
25 | uses: codecov/codecov-action@v2
26 | size:
27 | runs-on: ubuntu-latest
28 | steps:
29 | - uses: actions/checkout@v3
30 | - uses: andresz1/size-limit-action@v1
31 | with:
32 | github_token: ${{ secrets.GITHUB_TOKEN }}
33 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [master, next]
6 | pull_request:
7 | # The branches below must be a subset of the branches above
8 | branches: [master]
9 | schedule:
10 | - cron: "32 10 * * 3"
11 |
12 | jobs:
13 | analyze:
14 | name: Analyze
15 | runs-on: ubuntu-latest
16 | permissions:
17 | actions: read
18 | contents: read
19 | security-events: write
20 |
21 | strategy:
22 | fail-fast: false
23 | matrix:
24 | language: ["javascript"]
25 |
26 | steps:
27 | - name: Checkout repository
28 | uses: actions/checkout@v3
29 |
30 | - name: Initialize CodeQL
31 | uses: github/codeql-action/init@v1
32 | with:
33 | languages: ${{ matrix.language }}
34 |
35 | - name: Perform CodeQL Analysis
36 | uses: github/codeql-action/analyze@v1
37 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: stale
2 |
3 | on:
4 | schedule:
5 | - cron: "30 1 * * MON"
6 |
7 | jobs:
8 | stale:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/stale@v3
12 | with:
13 | repo-token: ${{ secrets.GITHUB_TOKEN }}
14 | stale-issue-message: "This issue has been marked as stale."
15 | stale-pr-message: "This PR has been marked as stale."
16 | stale-issue-label: "no-issue-activity"
17 | stale-pr-label: "no-pr-activity"
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | node_modules
3 |
4 | # Builds
5 | dist
6 | types
7 | coverage
8 |
9 | # Misc
10 | .DS_Store
11 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | coverage
4 | README.md
5 |
--------------------------------------------------------------------------------
/.releaserc.json:
--------------------------------------------------------------------------------
1 | {
2 | "branches": [
3 | "master",
4 | { "name": "next", "channel": "next", "prerelease": "next" }
5 | ],
6 | "preset": "conventionalcommits",
7 | "plugins": [
8 | [
9 | "@semantic-release/commit-analyzer",
10 | {
11 | "releaseRules": [
12 | { "type": "docs", "release": "patch" },
13 | { "type": "refactor", "release": "patch" },
14 | { "type": "style", "release": "patch" },
15 | { "type": "test", "release": "patch" },
16 | { "type": "chore", "release": "patch" }
17 | ],
18 | "parserOpts": {
19 | "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES"]
20 | }
21 | }
22 | ],
23 | [
24 | "@semantic-release/release-notes-generator",
25 | {
26 | "presetConfig": {
27 | "types": [
28 | {
29 | "type": "feat",
30 | "section": "Features",
31 | "hidden": false
32 | },
33 | {
34 | "type": "fix",
35 | "section": "Bug Fixes",
36 | "hidden": false
37 | },
38 | {
39 | "type": "docs",
40 | "section": "Documentation",
41 | "hidden": false
42 | },
43 | {
44 | "type": "refactor",
45 | "section": "Code Refactors",
46 | "hidden": false
47 | },
48 | {
49 | "type": "perf",
50 | "section": "Performance",
51 | "hidden": false
52 | },
53 | {
54 | "type": "style",
55 | "section": "Styles",
56 | "hidden": false
57 | },
58 | {
59 | "type": "test",
60 | "section": "Tests",
61 | "hidden": false
62 | },
63 | {
64 | "type": "chore",
65 | "section": "Chores",
66 | "hidden": false
67 | }
68 | ]
69 | }
70 | }
71 | ],
72 | "@semantic-release/npm",
73 | "@semantic-release/github"
74 | ]
75 | }
76 |
--------------------------------------------------------------------------------
/.size-limit.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "JS Minified",
4 | "path": "dist/*.js",
5 | "gzip": false,
6 | "limit": "1608B"
7 | },
8 | {
9 | "name": "JS Compressed",
10 | "path": "dist/*.js",
11 | "limit": "750B"
12 | }
13 | ]
14 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Alex Nault
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # React Headings
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | > Never worry about using the wrong heading level (`h1`, `h2`, etc.) in complex React apps!
21 |
22 | React-headings maintains the proper hierarchy of headings for improved accessibility and SEO, no matter the component structure, while you keep full control of what's rendered.
23 |
24 | References:
25 |
26 | - [WCAG 2.0 technique H69](https://www.w3.org/TR/WCAG20-TECHS/H69.html)
27 | - [Lighthouse SEO heading order audit](https://web.dev/heading-order/)
28 |
29 | ## Table of contents
30 |
31 | - [Demos](#demos)
32 | - [Highlights](#highlights)
33 | - [Installation](#installation)
34 | - [Examples](#examples)
35 | - [API](#api)
36 | - [Changelog](#changelog)
37 | - [Contributing](#contributing)
38 |
39 | ## Demos
40 |
41 | - [Minimal](https://codesandbox.io/s/react-headings-minimal-4temt?file=/src/Demo.js)
42 | - [Custom component](https://codesandbox.io/s/react-headings-custom-component-l4bjb?file=/src/Demo.js)
43 | - [Advanced structure](https://codesandbox.io/s/react-headings-advanced-structure-uxk4p?file=/src/Demo.js)
44 |
45 | ## Highlights
46 |
47 | - Improves SEO and accessibility
48 | - Supports server-side rendering
49 | - Under 1 kB minified & gzipped
50 | - Typed with TypeScript
51 | - Fully tested
52 | - Works with any CSS solutions (Tailwind, CSS-in-JS, etc.)
53 | - Plays nicely with component libraries (Material UI, etc.)
54 | - Follows [semantic versioning](https://semver.org/)
55 |
56 | ## Installation
57 |
58 | ```bash
59 | npm install react-headings
60 | ```
61 |
62 | ## Examples
63 |
64 | ### Basic usage
65 |
66 | ```jsx
67 | import React from "react";
68 | import { H, Section } from "react-headings";
69 |
70 | function App() {
71 | return (
72 | My hx}>
73 | ...
74 | ...
75 | ...
76 | My hx+1}>
77 | ...
78 | ...
79 | ...
80 |
81 |
82 | );
83 | }
84 | ```
85 |
86 | ### Advanced structure
87 |
88 | Child components inherit the current level of their parent:
89 |
90 | ```jsx
91 | import React from "react";
92 | import { H, Section } from "react-headings";
93 |
94 | function ParentComponent() {
95 | return (
96 |
103 | );
104 | }
105 |
106 | function ChildComponent() {
107 | return (
108 | My hy}>
109 | {/* The following heading would be a in the current context */}
110 | My hy+1}>
111 | ...
112 |
113 |
114 | );
115 | }
116 | ```
117 |
118 | ### Styling
119 |
120 | A heading can be styled like any ordinary `` element since it accepts all the same props:
121 |
122 | ```jsx
123 | import React from "react";
124 | import { H, Section } from "react-headings";
125 |
126 | function App() {
127 | return (
128 |
131 | );
132 | }
133 | ```
134 |
135 | ### Custom heading
136 |
137 | A heading can be as complex as we want:
138 |
139 | ```jsx
140 | import React from "react";
141 | import { H, Section } from "react-headings";
142 | import MyIcon from "./MyIcon";
143 |
144 | function App() {
145 | return (
146 |
149 |
150 | My hx
151 |
152 | }
153 | >
154 | ...
155 | ...
156 | ...
157 |
158 | );
159 | }
160 | ```
161 |
162 | ### Using component libraries
163 |
164 | Leveraging `Component` and `level` from the context allows the use of component libraries.
165 | Here's an example with [Material UI](https://material-ui.com/api/typography/):
166 |
167 | ```jsx
168 | import React from "react";
169 | import { useLevel } from "react-headings";
170 | import { Typography } from "@material-ui/core";
171 |
172 | function MyHeading(props) {
173 | const { Component } = useLevel();
174 |
175 | return ;
176 | }
177 | ```
178 |
179 | ## API
180 |
181 | ### `` component
182 |
183 | Renders a ``, ``, ``, ``, `` or `` depending on the current level.
184 |
185 | #### Props
186 |
187 | | Name | Type | Required | Description |
188 | | ---------- | ---------- | -------- | --------------------------------------------------------------- |
189 | | `render` | `function` | No | Override with a custom heading. Has precedence over `children`. |
190 | | `children` | `node` | No | The content of the heading. Usually the title. |
191 |
192 | Any other props will be passed to the heading element.
193 |
194 | #### Example
195 |
196 | ```jsx
197 | import React from "react";
198 | import { H } from "react-headings";
199 |
200 | function Example1() {
201 | return This is my title;
202 | }
203 |
204 | function Example2() {
205 | return (
206 | My h{level}} />
207 | );
208 | }
209 | ```
210 |
211 | ### `` component
212 |
213 | Creates a new section (a heading and its level).
214 |
215 | #### Props
216 |
217 | | Name | Type | Required | Description |
218 | | ----------- | ------ | -------- | ------------------------------------------------------------------------------- |
219 | | `component` | `node` | Yes | The heading component. Can be anything but best used in combination with ``. |
220 | | `children` | `node` | No | The content of the new level. |
221 |
222 | #### Example
223 |
224 | ```jsx
225 | import React from "react";
226 | import { Section, H } from "react-headings";
227 |
228 | function Example1() {
229 | return (
230 | }>
231 | This is my content
232 |
233 | );
234 | }
235 |
236 | function Example2() {
237 | return (
238 |
241 |
242 | This is my title
243 |
244 |
245 | }
246 | >
247 | This is my content
248 |
249 | );
250 | }
251 | ```
252 |
253 | ### `useLevel` hook
254 |
255 | Returns an object containing the current `level` and current `Component`.
256 |
257 | #### Arguments
258 |
259 | None
260 |
261 | #### Returns
262 |
263 | | Name | Type | Description |
264 | | ----------- | -------------------------------------------------------- | ------------------------------------- |
265 | | `level` | `1` \| `2` \| `3` \| `4` \| `5` \| `6` | The current level. |
266 | | `Component` | `"h1"` \| `"h2"` \| `"h3"` \| `"h4"` \| `"h5"` \| `"h6"` | The current component. Same as level. |
267 |
268 | #### Example
269 |
270 | ```jsx
271 | import React from "react";
272 | import { useLevel } from "react-headings";
273 |
274 | function Example(props) {
275 | const { level, Component } = useLevel();
276 |
277 | return This is a h{level};
278 | }
279 | ```
280 |
281 | ## Changelog
282 |
283 | For a list of changes and releases, see the [changelog](https://github.com/alexnault/react-headings/releases).
284 |
285 | ## Contributing
286 |
287 | Found a bug, have a question or looking to improve react-headings? Open an [issue](https://github.com/alexnault/react-headings/issues/new), start a [discussion](https://github.com/alexnault/react-headings/discussions/new) or submit a [PR](https://github.com/alexnault/react-headings/fork)!
288 |
--------------------------------------------------------------------------------
/assets/react-headings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexnault/react-headings/5ab92f31c77769335d6323ada1dbb1d897750dcc/assets/react-headings.png
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | project:
4 | default:
5 | target: 100%
6 | threshold: 0%
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-headings",
3 | "version": "1.0.0-semantic-release",
4 | "description": "HTML headings with auto-incrementing levels for WCAG compliance.",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "author": "Alex Nault",
8 | "keywords": [
9 | "react",
10 | "heading",
11 | "auto",
12 | "level",
13 | "accessibility",
14 | "a11y",
15 | "wcag",
16 | "h1",
17 | "header",
18 | "title"
19 | ],
20 | "license": "MIT",
21 | "repository": "https://github.com/alexnault/react-headings",
22 | "homepage": "https://github.com/alexnault/react-headings#readme",
23 | "scripts": {
24 | "build": "rm -rf ./dist && tsc",
25 | "format": "prettier ./ --write",
26 | "lint": "eslint ./ --max-warnings=0",
27 | "size": "size-limit",
28 | "test": "vitest run --coverage"
29 | },
30 | "files": [
31 | "dist"
32 | ],
33 | "sideEffects": false,
34 | "peerDependencies": {
35 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
36 | },
37 | "devDependencies": {
38 | "@size-limit/preset-small-lib": "^7.0.8",
39 | "@testing-library/react": "^13.0.1",
40 | "@types/react": "^18.0.3",
41 | "@types/react-dom": "^18.0.0",
42 | "@typescript-eslint/eslint-plugin": "^4.33.0",
43 | "@typescript-eslint/parser": "^4.33.0",
44 | "@vitest/coverage-c8": "^0.30.1",
45 | "conventional-changelog-conventionalcommits": "^4.6.1",
46 | "eslint": "^7.32.0",
47 | "eslint-config-prettier": "^8.3.0",
48 | "jsdom": "^21.1.1",
49 | "prettier": "^2.7.1",
50 | "react": "^18.0.0",
51 | "react-dom": "^18.0.0",
52 | "semantic-release": "^21.0.1",
53 | "size-limit": "^7.0.8",
54 | "typescript": "^4.4.3",
55 | "vitest": "^0.30.1"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render, cleanup } from "@testing-library/react";
3 | import { describe, it, expect, afterEach } from "vitest";
4 |
5 | import { H, Section, useLevel } from "./index";
6 |
7 | afterEach(() => {
8 | cleanup();
9 | });
10 |
11 | describe("useLevel hook", () => {
12 | it("should be level 1 by default", () => {
13 | function MyComponent() {
14 | const { level, Component } = useLevel();
15 |
16 | expect(level).toBe(1);
17 | expect(Component).toBe("h1");
18 |
19 | return null;
20 | }
21 |
22 | render();
23 | });
24 |
25 | it("should be level 2 when 1 level down", () => {
26 | function MyComponent() {
27 | const { level, Component } = useLevel();
28 |
29 | expect(level).toBe(2);
30 | expect(Component).toBe("h2");
31 |
32 | return null;
33 | }
34 |
35 | render(
36 | }>
37 |
38 |
39 | );
40 | });
41 |
42 | it("should be level 6 when at level 7 or more", () => {
43 | function MyComponent() {
44 | const { level, Component } = useLevel();
45 |
46 | expect(level).toBe(6);
47 | expect(Component).toBe("h6");
48 |
49 | return null;
50 | }
51 |
52 | render(
53 |
}>
54 |
67 |
68 | );
69 | });
70 | });
71 |
72 | describe("H component", () => {
73 | it("should be level 1 by default", () => {
74 | const { getByText } = render(My H1);
75 |
76 | const headingEl = getByText("My H1");
77 |
78 | expect(headingEl.tagName).toBe("H1");
79 | });
80 |
81 | it("should forward HTML heading props", () => {
82 | const { getByText } = render(My H1);
83 |
84 | const headingEl = getByText("My H1");
85 |
86 | expect(headingEl.className).toBe("myClass");
87 | });
88 |
89 | it("should render a custom component", () => {
90 | const { getByText } = render( My span} />);
91 |
92 | const headingEl = getByText("My span");
93 |
94 | expect(headingEl.tagName).toBe("SPAN");
95 | });
96 |
97 | it("should render a custom component based on arguments", () => {
98 | const { getByText } = render(
99 | }>
100 | (
104 | My H{level}
105 | )}
106 | />
107 | }
108 | >
109 |
110 | );
111 |
112 | const headingEl = getByText("My H2");
113 |
114 | expect(headingEl.tagName).toBe("H2");
115 | });
116 |
117 | it("should forward ref", () => {
118 | const ref = React.createRef();
119 | const { container } = render(Heading);
120 | expect(ref.current).toBe(container.firstElementChild);
121 | });
122 | });
123 |
124 | describe("Section component", () => {
125 | it("should be level 1 in first section", () => {
126 | const { getByText } = render();
127 |
128 | const headingEl = getByText("My H1");
129 |
130 | expect(headingEl.tagName).toBe("H1");
131 | });
132 |
133 | it("should be level 2 in second section", () => {
134 | const { getByText } = render(
135 |
138 | );
139 |
140 | const headingEl = getByText("My H2");
141 |
142 | expect(headingEl.tagName).toBe("H2");
143 | });
144 |
145 | it("should be level 6 in 7th section", () => {
146 | const { getByText } = render(
147 |
160 | );
161 |
162 | const headingEl = getByText("My H6-2");
163 |
164 | expect(headingEl.tagName).toBe("H6");
165 | });
166 |
167 | it("should render a heading and its content", () => {
168 | const { getByText } = render(
169 | My H1}>
170 | My content
171 |
172 | );
173 |
174 | const headingEl = getByText("My H1");
175 |
176 | expect(headingEl.tagName).toBe("H1");
177 |
178 | const pEl = getByText("My content");
179 |
180 | expect(pEl.tagName).toBe("P");
181 | });
182 | });
183 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | type Level = 1 | 2 | 3 | 4 | 5 | 6;
4 |
5 | type Heading = `h${Level}`;
6 |
7 | type LevelContextValue = { level: Level; Component: Heading };
8 |
9 | const LevelContext = React.createContext({
10 | level: 1,
11 | Component: "h1",
12 | });
13 |
14 | /**
15 | * Returns the current heading and level.
16 | */
17 | export function useLevel(): LevelContextValue {
18 | return React.useContext(LevelContext);
19 | }
20 |
21 | type HProps = React.DetailedHTMLProps<
22 | React.HTMLAttributes,
23 | HTMLHeadingElement
24 | > & {
25 | render?: (context: LevelContextValue) => React.ReactElement;
26 | };
27 |
28 | function InternalH(
29 | { render, ...props }: HProps,
30 | forwardedRef: React.ForwardedRef
31 | ): JSX.Element {
32 | const context = useLevel();
33 |
34 | if (render) {
35 | return render(context);
36 | }
37 |
38 | return ;
39 | }
40 |
41 | /**
42 | * Renders a dynamic HTML heading (h1, h2, etc.) or custom component according to the current level.
43 | */
44 | export const H = React.forwardRef(InternalH);
45 |
46 | type SectionProps = {
47 | component: React.ReactNode;
48 | children?: React.ReactNode;
49 | };
50 |
51 | /**
52 | * Renders `component` in the current level and `children` in the next level.
53 | * @param component A component containing a heading
54 | * @param children The children in the next level
55 | */
56 | export function Section({ component, children }: SectionProps): JSX.Element {
57 | const { level } = useLevel();
58 |
59 | const nextLevel = Math.min(level + 1, 6) as Level;
60 |
61 | const value = {
62 | level: nextLevel,
63 | Component: `h${nextLevel}` as Heading,
64 | };
65 |
66 | return (
67 | <>
68 | {component}
69 | {children}
70 | >
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "isolatedModules": false,
5 | "jsx": "react",
6 | "lib": ["esnext", "dom"],
7 | "outDir": "dist",
8 | "target": "es5",
9 | "declaration": true,
10 | "noImplicitAny": true,
11 | "noImplicitReturns": true,
12 | "noImplicitThis": true,
13 | "noUnusedLocals": true,
14 | "noUnusedParameters": true,
15 | "skipLibCheck": true,
16 | "strict": true
17 | },
18 | "include": ["src/**/*"],
19 | "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
20 | }
21 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config";
2 |
3 | export default defineConfig({
4 | test: {
5 | environment: "jsdom",
6 | coverage: {
7 | provider: "c8",
8 | reporter: ["lcov", "text"],
9 | branches: 100,
10 | functions: 100,
11 | lines: 100,
12 | statements: 100,
13 | },
14 | },
15 | });
16 |
--------------------------------------------------------------------------------