mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(site): clean up api reference header hierarchy
I'm ok with H4s now
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
import type { MarkdownHeading } from 'astro';
|
||||
import { TableOfContentsDesktop } from './TableOfContents.desktop';
|
||||
import { TableOfContentsMobile } from './TableOfContents.mobile';
|
||||
import { filterHeadingsByDepth, navigateToHeading, useActiveHeading } from './utils';
|
||||
import { filterHeadingsForToc, navigateToHeading, useActiveHeading } from './utils';
|
||||
|
||||
interface TableOfContentsProps {
|
||||
headings: MarkdownHeading[];
|
||||
}
|
||||
|
||||
export function TableOfContents({ headings }: TableOfContentsProps) {
|
||||
const filteredHeadings = filterHeadingsByDepth(headings, 2, 3);
|
||||
const filteredHeadings = filterHeadingsForToc(headings);
|
||||
const activeId = useActiveHeading(filteredHeadings);
|
||||
|
||||
if (filteredHeadings.length === 0) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import debounce from 'just-debounce-it';
|
||||
import throttle from 'just-throttle';
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { API_REFERENCE_SUBSECTION_TITLES } from '@/utils/apiReferenceModel';
|
||||
|
||||
/**
|
||||
* Find the first scrollable ancestor of an element
|
||||
@@ -39,14 +40,28 @@ export function isElementOffscreen(element: HTMLElement, container: HTMLElement)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter headings by depth range
|
||||
* Include headings for docs TOC, including API-reference subsection H4s only.
|
||||
*/
|
||||
export function filterHeadingsByDepth(
|
||||
headings: MarkdownHeading[],
|
||||
minDepth: number,
|
||||
maxDepth: number
|
||||
): MarkdownHeading[] {
|
||||
return headings.filter((h) => h.depth >= minDepth && h.depth <= maxDepth);
|
||||
export function filterHeadingsForToc(headings: MarkdownHeading[]): MarkdownHeading[] {
|
||||
const apiReferenceSubsectionTitles = new Set(API_REFERENCE_SUBSECTION_TITLES);
|
||||
const isTocHeadingDepth = (depth: number): boolean => depth === 2 || depth === 3;
|
||||
const isApiReferenceSubsectionHeading = (heading: MarkdownHeading): boolean => {
|
||||
const tocKind = (heading as MarkdownHeading & { tocKind?: string }).tocKind;
|
||||
|
||||
return tocKind === 'api-reference-subsection' && apiReferenceSubsectionTitles.has(heading.text);
|
||||
};
|
||||
|
||||
return headings.filter((heading) => {
|
||||
if (isTocHeadingDepth(heading.depth)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (heading.depth === 4) {
|
||||
return isApiReferenceSubsectionHeading(heading);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,10 +4,12 @@ import { kebabCase } from 'es-toolkit/string';
|
||||
import ContentWidth from '@/components/frames/ContentWidth.astro';
|
||||
import H2 from '@/components/typography/H2Markdown.astro';
|
||||
import H3 from '@/components/typography/H3Markdown.astro';
|
||||
import H4 from '@/components/typography/H4Markdown.astro';
|
||||
import MarkdownCode from '@/components/typography/MarkdownCode.astro';
|
||||
import P from '@/components/typography/P.astro';
|
||||
import type { ComponentApiReference } from '@/types/api-reference';
|
||||
import { isValidFramework } from '@/types/docs';
|
||||
import { createApiReferenceModel } from '@/utils/apiReferenceModel';
|
||||
import FrameworkCase from '../FrameworkCase.astro';
|
||||
import ApiDataAttrsTable from './ApiDataAttrsTable.astro';
|
||||
import ApiPropsTable from './ApiPropsTable.astro';
|
||||
@@ -28,47 +30,50 @@ const entry = await getEntry('apiReference', kebabCase(component));
|
||||
const apiRef: ComponentApiReference | null = entry?.data ?? null;
|
||||
if (!apiRef) return;
|
||||
|
||||
const hasParts = apiRef.parts && Object.keys(apiRef.parts).length > 0;
|
||||
const apiReferenceModel = createApiReferenceModel(component, apiRef);
|
||||
if (!apiReferenceModel) return;
|
||||
|
||||
const hasProps = Object.keys(apiRef.props).length > 0;
|
||||
const hasState = Object.keys(apiRef.state).length > 0;
|
||||
const hasDataAttrs = Object.keys(apiRef.dataAttributes).length > 0;
|
||||
const singlePropsSection = !apiReferenceModel.hasParts
|
||||
? apiReferenceModel.sections.find((section) => section.key === 'props')
|
||||
: null;
|
||||
const singleStateSection = !apiReferenceModel.hasParts
|
||||
? apiReferenceModel.sections.find((section) => section.key === 'state')
|
||||
: null;
|
||||
const singleDataAttributesSection = !apiReferenceModel.hasParts
|
||||
? apiReferenceModel.sections.find((section) => section.key === 'dataAttributes')
|
||||
: null;
|
||||
---
|
||||
|
||||
{hasParts ? (
|
||||
<ContentWidth>
|
||||
{Object.entries(apiRef.parts!).map(([partKebab, part]) => {
|
||||
const tagName = part.platforms?.html?.tagName;
|
||||
const partHasProps = Object.keys(part.props).length > 0;
|
||||
const partHasState = Object.keys(part.state).length > 0;
|
||||
const partHasDataAttrs = Object.keys(part.dataAttributes).length > 0;
|
||||
const componentName = `${component}.${part.name}`;
|
||||
<ContentWidth>
|
||||
<H2 id={apiReferenceModel.heading.id}>{apiReferenceModel.heading.text}</H2>
|
||||
|
||||
{apiReferenceModel.hasParts ? (
|
||||
apiReferenceModel.parts.map((part) => {
|
||||
const partPropsSection = part.sections.find((section) => section.key === 'props');
|
||||
const partStateSection = part.sections.find((section) => section.key === 'state');
|
||||
const partDataAttributesSection = part.sections.find((section) => section.key === 'dataAttributes');
|
||||
|
||||
return (
|
||||
<>
|
||||
<H2 id={partKebab}>
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
<MarkdownCode>{`<${component}.${part.name} />`}</MarkdownCode> reference
|
||||
</FrameworkCase>
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
<MarkdownCode>{tagName ? `<${tagName}>` : part.name}</MarkdownCode> reference
|
||||
</FrameworkCase>
|
||||
</H2>
|
||||
<H3 id={part.id}>
|
||||
<FrameworkCase frameworks={["react"]}>{part.labelByFramework.react}</FrameworkCase>
|
||||
<FrameworkCase frameworks={["html"]}>{part.labelByFramework.html}</FrameworkCase>
|
||||
</H3>
|
||||
|
||||
{part.description && (
|
||||
<P><InlineMarkdown content={part.description} /></P>
|
||||
)}
|
||||
|
||||
{partHasProps && (
|
||||
{partPropsSection && (
|
||||
<>
|
||||
<H3 id={`${partKebab}-props`}>Props</H3>
|
||||
<ApiPropsTable props={part.props} componentName={componentName} />
|
||||
<H4 id={partPropsSection.id}>{partPropsSection.title}</H4>
|
||||
<ApiPropsTable props={part.data.props} componentName={part.componentName} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{partHasState && (
|
||||
{partStateSection && (
|
||||
<>
|
||||
<H3 id={`${partKebab}-state`}>State</H3>
|
||||
<H4 id={partStateSection.id}>{partStateSection.title}</H4>
|
||||
<P>
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
State is accessible via the{" "}
|
||||
@@ -80,54 +85,52 @@ const hasDataAttrs = Object.keys(apiRef.dataAttributes).length > 0;
|
||||
State is reflected as data attributes for CSS styling.
|
||||
</FrameworkCase>
|
||||
</P>
|
||||
<ApiStateTable state={part.state} componentName={componentName} />
|
||||
<ApiStateTable state={part.data.state} componentName={part.componentName} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{partHasDataAttrs && (
|
||||
{partDataAttributesSection && (
|
||||
<>
|
||||
<H3 id={`${partKebab}-data-attributes`}>Data attributes</H3>
|
||||
<ApiDataAttrsTable dataAttributes={part.dataAttributes} />
|
||||
<H4 id={partDataAttributesSection.id}>{partDataAttributesSection.title}</H4>
|
||||
<ApiDataAttrsTable dataAttributes={part.data.dataAttributes} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</ContentWidth>
|
||||
) : (
|
||||
<ContentWidth>
|
||||
<H2 id="api-reference">API reference</H2>
|
||||
})
|
||||
) : (
|
||||
<>
|
||||
{singlePropsSection && (
|
||||
<>
|
||||
<H3 id={singlePropsSection.id}>{singlePropsSection.title}</H3>
|
||||
<ApiPropsTable props={apiReferenceModel.data.props} componentName={component} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasProps && (
|
||||
<>
|
||||
<H3 id="props">Props</H3>
|
||||
<ApiPropsTable props={apiRef.props} componentName={component} />
|
||||
</>
|
||||
)}
|
||||
{singleStateSection && (
|
||||
<>
|
||||
<H3 id={singleStateSection.id}>{singleStateSection.title}</H3>
|
||||
<P>
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
State is accessible via the{" "}
|
||||
<MarkdownCode>render</MarkdownCode>,{" "}
|
||||
<MarkdownCode>className</MarkdownCode>, and{" "}
|
||||
<MarkdownCode>style</MarkdownCode> props.
|
||||
</FrameworkCase>
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
State is reflected as data attributes for CSS styling.
|
||||
</FrameworkCase>
|
||||
</P>
|
||||
<ApiStateTable state={apiReferenceModel.data.state} componentName={component} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasState && (
|
||||
<>
|
||||
<H3 id="state">State</H3>
|
||||
<P>
|
||||
<FrameworkCase frameworks={["react"]}>
|
||||
State is accessible via the{" "}
|
||||
<MarkdownCode>render</MarkdownCode>,{" "}
|
||||
<MarkdownCode>className</MarkdownCode>, and{" "}
|
||||
<MarkdownCode>style</MarkdownCode> props.
|
||||
</FrameworkCase>
|
||||
<FrameworkCase frameworks={["html"]}>
|
||||
State is reflected as data attributes for CSS styling.
|
||||
</FrameworkCase>
|
||||
</P>
|
||||
<ApiStateTable state={apiRef.state} componentName={component} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasDataAttrs && (
|
||||
<>
|
||||
<H3 id="data-attributes">Data attributes</H3>
|
||||
<ApiDataAttrsTable dataAttributes={apiRef.dataAttributes} />
|
||||
</>
|
||||
)}
|
||||
</ContentWidth>
|
||||
)}
|
||||
{singleDataAttributesSection && (
|
||||
<>
|
||||
<H3 id={singleDataAttributesSection.id}>{singleDataAttributesSection.title}</H3>
|
||||
<ApiDataAttrsTable dataAttributes={apiReferenceModel.data.dataAttributes} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ContentWidth>
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Centralized API subsection definitions.
|
||||
*
|
||||
* Why this exists:
|
||||
* API reference headings are produced in two different places:
|
||||
* 1) rendered markup in ApiReference.astro
|
||||
* 2) synthetic TOC metadata in remarkConditionalHeadings
|
||||
*
|
||||
* Historically each side computed ids/slugs independently, which caused drift
|
||||
* (TOC links diverging from rendered heading ids). Keeping subsection shape and
|
||||
* id pieces here makes both sides consume the same contract.
|
||||
*/
|
||||
const API_REFERENCE_SUBSECTIONS = Object.freeze([
|
||||
{
|
||||
key: 'props',
|
||||
title: 'Props',
|
||||
singleId: 'props',
|
||||
suffix: 'props',
|
||||
},
|
||||
{
|
||||
key: 'state',
|
||||
title: 'State',
|
||||
singleId: 'state',
|
||||
suffix: 'state',
|
||||
},
|
||||
{
|
||||
key: 'dataAttributes',
|
||||
title: 'Data attributes',
|
||||
singleId: 'data-attributes',
|
||||
suffix: 'data-attributes',
|
||||
},
|
||||
]);
|
||||
|
||||
export const API_REFERENCE_SUBSECTION_TITLES = Object.freeze(API_REFERENCE_SUBSECTIONS.map((section) => section.title));
|
||||
|
||||
function hasEntries(value) {
|
||||
return Object.keys(value ?? {}).length > 0;
|
||||
}
|
||||
|
||||
function createSections(source, options) {
|
||||
return API_REFERENCE_SUBSECTIONS.flatMap((definition) => {
|
||||
if (!hasEntries(source[definition.key])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (options.forPart) {
|
||||
return [
|
||||
{
|
||||
key: definition.key,
|
||||
title: definition.title,
|
||||
id: `${options.partId}-${definition.suffix}`,
|
||||
depth: 4,
|
||||
tocKind: 'api-reference-subsection',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
key: definition.key,
|
||||
title: definition.title,
|
||||
id: definition.singleId,
|
||||
depth: 3,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single source-of-truth model for API reference headings and sections.
|
||||
*
|
||||
* This model intentionally carries both:
|
||||
* - display concerns (heading text/depth/framework label)
|
||||
* - identity concerns (exact ids used by anchors + TOC entries)
|
||||
*
|
||||
* The shared model is what prevents anchor drift: ids are computed once and
|
||||
* reused verbatim by the renderer and the remark plugin.
|
||||
*/
|
||||
export function createApiReferenceModel(componentName, apiReference) {
|
||||
if (!apiReference) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasParts = Boolean(apiReference.parts && Object.keys(apiReference.parts).length > 0);
|
||||
|
||||
if (hasParts) {
|
||||
const parts = Object.entries(apiReference.parts).map(([partId, part]) => ({
|
||||
id: partId,
|
||||
name: part.name,
|
||||
description: part.description,
|
||||
componentName: `${componentName}.${part.name}`,
|
||||
labelByFramework: {
|
||||
react: part.name,
|
||||
html: part.platforms?.html?.tagName ?? part.name,
|
||||
},
|
||||
sections: createSections(part, { forPart: true, partId }),
|
||||
data: part,
|
||||
}));
|
||||
|
||||
return {
|
||||
componentName,
|
||||
hasParts: true,
|
||||
heading: {
|
||||
id: 'api-reference',
|
||||
depth: 2,
|
||||
text: 'API Reference',
|
||||
},
|
||||
sections: [],
|
||||
parts,
|
||||
data: apiReference,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
componentName,
|
||||
hasParts: false,
|
||||
heading: {
|
||||
id: 'api-reference',
|
||||
depth: 2,
|
||||
text: 'API Reference',
|
||||
},
|
||||
sections: createSections(apiReference, { forPart: false }),
|
||||
parts: [],
|
||||
data: apiReference,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build TOC heading metadata from the shared API reference model.
|
||||
*
|
||||
* Important: this function does not slugify heading text. It uses model ids
|
||||
* directly, so TOC slugs are guaranteed to match rendered heading ids.
|
||||
*/
|
||||
export function buildApiReferenceTocHeadings(apiReferenceModel) {
|
||||
if (!apiReferenceModel) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const headings = [
|
||||
{
|
||||
depth: apiReferenceModel.heading.depth,
|
||||
text: apiReferenceModel.heading.text,
|
||||
slug: apiReferenceModel.heading.id,
|
||||
},
|
||||
];
|
||||
|
||||
if (apiReferenceModel.hasParts) {
|
||||
for (const part of apiReferenceModel.parts) {
|
||||
headings.push({
|
||||
depth: 3,
|
||||
text: part.labelByFramework.react,
|
||||
slug: part.id,
|
||||
frameworks: ['react'],
|
||||
});
|
||||
headings.push({
|
||||
depth: 3,
|
||||
text: part.labelByFramework.html,
|
||||
slug: part.id,
|
||||
frameworks: ['html'],
|
||||
});
|
||||
|
||||
for (const section of part.sections) {
|
||||
headings.push({
|
||||
depth: section.depth,
|
||||
text: section.title,
|
||||
slug: section.id,
|
||||
tocKind: section.tocKind,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return headings;
|
||||
}
|
||||
|
||||
for (const section of apiReferenceModel.sections) {
|
||||
headings.push({
|
||||
depth: section.depth,
|
||||
text: section.title,
|
||||
slug: section.id,
|
||||
});
|
||||
}
|
||||
|
||||
return headings;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import * as path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { kebabCase } from 'es-toolkit/string';
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { buildApiReferenceTocHeadings, createApiReferenceModel } from './apiReferenceModel';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const API_REF_DIR = path.resolve(__dirname, '../content/generated-api-reference');
|
||||
@@ -28,6 +29,7 @@ export default function remarkConditionalHeadings() {
|
||||
return (tree, file) => {
|
||||
const headingsWithMetadata = [];
|
||||
const slugger = new GithubSlugger();
|
||||
const reservedSlugs = new Set();
|
||||
|
||||
// Process the tree with a stateful visitor
|
||||
function visitWithContext(node, context = { frameworks: null, styles: null }) {
|
||||
@@ -60,7 +62,7 @@ export default function remarkConditionalHeadings() {
|
||||
|
||||
return;
|
||||
} else if (node.name === 'ApiReference') {
|
||||
injectApiReferenceHeadings(node, slugger, headingsWithMetadata);
|
||||
injectApiReferenceHeadings(node, headingsWithMetadata, reservedSlugs);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -68,10 +70,18 @@ export default function remarkConditionalHeadings() {
|
||||
// Handle headings
|
||||
if (node.type === 'heading') {
|
||||
const text = extractText(node);
|
||||
let slug = slugger.slug(text);
|
||||
|
||||
// Avoid collisions with explicit API reference ids.
|
||||
while (reservedSlugs.has(slug)) {
|
||||
slug = slugger.slug(text);
|
||||
}
|
||||
reservedSlugs.add(slug);
|
||||
|
||||
const metadata = {
|
||||
depth: node.depth,
|
||||
text,
|
||||
slug: slugger.slug(text),
|
||||
slug,
|
||||
};
|
||||
|
||||
// Add conditional context if present
|
||||
@@ -114,7 +124,7 @@ export default function remarkConditionalHeadings() {
|
||||
* For single-part components, injects "API reference".
|
||||
* For each, injects Props/State/Data attributes headings
|
||||
*/
|
||||
function injectApiReferenceHeadings(node, slugger, headingsWithMetadata) {
|
||||
function injectApiReferenceHeadings(node, headingsWithMetadata, reservedSlugs) {
|
||||
const componentAttr = node.attributes?.find((a) => a.name === 'component');
|
||||
const componentName = typeof componentAttr?.value === 'string' ? componentAttr.value : null;
|
||||
if (!componentName) return;
|
||||
@@ -122,43 +132,12 @@ function injectApiReferenceHeadings(node, slugger, headingsWithMetadata) {
|
||||
const json = readApiRefJson(componentName);
|
||||
if (!json) return;
|
||||
|
||||
if (json.parts && Object.keys(json.parts).length > 0) {
|
||||
for (const [partKebab, part] of Object.entries(json.parts)) {
|
||||
const tagName = part.platforms?.html?.tagName;
|
||||
const partKebabSlug = slugger.slug(partKebab);
|
||||
headingsWithMetadata.push({
|
||||
depth: 2,
|
||||
text: `<${componentName}.${part.name} /> reference`,
|
||||
slug: partKebabSlug,
|
||||
frameworks: ['react'],
|
||||
});
|
||||
headingsWithMetadata.push({
|
||||
depth: 2,
|
||||
text: `${tagName ? `<${tagName}>` : part.name} reference`,
|
||||
slug: partKebabSlug,
|
||||
frameworks: ['html'],
|
||||
});
|
||||
if (part.props && Object.keys(part.props).length > 0) {
|
||||
headingsWithMetadata.push({ depth: 3, text: 'Props', slug: slugger.slug('Props') });
|
||||
}
|
||||
if (part.state && Object.keys(part.state).length > 0) {
|
||||
headingsWithMetadata.push({ depth: 3, text: 'State', slug: slugger.slug('State') });
|
||||
}
|
||||
if (part.dataAttributes && Object.keys(part.dataAttributes).length > 0) {
|
||||
headingsWithMetadata.push({ depth: 3, text: 'Data attributes', slug: slugger.slug('Data attributes') });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
headingsWithMetadata.push({ depth: 2, text: 'API reference', slug: slugger.slug('API reference') });
|
||||
if (json.props && Object.keys(json.props).length > 0) {
|
||||
headingsWithMetadata.push({ depth: 3, text: 'Props', slug: slugger.slug('Props') });
|
||||
}
|
||||
if (json.state && Object.keys(json.state).length > 0) {
|
||||
headingsWithMetadata.push({ depth: 3, text: 'State', slug: slugger.slug('State') });
|
||||
}
|
||||
if (json.dataAttributes && Object.keys(json.dataAttributes).length > 0) {
|
||||
headingsWithMetadata.push({ depth: 3, text: 'Data attributes', slug: slugger.slug('Data attributes') });
|
||||
}
|
||||
const apiReferenceModel = createApiReferenceModel(componentName, json);
|
||||
const apiReferenceHeadings = buildApiReferenceTocHeadings(apiReferenceModel);
|
||||
|
||||
headingsWithMetadata.push(...apiReferenceHeadings);
|
||||
for (const heading of apiReferenceHeadings) {
|
||||
reservedSlugs.add(heading.slug);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildApiReferenceTocHeadings, createApiReferenceModel } from '../apiReferenceModel';
|
||||
|
||||
describe('createApiReferenceModel', () => {
|
||||
it('builds a single-part model with H3 sections for present data only', () => {
|
||||
const apiReference = {
|
||||
name: 'PlayButton',
|
||||
props: {
|
||||
size: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
state: {
|
||||
pressed: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
dataAttributes: {},
|
||||
platforms: {},
|
||||
};
|
||||
|
||||
const model = createApiReferenceModel('PlayButton', apiReference);
|
||||
|
||||
expect(model).toMatchObject({
|
||||
hasParts: false,
|
||||
heading: {
|
||||
id: 'api-reference',
|
||||
depth: 2,
|
||||
text: 'API Reference',
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
key: 'props',
|
||||
title: 'Props',
|
||||
id: 'props',
|
||||
depth: 3,
|
||||
},
|
||||
{
|
||||
key: 'state',
|
||||
title: 'State',
|
||||
id: 'state',
|
||||
depth: 3,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('builds a multi-part model with framework-specific labels and H4 section ids', () => {
|
||||
const apiReference = {
|
||||
name: 'Controls',
|
||||
props: {},
|
||||
state: {},
|
||||
dataAttributes: {},
|
||||
platforms: {},
|
||||
parts: {
|
||||
root: {
|
||||
name: 'Root',
|
||||
description: 'Root part',
|
||||
props: {},
|
||||
state: {
|
||||
visible: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
dataAttributes: {
|
||||
'data-visible': {
|
||||
description: 'Visible',
|
||||
},
|
||||
},
|
||||
platforms: {
|
||||
html: {
|
||||
tagName: 'media-controls',
|
||||
},
|
||||
},
|
||||
},
|
||||
group: {
|
||||
name: 'Group',
|
||||
props: {},
|
||||
state: {},
|
||||
dataAttributes: {},
|
||||
platforms: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const model = createApiReferenceModel('Controls', apiReference);
|
||||
|
||||
expect(model).toMatchObject({
|
||||
hasParts: true,
|
||||
heading: {
|
||||
id: 'api-reference',
|
||||
depth: 2,
|
||||
text: 'API Reference',
|
||||
},
|
||||
parts: [
|
||||
{
|
||||
id: 'root',
|
||||
labelByFramework: {
|
||||
react: 'Root',
|
||||
html: 'media-controls',
|
||||
},
|
||||
componentName: 'Controls.Root',
|
||||
sections: [
|
||||
{
|
||||
key: 'state',
|
||||
title: 'State',
|
||||
id: 'root-state',
|
||||
depth: 4,
|
||||
tocKind: 'api-reference-subsection',
|
||||
},
|
||||
{
|
||||
key: 'dataAttributes',
|
||||
title: 'Data attributes',
|
||||
id: 'root-data-attributes',
|
||||
depth: 4,
|
||||
tocKind: 'api-reference-subsection',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'group',
|
||||
labelByFramework: {
|
||||
react: 'Group',
|
||||
html: 'Group',
|
||||
},
|
||||
componentName: 'Controls.Group',
|
||||
sections: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildApiReferenceTocHeadings', () => {
|
||||
it('creates TOC headings with API H4 metadata for multi-part sections', () => {
|
||||
const apiReference = {
|
||||
name: 'Controls',
|
||||
props: {},
|
||||
state: {},
|
||||
dataAttributes: {},
|
||||
platforms: {},
|
||||
parts: {
|
||||
root: {
|
||||
name: 'Root',
|
||||
props: {},
|
||||
state: {
|
||||
visible: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
dataAttributes: {
|
||||
'data-visible': {
|
||||
description: 'Visible',
|
||||
},
|
||||
},
|
||||
platforms: {
|
||||
html: {
|
||||
tagName: 'media-controls',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const model = createApiReferenceModel('Controls', apiReference);
|
||||
const headings = buildApiReferenceTocHeadings(model);
|
||||
|
||||
expect(headings).toEqual([
|
||||
{
|
||||
depth: 2,
|
||||
text: 'API Reference',
|
||||
slug: 'api-reference',
|
||||
},
|
||||
{
|
||||
depth: 3,
|
||||
text: 'Root',
|
||||
slug: 'root',
|
||||
frameworks: ['react'],
|
||||
},
|
||||
{
|
||||
depth: 3,
|
||||
text: 'media-controls',
|
||||
slug: 'root',
|
||||
frameworks: ['html'],
|
||||
},
|
||||
{
|
||||
depth: 4,
|
||||
text: 'State',
|
||||
slug: 'root-state',
|
||||
tocKind: 'api-reference-subsection',
|
||||
},
|
||||
{
|
||||
depth: 4,
|
||||
text: 'Data attributes',
|
||||
slug: 'root-data-attributes',
|
||||
tocKind: 'api-reference-subsection',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user