fix(docs-infra): exclude heading anchor icon text from ToC tooltips (#32418)
The Table of Contents (ToC) is auto-generated based on the content of heading elements on the page. At the same time, anchor links are auto-generated and added to each heading element. Note that the Material Icons used for the anchor icon make use of ligatures, which means that the icons are specified by using their textual name as text content of the icon element. As a result, the name of the icon is included in the parent element's `textContent`. Previously, the `TocService` used to strip off these anchor elements when generating the content of ToC items, but not when generating the content of their tooltips. Thus, tooltips for ToC items would confusingly include a `link` suffix (`link` is the textual name of the icon used in heading anchor links). This commit fixes this by deriving the tooltip content from the transformed text content (which already has anchor links stripped off), instead of from the original heading content. PR Close #32418
This commit is contained in:
parent
094538c0ce
commit
007282d2bb
|
@ -299,12 +299,11 @@ describe('TocService', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
docId = 'fizz/buzz/';
|
docId = 'fizz/buzz/';
|
||||||
|
|
||||||
// An almost-actual <h2> ... with extra whitespace
|
|
||||||
callGenToc(`
|
callGenToc(`
|
||||||
<h2 id="setup-to-develop-locally">
|
<h2 id="setup-to-develop-locally">
|
||||||
Setup to <a href="moo">develop</a> <i>locally</i>.
|
Setup to <a href="moo">develop</a> <i>locally</i>.
|
||||||
<a class="header-link" href="tutorial/toh-pt1#setup-to-develop-locally" aria-hidden="true">
|
<a class="header-link" href="tutorial/toh-pt1#setup-to-develop-locally" aria-hidden="true">
|
||||||
<span class="icon icon-link"></span>
|
<span class="icon">icon-link</span>
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
`, docId);
|
`, docId);
|
||||||
|
|
|
@ -34,12 +34,16 @@ export class TocService {
|
||||||
|
|
||||||
const headings = this.findTocHeadings(docElement);
|
const headings = this.findTocHeadings(docElement);
|
||||||
const idMap = new Map<string, number>();
|
const idMap = new Map<string, number>();
|
||||||
const tocList = headings.map(heading => ({
|
const tocList = headings.map(heading => {
|
||||||
content: this.extractHeadingSafeHtml(heading),
|
const {title, content} = this.extractHeadingSafeHtml(heading);
|
||||||
href: `${docId}#${this.getId(heading, idMap)}`,
|
|
||||||
|
return {
|
||||||
level: heading.tagName.toLowerCase(),
|
level: heading.tagName.toLowerCase(),
|
||||||
title: (heading.textContent || '').trim(),
|
href: `${docId}#${this.getId(heading, idMap)}`,
|
||||||
}));
|
title,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
this.tocList.next(tocList);
|
this.tocList.next(tocList);
|
||||||
|
|
||||||
|
@ -52,29 +56,35 @@ export class TocService {
|
||||||
this.tocList.next([]);
|
this.tocList.next([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This bad boy exists only to strip off the anchor link attached to a heading
|
// Transform the HTML content to be safe to use in the ToC:
|
||||||
|
// - Strip off the auto-generated heading anchor links).
|
||||||
|
// - Strip off any anchor links (but keep their content)
|
||||||
|
// - Mark the HTML as trusted to be used with `[innerHTML]`.
|
||||||
private extractHeadingSafeHtml(heading: HTMLHeadingElement) {
|
private extractHeadingSafeHtml(heading: HTMLHeadingElement) {
|
||||||
const div: HTMLDivElement = this.document.createElement('div');
|
const div: HTMLDivElement = this.document.createElement('div');
|
||||||
div.innerHTML = heading.innerHTML;
|
div.innerHTML = heading.innerHTML;
|
||||||
const anchorLinks = Array.from(div.querySelectorAll('a'));
|
|
||||||
for (const anchorLink of anchorLinks) {
|
// Remove any `.header-link` elements (along with their content).
|
||||||
if (!anchorLink.classList.contains('header-link')) {
|
div.querySelectorAll('.header-link').forEach(removeNode);
|
||||||
// this is an anchor that contains actual content that we want to keep
|
|
||||||
// move the contents of the anchor into its parent
|
// Remove any remaining `a` elements (but keep their content).
|
||||||
|
div.querySelectorAll('a').forEach(anchorLink => {
|
||||||
|
// We want to keep the content of this anchor, so move it into its parent.
|
||||||
const parent = anchorLink.parentNode!;
|
const parent = anchorLink.parentNode!;
|
||||||
while (anchorLink.childNodes.length) {
|
while (anchorLink.childNodes.length) {
|
||||||
parent.insertBefore(anchorLink.childNodes[0], anchorLink);
|
parent.insertBefore(anchorLink.childNodes[0], anchorLink);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// now remove the anchor
|
// Now, remove the anchor.
|
||||||
if (anchorLink.parentNode !== null) {
|
removeNode(anchorLink);
|
||||||
// We cannot use ChildNode.remove() because of IE11
|
});
|
||||||
anchorLink.parentNode.removeChild(anchorLink);
|
|
||||||
}
|
return {
|
||||||
}
|
// Security: The document element which provides this heading content is always authored by
|
||||||
// security: the document element which provides this heading content
|
// the documentation team and is considered to be safe.
|
||||||
// is always authored by the documentation team and is considered to be safe
|
content: this.domSanitizer.bypassSecurityTrustHtml(div.innerHTML.trim()),
|
||||||
return this.domSanitizer.bypassSecurityTrustHtml(div.innerHTML.trim());
|
title: (div.textContent || '').trim(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private findTocHeadings(docElement: Element): HTMLHeadingElement[] {
|
private findTocHeadings(docElement: Element): HTMLHeadingElement[] {
|
||||||
|
@ -115,3 +125,11 @@ export class TocService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
function removeNode(node: Node): void {
|
||||||
|
if (node.parentNode !== null) {
|
||||||
|
// We cannot use `Node.remove()` because of IE11.
|
||||||
|
node.parentNode.removeChild(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue