react-headings 4.0.1 → 4.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.d.ts +18 -18
- package/dist/index.development.js +118 -86
- package/dist/index.js +44 -32
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Hardcoded heading levels break accessibility and SEO when components are reused.
|
|
|
18
18
|
## Features
|
|
19
19
|
|
|
20
20
|
- Supports server-side rendering
|
|
21
|
-
- Only
|
|
21
|
+
- Only 373 bytes minified & brotlied
|
|
22
22
|
- Typed with TypeScript
|
|
23
23
|
- Works with any CSS solution (Tailwind, CSS-in-JS, etc.)
|
|
24
24
|
- Plays nicely with component libraries
|
package/dist/index.d.ts
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ComponentPropsWithRef, ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
2
2
|
type Level = 1 | 2 | 3 | 4 | 5 | 6;
|
|
3
3
|
type Tag = `h${Level}`;
|
|
4
4
|
export type HeadingProps = ComponentPropsWithoutRef<"h1"> & {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
render?: (props: ComponentPropsWithRef<"h1">, heading: {
|
|
6
|
+
level: Level;
|
|
7
|
+
tag: Tag;
|
|
8
|
+
}) => ReactElement;
|
|
9
9
|
};
|
|
10
10
|
export type HeadingLevelProps = {
|
|
11
|
-
|
|
11
|
+
children?: ReactNode;
|
|
12
12
|
} & ({
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
offset?: number;
|
|
14
|
+
level?: never;
|
|
15
15
|
} | {
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
level: number;
|
|
17
|
+
offset?: never;
|
|
18
18
|
});
|
|
19
19
|
/**
|
|
20
20
|
* Renders an `h1`–`h6` according to the current level.
|
|
21
21
|
*/
|
|
22
22
|
export declare const Heading: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>, "ref"> & {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
render?: (props: ComponentPropsWithRef<"h1">, heading: {
|
|
24
|
+
level: Level;
|
|
25
|
+
tag: Tag;
|
|
26
|
+
}) => ReactElement;
|
|
27
27
|
} & import("react").RefAttributes<HTMLHeadingElement>>;
|
|
28
28
|
/**
|
|
29
29
|
* Renders headings inside `offset` levels deeper (default 1), or at `level`
|
|
30
30
|
* regardless of ancestors.
|
|
31
31
|
*/
|
|
32
|
-
export declare function HeadingLevel({ offset, level, children
|
|
32
|
+
export declare function HeadingLevel({ offset, level, children }: HeadingLevelProps): import("react").JSX.Element;
|
|
33
33
|
/**
|
|
34
34
|
* Returns the level and tag a `Heading` would render at this position.
|
|
35
35
|
*/
|
|
36
36
|
export declare function useHeadingLevel(): {
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
level: Level;
|
|
38
|
+
tag: Tag;
|
|
39
39
|
};
|
|
40
|
-
export {};
|
|
40
|
+
export {};
|
|
@@ -1,103 +1,135 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
|
|
3
|
-
// src/index.tsx
|
|
4
|
-
import {
|
|
5
|
-
createContext,
|
|
6
|
-
forwardRef,
|
|
7
|
-
useContext,
|
|
8
|
-
useEffect,
|
|
9
|
-
useMemo
|
|
10
|
-
} from "react";
|
|
2
|
+
import { createContext, forwardRef, useContext, useEffect, useMemo } from "react";
|
|
11
3
|
import { jsx } from "react/jsx-runtime";
|
|
12
|
-
|
|
4
|
+
// src/index.tsx
|
|
5
|
+
const LevelContext = createContext(1);
|
|
13
6
|
function isValidOffset(offset) {
|
|
14
|
-
|
|
7
|
+
return Number.isInteger(offset) && offset >= 0;
|
|
15
8
|
}
|
|
16
9
|
function isValidLevel(level) {
|
|
17
|
-
|
|
10
|
+
return Number.isInteger(level) && level >= 1;
|
|
18
11
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
);
|
|
12
|
+
/**
|
|
13
|
+
* Renders an `h1`–`h6` according to the current level.
|
|
14
|
+
*/
|
|
15
|
+
const Heading = forwardRef(function Heading({ render, ...props }, ref) {
|
|
16
|
+
const { level, tag } = useHeadingLevel();
|
|
17
|
+
useHeadingChecks();
|
|
18
|
+
if (render) return render({
|
|
19
|
+
...props,
|
|
20
|
+
ref
|
|
21
|
+
}, {
|
|
22
|
+
level,
|
|
23
|
+
tag
|
|
24
|
+
});
|
|
25
|
+
return /* @__PURE__ */ jsx(tag, {
|
|
26
|
+
ref,
|
|
27
|
+
...props
|
|
28
|
+
});
|
|
29
|
+
});
|
|
25
30
|
Heading.displayName = "Heading";
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Renders headings inside `offset` levels deeper (default 1), or at `level`
|
|
33
|
+
* regardless of ancestors.
|
|
34
|
+
*/
|
|
35
|
+
function HeadingLevel({ offset = 1, level, children }) {
|
|
36
|
+
const parentLevel = useContext(LevelContext);
|
|
37
|
+
const isReset = isValidLevel(level);
|
|
38
|
+
const isOffsetValid = isValidOffset(offset);
|
|
39
|
+
const innerLevel = isReset ? level : parentLevel + (isOffsetValid ? offset : 1);
|
|
40
|
+
useInvalidPropWarning("offset", offset, isOffsetValid);
|
|
41
|
+
useInvalidPropWarning("level", level, isReset);
|
|
42
|
+
children = /* @__PURE__ */ jsx(SkippedLevelScope, {
|
|
43
|
+
firstLevel: isReset ? innerLevel : parentLevel + 1,
|
|
44
|
+
isIsolated: isReset,
|
|
45
|
+
isPassthrough: !isReset && offset === 0,
|
|
46
|
+
children
|
|
47
|
+
});
|
|
48
|
+
return /* @__PURE__ */ jsx(LevelContext.Provider, {
|
|
49
|
+
value: innerLevel,
|
|
50
|
+
children
|
|
51
|
+
});
|
|
41
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Returns the level and tag a `Heading` would render at this position.
|
|
55
|
+
*/
|
|
42
56
|
function useHeadingLevel() {
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
const level = Math.min(useContext(LevelContext), 6);
|
|
58
|
+
return {
|
|
59
|
+
level,
|
|
60
|
+
tag: `h${level}`
|
|
61
|
+
};
|
|
45
62
|
}
|
|
46
|
-
|
|
63
|
+
const ScopeContext = /* @__PURE__ */ createContext(null);
|
|
64
|
+
/**
|
|
65
|
+
* Collects the heading levels rendered inside a HeadingLevel and warns if one between `firstLevel` and the deepest is missing. A
|
|
66
|
+
* non-empty nested scope counts as a heading at its own `firstLevel` in its
|
|
67
|
+
* parent, so each gap is reported by exactly one scope.
|
|
68
|
+
*/
|
|
47
69
|
function createScopeRegistry(firstLevel, parentRegistry) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
const headingLevels = /* @__PURE__ */ new Map();
|
|
71
|
+
let unregisterFromParent;
|
|
72
|
+
let isCheckScheduled = false;
|
|
73
|
+
let hasWarned = false;
|
|
74
|
+
function warnIfLevelSkipped() {
|
|
75
|
+
isCheckScheduled = false;
|
|
76
|
+
if (hasWarned || headingLevels.size === 0) return;
|
|
77
|
+
const presentLevels = new Set(headingLevels.values());
|
|
78
|
+
const deepestLevel = Math.max(...presentLevels);
|
|
79
|
+
for (let level = firstLevel; level < deepestLevel; level++) if (!presentLevels.has(level)) {
|
|
80
|
+
hasWarned = true;
|
|
81
|
+
console.warn(`react-headings: Skipped heading level ${level}: found a level ${deepestLevel} heading but no level ${level} heading.`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function scheduleCheck() {
|
|
86
|
+
if (isCheckScheduled) return;
|
|
87
|
+
isCheckScheduled = true;
|
|
88
|
+
queueMicrotask(warnIfLevelSkipped);
|
|
89
|
+
}
|
|
90
|
+
return { register(level) {
|
|
91
|
+
const token = {};
|
|
92
|
+
headingLevels.set(token, level);
|
|
93
|
+
if (headingLevels.size === 1) unregisterFromParent = parentRegistry?.register(firstLevel);
|
|
94
|
+
scheduleCheck();
|
|
95
|
+
return () => {
|
|
96
|
+
headingLevels.delete(token);
|
|
97
|
+
if (headingLevels.size === 0) {
|
|
98
|
+
unregisterFromParent?.();
|
|
99
|
+
unregisterFromParent = void 0;
|
|
100
|
+
}
|
|
101
|
+
scheduleCheck();
|
|
102
|
+
};
|
|
103
|
+
} };
|
|
71
104
|
}
|
|
72
|
-
function SkippedLevelScope({
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
105
|
+
function SkippedLevelScope({ firstLevel, isIsolated, isPassthrough, children }) {
|
|
106
|
+
const parentRegistry = useContext(ScopeContext);
|
|
107
|
+
const registry = useMemo(() => isPassthrough ? parentRegistry : createScopeRegistry(firstLevel, isIsolated ? null : parentRegistry), [
|
|
108
|
+
firstLevel,
|
|
109
|
+
isIsolated,
|
|
110
|
+
isPassthrough,
|
|
111
|
+
parentRegistry
|
|
112
|
+
]);
|
|
113
|
+
return /* @__PURE__ */ jsx(ScopeContext.Provider, {
|
|
114
|
+
value: registry,
|
|
115
|
+
children
|
|
116
|
+
});
|
|
83
117
|
}
|
|
84
118
|
function useHeadingChecks() {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
119
|
+
const level = useContext(LevelContext);
|
|
120
|
+
const scopeRegistry = useContext(ScopeContext);
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (level > 6) console.warn(`react-headings: Heading level ${level} exceeds 6 and was clamped to h6.`);
|
|
123
|
+
}, [level]);
|
|
124
|
+
useEffect(() => scopeRegistry?.register(level), [scopeRegistry, level]);
|
|
91
125
|
}
|
|
92
126
|
function useInvalidPropWarning(prop, value, isValid) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (value != null && !isValid) console.warn(`react-headings: HeadingLevel received invalid ${prop} ${value}, ignored.`);
|
|
129
|
+
}, [
|
|
130
|
+
prop,
|
|
131
|
+
value,
|
|
132
|
+
isValid
|
|
133
|
+
]);
|
|
98
134
|
}
|
|
99
|
-
export {
|
|
100
|
-
Heading,
|
|
101
|
-
HeadingLevel,
|
|
102
|
-
useHeadingLevel
|
|
103
|
-
};
|
|
135
|
+
export { Heading, HeadingLevel, useHeadingLevel };
|
package/dist/index.js
CHANGED
|
@@ -1,42 +1,54 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
|
|
3
|
-
// src/index.tsx
|
|
4
|
-
import {
|
|
5
|
-
createContext,
|
|
6
|
-
forwardRef,
|
|
7
|
-
useContext,
|
|
8
|
-
useEffect,
|
|
9
|
-
useMemo
|
|
10
|
-
} from "react";
|
|
2
|
+
import { createContext, forwardRef, useContext } from "react";
|
|
11
3
|
import { jsx } from "react/jsx-runtime";
|
|
12
|
-
|
|
4
|
+
// src/index.tsx
|
|
5
|
+
const LevelContext = createContext(1);
|
|
13
6
|
function isValidOffset(offset) {
|
|
14
|
-
|
|
7
|
+
return Number.isInteger(offset) && offset >= 0;
|
|
15
8
|
}
|
|
16
9
|
function isValidLevel(level) {
|
|
17
|
-
|
|
10
|
+
return Number.isInteger(level) && level >= 1;
|
|
18
11
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
)
|
|
12
|
+
/**
|
|
13
|
+
* Renders an `h1`–`h6` according to the current level.
|
|
14
|
+
*/
|
|
15
|
+
const Heading = forwardRef(function Heading({ render, ...props }, ref) {
|
|
16
|
+
const { level, tag } = useHeadingLevel();
|
|
17
|
+
if (render) return render({
|
|
18
|
+
...props,
|
|
19
|
+
ref
|
|
20
|
+
}, {
|
|
21
|
+
level,
|
|
22
|
+
tag
|
|
23
|
+
});
|
|
24
|
+
return /* @__PURE__ */ jsx(tag, {
|
|
25
|
+
ref,
|
|
26
|
+
...props
|
|
27
|
+
});
|
|
28
|
+
});
|
|
25
29
|
Heading.displayName = "Heading";
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}) {
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Renders headings inside `offset` levels deeper (default 1), or at `level`
|
|
32
|
+
* regardless of ancestors.
|
|
33
|
+
*/
|
|
34
|
+
function HeadingLevel({ offset = 1, level, children }) {
|
|
35
|
+
const parentLevel = useContext(LevelContext);
|
|
36
|
+
const isReset = isValidLevel(level);
|
|
37
|
+
const isOffsetValid = isValidOffset(offset);
|
|
38
|
+
const innerLevel = isReset ? level : parentLevel + (isOffsetValid ? offset : 1);
|
|
39
|
+
return /* @__PURE__ */ jsx(LevelContext.Provider, {
|
|
40
|
+
value: innerLevel,
|
|
41
|
+
children
|
|
42
|
+
});
|
|
33
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Returns the level and tag a `Heading` would render at this position.
|
|
46
|
+
*/
|
|
34
47
|
function useHeadingLevel() {
|
|
35
|
-
|
|
36
|
-
|
|
48
|
+
const level = Math.min(useContext(LevelContext), 6);
|
|
49
|
+
return {
|
|
50
|
+
level,
|
|
51
|
+
tag: `h${level}`
|
|
52
|
+
};
|
|
37
53
|
}
|
|
38
|
-
export {
|
|
39
|
-
Heading,
|
|
40
|
-
HeadingLevel,
|
|
41
|
-
useHeadingLevel
|
|
42
|
-
};
|
|
54
|
+
export { Heading, HeadingLevel, useHeadingLevel };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-headings",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Dynamic heading levels for React.",
|
|
6
6
|
"author": "Alex Nault",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"./package.json": "./package.json"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
|
-
"build": "
|
|
32
|
+
"build": "tsdown",
|
|
33
33
|
"typecheck": "tsc",
|
|
34
34
|
"format": "prettier ./ --write",
|
|
35
35
|
"lint": "eslint ./ --max-warnings=0",
|
|
@@ -48,21 +48,22 @@
|
|
|
48
48
|
"@size-limit/preset-small-lib": "^14.3.0",
|
|
49
49
|
"@testing-library/dom": "^10.4.2",
|
|
50
50
|
"@testing-library/react": "^16.3.3",
|
|
51
|
+
"@types/node": "^24.19.2",
|
|
51
52
|
"@types/react": "^19.3.0",
|
|
52
53
|
"@types/react-dom": "^19.3.0",
|
|
53
|
-
"@vitest/coverage-v8": "^5.0
|
|
54
|
+
"@vitest/coverage-v8": "^5.1.0",
|
|
54
55
|
"conventional-changelog-conventionalcommits": "^9.3.1",
|
|
55
|
-
"esbuild": "^0.28.2",
|
|
56
56
|
"eslint": "^10.14.0",
|
|
57
57
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
58
58
|
"jsdom": "^30.1.3",
|
|
59
59
|
"prettier": "^3.9.10",
|
|
60
60
|
"react": "^19.3.1",
|
|
61
61
|
"react-dom": "^19.3.1",
|
|
62
|
-
"semantic-release": "^25.0.
|
|
62
|
+
"semantic-release": "^25.0.9",
|
|
63
63
|
"size-limit": "^14.3.0",
|
|
64
|
-
"
|
|
64
|
+
"tsdown": "^0.23.5",
|
|
65
|
+
"typescript": "^6.0.3",
|
|
65
66
|
"typescript-eslint": "^8.72.1",
|
|
66
|
-
"vitest": "^5.0
|
|
67
|
+
"vitest": "^5.1.0"
|
|
67
68
|
}
|
|
68
69
|
}
|