fix(aio): fix window title on Home page (#20440)
Using `display: none` on the `<h1>` causes `innerText` to not work as expected and include the icon ligature (`link`) in the title. This caused the window title on the angular.io Home page to appear as "Angular - link". This commit fixes it by not generating anchors at all for headings with the `no-anchor` class. Fixes #20427 PR Close #20440
This commit is contained in:
parent
c28b52187a
commit
7e38f4fd1f
|
@ -25,7 +25,7 @@
|
||||||
<!-- MAIN CONTENT -->
|
<!-- MAIN CONTENT -->
|
||||||
<article>
|
<article>
|
||||||
|
|
||||||
<h1 class="no-toc" style="display: none"></h1>
|
<h1 class="no-anchor no-toc" style="display: none"></h1>
|
||||||
|
|
||||||
<div class="home-rows">
|
<div class="home-rows">
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,16 @@ describe('site App', function() {
|
||||||
expect(page.getDocViewerText()).toMatch(/Progressive web apps/i);
|
expect(page.getDocViewerText()).toMatch(/Progressive web apps/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should set appropriate window titles', () => {
|
||||||
|
expect(browser.getTitle()).toBe('Angular');
|
||||||
|
|
||||||
|
page.getTopMenuLink('features').click();
|
||||||
|
expect(browser.getTitle()).toBe('Angular - FEATURES & BENEFITS');
|
||||||
|
|
||||||
|
page.homeLink.click();
|
||||||
|
expect(browser.getTitle()).toBe('Angular');
|
||||||
|
});
|
||||||
|
|
||||||
it('should show the tutorial index page at `/tutorial/` after jitterbugging through features', () => {
|
it('should show the tutorial index page at `/tutorial/` after jitterbugging through features', () => {
|
||||||
// check that we can navigate directly to the tutorial page
|
// check that we can navigate directly to the tutorial page
|
||||||
page.navigateTo('tutorial/');
|
page.navigateTo('tutorial/');
|
||||||
|
|
|
@ -5,6 +5,7 @@ const githubRegex = /https:\/\/github.com\/angular\/angular\//;
|
||||||
export class SitePage {
|
export class SitePage {
|
||||||
|
|
||||||
links = element.all(by.css('md-toolbar a'));
|
links = element.all(by.css('md-toolbar a'));
|
||||||
|
homeLink = element(by.css('a.home'));
|
||||||
docsMenuLink = element(by.cssContainingText('aio-top-menu a', 'Docs'));
|
docsMenuLink = element(by.cssContainingText('aio-top-menu a', 'Docs'));
|
||||||
docViewer = element(by.css('aio-doc-viewer'));
|
docViewer = element(by.css('aio-doc-viewer'));
|
||||||
codeExample = element.all(by.css('aio-doc-viewer pre > code'));
|
codeExample = element.all(by.css('aio-doc-viewer pre > code'));
|
||||||
|
|
|
@ -126,7 +126,6 @@
|
||||||
"lunr": "^2.1.0",
|
"lunr": "^2.1.0",
|
||||||
"protractor": "^5.2.0",
|
"protractor": "^5.2.0",
|
||||||
"rehype": "^4.0.0",
|
"rehype": "^4.0.0",
|
||||||
"rehype-autolink-headings": "^2.0.0",
|
|
||||||
"rehype-slug": "^2.0.0",
|
"rehype-slug": "^2.0.0",
|
||||||
"remark": "^7.0.0",
|
"remark": "^7.0.0",
|
||||||
"remark-html": "^6.0.0",
|
"remark-html": "^6.0.0",
|
||||||
|
|
|
@ -361,6 +361,22 @@ describe('DocViewerComponent', () => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(titleService.setTitle).toHaveBeenCalledWith('Angular - Text Content');
|
expect(titleService.setTitle).toHaveBeenCalledWith('Angular - Text Content');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should still use `innerText` if available but empty', () => {
|
||||||
|
const querySelector_ = docViewerEl.querySelector;
|
||||||
|
spyOn(docViewerEl, 'querySelector').and.callFake((selector: string) => {
|
||||||
|
const elem = querySelector_.call(docViewerEl, selector);
|
||||||
|
Object.defineProperties(elem, {
|
||||||
|
innerText: { value: '' },
|
||||||
|
textContent: { value: 'Text Content' }
|
||||||
|
});
|
||||||
|
return elem;
|
||||||
|
});
|
||||||
|
|
||||||
|
setCurrentDoc('<h1><i style="visibility: hidden">link</i></h1>Some content');
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(titleService.setTitle).toHaveBeenCalledWith('Angular');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('TOC', () => {
|
describe('TOC', () => {
|
||||||
|
|
|
@ -92,17 +92,19 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
|
||||||
|
|
||||||
private addTitleAndToc(docId: string) {
|
private addTitleAndToc(docId: string) {
|
||||||
this.tocService.reset();
|
this.tocService.reset();
|
||||||
let title = '';
|
|
||||||
const titleEl = this.hostElement.querySelector('h1');
|
const titleEl = this.hostElement.querySelector('h1');
|
||||||
|
let title = '';
|
||||||
|
|
||||||
// Only create TOC for docs with an <h1> title
|
// Only create TOC for docs with an <h1> title
|
||||||
// If you don't want a TOC, add "no-toc" class to <h1>
|
// If you don't want a TOC, add "no-toc" class to <h1>
|
||||||
if (titleEl) {
|
if (titleEl) {
|
||||||
title = (titleEl.innerText || titleEl.textContent).trim();
|
title = (typeof titleEl.innerText === 'string') ? titleEl.innerText : titleEl.textContent;
|
||||||
if (!/(no-toc|notoc)/i.test(titleEl.className)) {
|
if (!/(no-toc|notoc)/i.test(titleEl.className)) {
|
||||||
this.tocService.genToc(this.hostElement, docId);
|
this.tocService.genToc(this.hostElement, docId);
|
||||||
titleEl.insertAdjacentHTML('afterend', '<aio-toc class="embedded"></aio-toc>');
|
titleEl.insertAdjacentHTML('afterend', '<aio-toc class="embedded"></aio-toc>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.titleService.setTitle(title ? `Angular - ${title}` : 'Angular');
|
this.titleService.setTitle(title ? `Angular - ${title}` : 'Angular');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,34 @@
|
||||||
|
const has = require('hast-util-has-property');
|
||||||
|
const is = require('hast-util-is-element');
|
||||||
const slug = require('rehype-slug');
|
const slug = require('rehype-slug');
|
||||||
const link = require('rehype-autolink-headings');
|
const visit = require('unist-util-visit');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get remark to inject anchors into headings
|
* Get remark to add IDs to headings and inject anchors into them.
|
||||||
|
* This is a stripped-down equivalent of [rehype-autolink-headings](https://github.com/wooorm/rehype-autolink-headings)
|
||||||
|
* that supports ignoring headings with the `no-anchor` class.
|
||||||
*/
|
*/
|
||||||
|
const HEADINGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
||||||
|
const NO_ANCHOR_CLASS = 'no-anchor';
|
||||||
|
|
||||||
|
const clone = obj => JSON.parse(JSON.stringify(obj));
|
||||||
|
const hasClass = (node, cls) => {
|
||||||
|
const className = node.properties.className;
|
||||||
|
return className && className.includes(cls);
|
||||||
|
};
|
||||||
|
|
||||||
|
const link = options =>
|
||||||
|
tree => visit(tree, node => {
|
||||||
|
if (is(node, HEADINGS) && has(node, 'id') && !hasClass(node, NO_ANCHOR_CLASS)) {
|
||||||
|
node.children.unshift({
|
||||||
|
type: 'element',
|
||||||
|
tagName: 'a',
|
||||||
|
properties: Object.assign(clone(options.properties), {href: `#${node.properties.id}`}),
|
||||||
|
children: clone(options.content)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
slug,
|
slug,
|
||||||
[link, {
|
[link, {
|
||||||
|
@ -12,11 +37,13 @@ module.exports = [
|
||||||
className: ['header-link'],
|
className: ['header-link'],
|
||||||
'aria-hidden': 'true'
|
'aria-hidden': 'true'
|
||||||
},
|
},
|
||||||
content: {
|
content: [
|
||||||
|
{
|
||||||
type: 'element',
|
type: 'element',
|
||||||
tagName: 'i',
|
tagName: 'i',
|
||||||
properties: {className: ['material-icons']},
|
properties: {className: ['material-icons']},
|
||||||
children: [{ type: 'text', value: 'link' }]
|
children: [{ type: 'text', value: 'link' }]
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}]
|
}]
|
||||||
];
|
];
|
||||||
|
|
|
@ -14,20 +14,37 @@ describe('autolink-headings postprocessor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add anchors to headings', () => {
|
it('should add anchors to headings', () => {
|
||||||
const docs = [ {
|
const originalContent = `
|
||||||
docType: 'a',
|
|
||||||
renderedContent: `
|
|
||||||
<h1>Heading 1</h2>
|
<h1>Heading 1</h2>
|
||||||
<h2>Heading with <strong>bold</strong></h2>
|
<h2>Heading with <strong>bold</strong></h2>
|
||||||
<h3>Heading with encoded chars &</h3>
|
<h3>Heading with encoded chars &</h3>
|
||||||
`
|
`;
|
||||||
}];
|
const processedContent = `
|
||||||
processor.$process(docs);
|
|
||||||
expect(docs[0].renderedContent).toEqual(`
|
|
||||||
<h1 id="heading-1"><a title="Link to this heading" class="header-link" aria-hidden="true" href="#heading-1"><i class="material-icons">link</i></a>Heading 1</h1>
|
<h1 id="heading-1"><a title="Link to this heading" class="header-link" aria-hidden="true" href="#heading-1"><i class="material-icons">link</i></a>Heading 1</h1>
|
||||||
<h2 id="heading-with-bold"><a title="Link to this heading" class="header-link" aria-hidden="true" href="#heading-with-bold"><i class="material-icons">link</i></a>Heading with <strong>bold</strong></h2>
|
<h2 id="heading-with-bold"><a title="Link to this heading" class="header-link" aria-hidden="true" href="#heading-with-bold"><i class="material-icons">link</i></a>Heading with <strong>bold</strong></h2>
|
||||||
<h3 id="heading-with-encoded-chars-"><a title="Link to this heading" class="header-link" aria-hidden="true" href="#heading-with-encoded-chars-"><i class="material-icons">link</i></a>Heading with encoded chars &</h3>
|
<h3 id="heading-with-encoded-chars-"><a title="Link to this heading" class="header-link" aria-hidden="true" href="#heading-with-encoded-chars-"><i class="material-icons">link</i></a>Heading with encoded chars &</h3>
|
||||||
`);
|
`;
|
||||||
|
|
||||||
|
const docs = [{docType: 'a', renderedContent: originalContent}];
|
||||||
|
processor.$process(docs);
|
||||||
|
expect(docs[0].renderedContent).toBe(processedContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore headings with the `no-anchor` class', () => {
|
||||||
|
const originalContent = `
|
||||||
|
<h1 class="no-anchor">Heading 1</h2>
|
||||||
|
<h2 class="no-anchor">Heading with <strong>bold</strong></h2>
|
||||||
|
<h3 class="no-anchor">Heading with encoded chars &</h3>
|
||||||
|
`;
|
||||||
|
const processedContent = `
|
||||||
|
<h1 class="no-anchor" id="heading-1">Heading 1</h1>
|
||||||
|
<h2 class="no-anchor" id="heading-with-bold">Heading with <strong>bold</strong></h2>
|
||||||
|
<h3 class="no-anchor" id="heading-with-encoded-chars-">Heading with encoded chars &</h3>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const docs = [{docType: 'a', renderedContent: originalContent}];
|
||||||
|
processor.$process(docs);
|
||||||
|
expect(docs[0].renderedContent).toBe(processedContent);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2977,7 +2977,7 @@ express@^4.13.3:
|
||||||
utils-merge "1.0.1"
|
utils-merge "1.0.1"
|
||||||
vary "~1.1.2"
|
vary "~1.1.2"
|
||||||
|
|
||||||
extend@3, extend@^3.0.0, extend@^3.0.1, extend@~3.0.0, extend@~3.0.1:
|
extend@3, extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
|
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
|
||||||
|
|
||||||
|
@ -6999,15 +6999,6 @@ regjsparser@^0.1.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
jsesc "~0.5.0"
|
jsesc "~0.5.0"
|
||||||
|
|
||||||
rehype-autolink-headings@^2.0.0:
|
|
||||||
version "2.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/rehype-autolink-headings/-/rehype-autolink-headings-2.0.2.tgz#48c7161b1a1020e942c758eb6b2c55cb1bc504d0"
|
|
||||||
dependencies:
|
|
||||||
extend "^3.0.1"
|
|
||||||
hast-util-has-property "^1.0.0"
|
|
||||||
hast-util-is-element "^1.0.0"
|
|
||||||
unist-util-visit "^1.1.0"
|
|
||||||
|
|
||||||
rehype-parse@^3.0.0:
|
rehype-parse@^3.0.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-3.1.0.tgz#7f5227a597a3f39fc4b938646161539c444ee728"
|
resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-3.1.0.tgz#7f5227a597a3f39fc4b938646161539c444ee728"
|
||||||
|
|
Loading…
Reference in New Issue