Skip to content

Commit 0304c26

Browse files
committed
Include page groups in page TOC
Resolves #2616
1 parent 9f0fb04 commit 0304c26

File tree

9 files changed

+141
-91
lines changed

9 files changed

+141
-91
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Unreleased
22

3+
### Features
4+
5+
- "On This Page" navigation now includes the page groups in collapsible sections, #2616.
6+
37
### Bug Fixes
48

59
- `mailto:` links are no longer incorrectly recognized as relative paths, #2613.

src/lib/output/events.ts

+31-7
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ export class RendererEvent {
6969
}
7070
}
7171

72+
export interface PageHeading {
73+
link: string;
74+
text: string;
75+
level?: number;
76+
kind?: ReflectionKind;
77+
classes?: string;
78+
}
79+
7280
/**
7381
* An event emitted by the {@link Renderer} class before and after the
7482
* markup of a page is rendered.
@@ -108,13 +116,29 @@ export class PageEvent<out Model = unknown> extends Event {
108116
* Links to content within this page that should be rendered in the page navigation.
109117
* This is built when rendering the document content.
110118
*/
111-
pageHeadings: Array<{
112-
link: string;
113-
text: string;
114-
level?: number;
115-
kind?: ReflectionKind;
116-
classes?: string;
117-
}> = [];
119+
pageHeadings: PageHeading[] = [];
120+
121+
/**
122+
* Sections of the page, generally set by `@group`s
123+
*/
124+
pageSections = [
125+
{
126+
title: "",
127+
headings: this.pageHeadings,
128+
},
129+
];
130+
131+
/**
132+
* Start a new section of the page. Sections are collapsible within
133+
* the "On This Page" sidebar.
134+
*/
135+
startNewSection(title: string) {
136+
this.pageHeadings = [];
137+
this.pageSections.push({
138+
title,
139+
headings: this.pageHeadings,
140+
});
141+
}
118142

119143
/**
120144
* Triggered before a document will be rendered.

src/lib/output/themes/default/DefaultThemeRenderContext.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import { memberSignatureTitle } from "./partials/member.signature.title";
3232
import { memberSignatures } from "./partials/member.signatures";
3333
import { memberSources } from "./partials/member.sources";
3434
import { members } from "./partials/members";
35-
import { membersGroup } from "./partials/members.group";
3635
import {
3736
sidebar,
3837
pageSidebar,
@@ -63,7 +62,7 @@ export class DefaultThemeRenderContext {
6362
i18n: TranslationProxy;
6463

6564
constructor(
66-
private theme: DefaultTheme,
65+
readonly theme: DefaultTheme,
6766
public page: PageEvent<Reflection>,
6867
options: Options,
6968
) {
@@ -144,7 +143,8 @@ export class DefaultThemeRenderContext {
144143
memberSignatures = bind(memberSignatures, this);
145144
memberSources = bind(memberSources, this);
146145
members = bind(members, this);
147-
membersGroup = bind(membersGroup, this);
146+
/** @deprecated Since 0.26.3 members does group/category flattening internally */
147+
membersGroup?: Function;
148148
sidebar = bind(sidebar, this);
149149
pageSidebar = bind(pageSidebar, this);
150150
sidebarLinks = bind(sidebarLinks, this);

src/lib/output/themes/default/partials/members.group.tsx

-39
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,61 @@
11
import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext";
2-
import { JSX } from "../../../../utils";
3-
import { type ContainerReflection, DeclarationReflection } from "../../../../models";
4-
import { classNames } from "../../lib";
2+
import { filterMap, JSX } from "../../../../utils";
3+
import type { ContainerReflection } from "../../../../models";
54

6-
export function members(context: DefaultThemeRenderContext, props: ContainerReflection) {
7-
if (props.categories && props.categories.length) {
8-
return (
9-
<>
10-
{props.categories.map(
11-
(item) =>
12-
!item.allChildrenHaveOwnDocument() && (
13-
<details
14-
class={classNames(
15-
{ "tsd-panel-group": true, "tsd-member-group": true, "tsd-accordion": true },
16-
props instanceof DeclarationReflection ? context.getReflectionClasses(props) : "",
17-
)}
18-
>
19-
<summary class="tsd-accordion-summary" data-key={"section-" + item.title}>
20-
<h2>
21-
{context.icons.chevronDown()} {item.title}
22-
</h2>
23-
</summary>
24-
<section>
25-
{item.children.map((item) => !item.hasOwnDocument && context.member(item))}
26-
</section>
27-
</details>
28-
),
29-
)}
30-
</>
31-
);
5+
function getMemberSections(parent: ContainerReflection) {
6+
if (parent.categories?.length) {
7+
return filterMap(parent.categories, (cat) => {
8+
if (!cat.allChildrenHaveOwnDocument()) {
9+
return {
10+
title: cat.title,
11+
children: cat.children.filter((child) => !child.hasOwnDocument),
12+
};
13+
}
14+
});
3215
}
3316

34-
return <>{props.groups?.map((item) => !item.allChildrenHaveOwnDocument() && context.membersGroup(item))}</>;
17+
if (parent.groups?.length) {
18+
return parent.groups.flatMap((group) => {
19+
if (group.categories?.length) {
20+
return filterMap(group.categories, (cat) => {
21+
if (!cat.allChildrenHaveOwnDocument()) {
22+
return {
23+
title: `${group.title} - ${cat.title}`,
24+
children: cat.children.filter((child) => !child.hasOwnDocument),
25+
};
26+
}
27+
});
28+
}
29+
30+
return {
31+
title: group.title,
32+
children: group.children.filter((child) => !child.hasOwnDocument),
33+
};
34+
});
35+
}
36+
37+
return [];
38+
}
39+
40+
export function members(context: DefaultThemeRenderContext, props: ContainerReflection) {
41+
const sections = getMemberSections(props).filter((sect) => sect.children.length);
42+
43+
return (
44+
<>
45+
{sections.map(({ title, children }) => {
46+
context.page.startNewSection(title);
47+
48+
return (
49+
<details class="tsd-panel-group tsd-member-group tsd-accordion" open>
50+
<summary class="tsd-accordion-summary" data-key={"section-" + title}>
51+
<h2>
52+
{context.icons.chevronDown()} {title}
53+
</h2>
54+
</summary>
55+
<section>{children.map((item) => context.member(item))}</section>
56+
</details>
57+
);
58+
})}
59+
</>
60+
);
3561
}

src/lib/output/themes/default/partials/navigation.tsx

+33-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type Reflection, ReflectionFlag, ReflectionKind } from "../../../../models";
22
import { JSX } from "../../../../utils";
3-
import type { PageEvent } from "../../../events";
3+
import type { PageEvent, PageHeading } from "../../../events";
44
import { classNames, getDisplayName, wbr } from "../../lib";
55
import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext";
66

@@ -145,7 +145,7 @@ export function pageSidebar(context: DefaultThemeRenderContext, props: PageEvent
145145
);
146146
}
147147

148-
export function pageNavigation(context: DefaultThemeRenderContext, props: PageEvent<Reflection>) {
148+
function buildSectionNavigation(context: DefaultThemeRenderContext, headings: PageHeading[]) {
149149
const levels: JSX.Element[][] = [[]];
150150

151151
function finalizeLevel(finishedHandlingHeadings: boolean) {
@@ -165,8 +165,12 @@ export function pageNavigation(context: DefaultThemeRenderContext, props: PageEv
165165
levels[levels.length - 1].push(built);
166166
}
167167

168-
for (const heading of props.pageHeadings) {
169-
const inferredLevel = heading.level ? heading.level + 1 : 1;
168+
for (const heading of headings) {
169+
const inferredLevel = heading.level
170+
? heading.level + 2 // regular heading
171+
: heading.kind
172+
? 2 // reflection
173+
: 1; // group/category
170174
while (inferredLevel < levels.length) {
171175
finalizeLevel(false);
172176
}
@@ -187,12 +191,33 @@ export function pageNavigation(context: DefaultThemeRenderContext, props: PageEv
187191
finalizeLevel(true);
188192
}
189193

190-
if (!levels[0].length) {
194+
levels.unshift([]);
195+
finalizeLevel(true);
196+
return levels[0];
197+
}
198+
199+
export function pageNavigation(context: DefaultThemeRenderContext, props: PageEvent<Reflection>) {
200+
if (!props.pageSections.some((sect) => sect.headings.length)) {
191201
return <></>;
192202
}
193203

194-
levels.unshift([]);
195-
finalizeLevel(true);
204+
const sections: JSX.Children = [];
205+
206+
for (const section of props.pageSections) {
207+
if (section.title) {
208+
sections.push(
209+
<details open class="tsd-accordion tsd-page-navigation-section">
210+
<summary class="tsd-accordion-summary" data-key={`tsd-otp-${section.title}`}>
211+
{context.icons.chevronDown()}
212+
{section.title}
213+
</summary>
214+
<div>{buildSectionNavigation(context, section.headings)}</div>
215+
</details>,
216+
);
217+
} else {
218+
sections.push(buildSectionNavigation(context, section.headings));
219+
}
220+
}
196221

197222
return (
198223
<details open={true} class="tsd-accordion tsd-page-navigation">
@@ -202,7 +227,7 @@ export function pageNavigation(context: DefaultThemeRenderContext, props: PageEv
202227
{context.i18n.theme_on_this_page()}
203228
</h3>
204229
</summary>
205-
<div class="tsd-accordion-details">{levels[0]}</div>
230+
<div class="tsd-accordion-details">{sections}</div>
206231
</details>
207232
);
208233
}

src/lib/utils/enum.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ export function getEnumKeys(Enum: {}): string[] {
3434
return Object.keys(E).filter((k) => E[E[k]] === k);
3535
}
3636

37-
export type EnumKeys<E extends {}> = keyof {
38-
[K in keyof E as number extends E[K] ? K : never]: 1;
39-
};
37+
export type EnumKeys<E extends {}> = {
38+
[K in keyof E]: number extends E[K] ? K : never;
39+
}[keyof E] & {};

src/lib/utils/options/declaration.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export interface TypeDocOptionMap {
110110
externalPattern: string[];
111111
excludeExternals: boolean;
112112
excludeNotDocumented: boolean;
113-
excludeNotDocumentedKinds: Array<keyof typeof ReflectionKind>;
113+
excludeNotDocumentedKinds: ReflectionKind.KindString[];
114114
excludeInternal: boolean;
115115
excludePrivate: boolean;
116116
excludeProtected: boolean;

static/style.css

+10
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,15 @@ input[type="checkbox"]:checked ~ svg .tsd-checkbox-checkmark {
793793
margin-left: -1.5rem;
794794
}
795795

796+
.tsd-page-navigation-section {
797+
margin-left: 10px;
798+
}
799+
.tsd-page-navigation-section > summary {
800+
padding: 0.25rem;
801+
}
802+
.tsd-page-navigation-section > div {
803+
margin-left: 20px;
804+
}
796805
.tsd-page-navigation ul {
797806
padding-left: 1.75rem;
798807
}
@@ -841,6 +850,7 @@ a.tsd-index-link {
841850
}
842851
.tsd-accordion .tsd-accordion-summary > svg {
843852
margin-left: 0.25rem;
853+
vertical-align: text-top;
844854
}
845855
.tsd-index-content > :not(:first-child) {
846856
margin-top: 0.75rem;

0 commit comments

Comments
 (0)