Merge "Use TOC template data for showing collapsible section arrows"

This commit is contained in:
jenkins-bot 2022-02-22 22:16:18 +00:00 committed by Gerrit Code Review
commit d93f9e9bed
8 changed files with 107 additions and 69 deletions

View File

@ -4,7 +4,7 @@
</div>
<ul id="table-of-contents">
{{#array-sections}}
{{>TableOfContents__topSection}}
{{>TableOfContents__line}}
{{/array-sections}}
</ul>
</nav>

View File

@ -1,8 +1,12 @@
<li id="toc-{{anchor}}" class="sidebar-toc-list-item sidebar-toc-level-{{toclevel}}">
<li id="toc-{{anchor}}"
class="sidebar-toc-list-item sidebar-toc-level-{{toclevel}}{{#is-top-level-section}}{{^vector-is-collapse-sections-enabled}} sidebar-toc-list-item-expanded{{/vector-is-collapse-sections-enabled}}{{/is-top-level-section}}">
<a class="sidebar-toc-link" href="#{{anchor}}">
<div class="sidebar-toc-text">
<span class="sidebar-toc-numb">{{number}}</span>{{{line}}}</div>
</a>
{{#is-top-level-section}}{{#is-parent-section}}
<button class="mw-ui-icon mw-ui-icon-wikimedia-downTriangle mw-ui-icon-small sidebar-toc-toggle"></button>
{{/is-parent-section}}{{/is-top-level-section}}
<ul class="sidebar-toc-list">
{{#array-sections}}
{{>TableOfContents__line}}

View File

@ -1,13 +0,0 @@
<li id="toc-{{anchor}}" class="sidebar-toc-list-item sidebar-toc-level-{{toclevel}}{{^vector-is-collapse-sections-enabled}} sidebar-toc-list-item-expanded{{/vector-is-collapse-sections-enabled}}">
<a class="sidebar-toc-link" href="#{{anchor}}">
<div class="sidebar-toc-text">
<span class="sidebar-toc-numb">{{number}}</span>{{{line}}}</div>
</a>
{{!
The following <ul> is placed on *one* line in order to leverage the
CSS `:empty` selector and hide the downTriangle icon when there are no sub-sections.
(`:empty` means no whitespace).
}}
<ul class="sidebar-toc-list">{{#array-sections}}{{>TableOfContents__line}}{{/array-sections}}</ul>
<button class="mw-ui-icon mw-ui-icon-wikimedia-downTriangle mw-ui-icon-small sidebar-toc-toggle"></button>
</li>

View File

@ -1,3 +1,5 @@
/** @module SectionObserver */
/**
* @callback OnIntersection
* @param {HTMLElement} element The section that triggered the new intersection change.

View File

@ -1,3 +1,5 @@
/** @module TableOfContents */
const SECTION_CLASS = 'sidebar-toc-list-item';
const ACTIVE_SECTION_CLASS = 'sidebar-toc-list-item-active';
const EXPANDED_SECTION_CLASS = 'sidebar-toc-list-item-expanded';

View File

@ -94,20 +94,13 @@
.sidebar-toc-toggle {
position: absolute;
display: block;
font-size: 0.7em; // reduces size of toggle icon (by an arbitrary amount)
top: 4px; // visually center icon (at least at default font size)
left: -18px;
transform: rotate( -90deg );
}
.sidebar-toc-level-1 > .sidebar-toc-toggle {
display: block;
}
.sidebar-toc-level-1 > .sidebar-toc-list:empty + .sidebar-toc-toggle {
display: none;
}
.sidebar-toc-level-1.sidebar-toc-list-item-expanded .sidebar-toc-toggle {
transform: rotate( 0deg );
}

View File

@ -11,32 +11,41 @@ exports[`Table of contents when \`vector-is-collapse-sections-enabled\` is false
<div class=\\"sidebar-toc-text\\">
<span class=\\"sidebar-toc-numb\\">1</span>foo</div>
</a>
<ul class=\\"sidebar-toc-list\\"></ul>
<button class=\\"mw-ui-icon mw-ui-icon-wikimedia-downTriangle mw-ui-icon-small sidebar-toc-toggle\\"></button>
<ul class=\\"sidebar-toc-list\\">
</ul>
</li>
<li id=\\"toc-bar\\" class=\\"sidebar-toc-list-item sidebar-toc-level-1 sidebar-toc-list-item-expanded\\">
<a class=\\"sidebar-toc-link\\" href=\\"#bar\\">
<div class=\\"sidebar-toc-text\\">
<span class=\\"sidebar-toc-numb\\">2</span>bar</div>
</a>
<ul class=\\"sidebar-toc-list\\"><li id=\\"toc-baz\\" class=\\"sidebar-toc-list-item sidebar-toc-level-2\\">
<button class=\\"mw-ui-icon mw-ui-icon-wikimedia-downTriangle mw-ui-icon-small sidebar-toc-toggle\\"></button>
<ul class=\\"sidebar-toc-list\\">
<li id=\\"toc-baz\\" class=\\"sidebar-toc-list-item sidebar-toc-level-2\\">
<a class=\\"sidebar-toc-link\\" href=\\"#baz\\">
<div class=\\"sidebar-toc-text\\">
<span class=\\"sidebar-toc-numb\\">2.1</span>baz</div>
</a>
<ul class=\\"sidebar-toc-list\\">
</ul>
</li>
</ul>
<button class=\\"mw-ui-icon mw-ui-icon-wikimedia-downTriangle mw-ui-icon-small sidebar-toc-toggle\\"></button>
</li>
<li id=\\"toc-qux\\" class=\\"sidebar-toc-list-item sidebar-toc-level-1 sidebar-toc-list-item-expanded\\">
<li id=\\"toc-qux\\" class=\\"sidebar-toc-list-item sidebar-toc-level-3\\">
<a class=\\"sidebar-toc-link\\" href=\\"#qux\\">
<div class=\\"sidebar-toc-text\\">
<span class=\\"sidebar-toc-numb\\">3</span>qux</div>
<span class=\\"sidebar-toc-numb\\">2.1.1</span>qux</div>
</a>
<ul class=\\"sidebar-toc-list\\"></ul>
<button class=\\"mw-ui-icon mw-ui-icon-wikimedia-downTriangle mw-ui-icon-small sidebar-toc-toggle\\"></button>
<ul class=\\"sidebar-toc-list\\">
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li id=\\"toc-quux\\" class=\\"sidebar-toc-list-item sidebar-toc-level-1 sidebar-toc-list-item-expanded\\">
<a class=\\"sidebar-toc-link\\" href=\\"#quux\\">
<div class=\\"sidebar-toc-text\\">
<span class=\\"sidebar-toc-numb\\">3</span>quux</div>
</a>
<ul class=\\"sidebar-toc-list\\">
</ul>
</li>
</ul>
</nav>
@ -54,32 +63,41 @@ exports[`Table of contents when \`vector-is-collapse-sections-enabled\` is true
<div class=\\"sidebar-toc-text\\">
<span class=\\"sidebar-toc-numb\\">1</span>foo</div>
</a>
<ul class=\\"sidebar-toc-list\\"></ul>
<button class=\\"mw-ui-icon mw-ui-icon-wikimedia-downTriangle mw-ui-icon-small sidebar-toc-toggle\\"></button>
<ul class=\\"sidebar-toc-list\\">
</ul>
</li>
<li id=\\"toc-bar\\" class=\\"sidebar-toc-list-item sidebar-toc-level-1\\">
<a class=\\"sidebar-toc-link\\" href=\\"#bar\\">
<div class=\\"sidebar-toc-text\\">
<span class=\\"sidebar-toc-numb\\">2</span>bar</div>
</a>
<ul class=\\"sidebar-toc-list\\"><li id=\\"toc-baz\\" class=\\"sidebar-toc-list-item sidebar-toc-level-2\\">
<button class=\\"mw-ui-icon mw-ui-icon-wikimedia-downTriangle mw-ui-icon-small sidebar-toc-toggle\\"></button>
<ul class=\\"sidebar-toc-list\\">
<li id=\\"toc-baz\\" class=\\"sidebar-toc-list-item sidebar-toc-level-2\\">
<a class=\\"sidebar-toc-link\\" href=\\"#baz\\">
<div class=\\"sidebar-toc-text\\">
<span class=\\"sidebar-toc-numb\\">2.1</span>baz</div>
</a>
<ul class=\\"sidebar-toc-list\\">
</ul>
</li>
</ul>
<button class=\\"mw-ui-icon mw-ui-icon-wikimedia-downTriangle mw-ui-icon-small sidebar-toc-toggle\\"></button>
</li>
<li id=\\"toc-qux\\" class=\\"sidebar-toc-list-item sidebar-toc-level-1\\">
<li id=\\"toc-qux\\" class=\\"sidebar-toc-list-item sidebar-toc-level-3\\">
<a class=\\"sidebar-toc-link\\" href=\\"#qux\\">
<div class=\\"sidebar-toc-text\\">
<span class=\\"sidebar-toc-numb\\">3</span>qux</div>
<span class=\\"sidebar-toc-numb\\">2.1.1</span>qux</div>
</a>
<ul class=\\"sidebar-toc-list\\"></ul>
<button class=\\"mw-ui-icon mw-ui-icon-wikimedia-downTriangle mw-ui-icon-small sidebar-toc-toggle\\"></button>
<ul class=\\"sidebar-toc-list\\">
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li id=\\"toc-quux\\" class=\\"sidebar-toc-list-item sidebar-toc-level-1\\">
<a class=\\"sidebar-toc-link\\" href=\\"#quux\\">
<div class=\\"sidebar-toc-text\\">
<span class=\\"sidebar-toc-numb\\">3</span>quux</div>
</a>
<ul class=\\"sidebar-toc-list\\">
</ul>
</li>
</ul>
</nav>

View File

@ -1,12 +1,14 @@
// @ts-nocheck
const mustache = require( 'mustache' );
const fs = require( 'fs' );
const tableOfContentsTemplate = fs.readFileSync( 'includes/templates/TableOfContents.mustache', 'utf8' );
const tableOfContentsTopSectionTemplate = fs.readFileSync( 'includes/templates/TableOfContents__topSection.mustache', 'utf8' );
const tableOfContentsLineTemplate = fs.readFileSync( 'includes/templates/TableOfContents__line.mustache', 'utf8' );
const initTableOfContents = require( '../../resources/skins.vector.es6/tableOfContents.js' );
let fooSection, barSection, bazSection, quxSection;
let /** @type {HTMLElement} */ fooSection,
/** @type {HTMLElement} */ barSection,
/** @type {HTMLElement} */ bazSection,
/** @type {HTMLElement} */ quxSection,
/** @type {HTMLElement} */ quuxSection;
const onHeadingClick = jest.fn();
const onToggleClick = jest.fn();
@ -22,53 +24,66 @@ function render( templateProps = {} ) {
number: '1',
line: 'foo',
anchor: 'foo',
'is-top-level-section': true,
'is-parent-section': false,
'array-sections': null
}, {
toclevel: 1,
number: '2',
line: 'bar',
anchor: 'bar',
'is-top-level-section': true,
'is-parent-section': true,
'array-sections': [ {
toclevel: 2,
number: '2.1',
line: 'baz',
anchor: 'baz',
'array-sections': null
'is-top-level-section': false,
'is-parent-section': true,
'array-sections': [ {
toclevel: 3,
number: '2.1.1',
line: 'qux',
anchor: 'qux',
'is-top-level-section': false,
'is-parent-section': false,
'array-sections': null
} ]
} ]
}, {
toclevel: 1,
number: '3',
line: 'qux',
anchor: 'qux',
line: 'quux',
anchor: 'quux',
'is-top-level-section': true,
'is-parent-section': false,
'array-sections': null
} ]
}, templateProps );
/* eslint-disable camelcase */
return mustache.render( tableOfContentsTemplate, templateData, {
TableOfContents__topSection: tableOfContentsTopSectionTemplate,
TableOfContents__line: tableOfContentsLineTemplate
TableOfContents__line: tableOfContentsLineTemplate // eslint-disable-line camelcase
} );
/* eslint-enable camelcase */
}
/**
* @param {Object} templateProps
* @return {initTableOfContents.TableOfContents}
* @return {module:TableOfContents~TableOfContents}
*/
function mount( templateProps = {} ) {
document.body.innerHTML = render( templateProps );
const toc = initTableOfContents( {
container: /** @type {HTMLElement} */ document.getElementById( 'mw-panel-toc' ),
container: /** @type {HTMLElement} */ ( document.getElementById( 'mw-panel-toc' ) ),
onHeadingClick,
onToggleClick
} );
fooSection = /** @type {HTMLElement} */ document.getElementById( 'toc-foo' );
barSection = /** @type {HTMLElement} */ document.getElementById( 'toc-bar' );
bazSection = /** @type {HTMLElement} */ document.getElementById( 'toc-baz' );
quxSection = /** @type {HTMLElement} */ document.getElementById( 'toc-qux' );
fooSection = /** @type {HTMLElement} */ ( document.getElementById( 'toc-foo' ) );
barSection = /** @type {HTMLElement} */ ( document.getElementById( 'toc-bar' ) );
bazSection = /** @type {HTMLElement} */ ( document.getElementById( 'toc-baz' ) );
quxSection = /** @type {HTMLElement} */ ( document.getElementById( 'toc-qux' ) );
quuxSection = /** @type {HTMLElement} */ ( document.getElementById( 'toc-quux' ) );
return toc;
}
@ -76,7 +91,7 @@ describe( 'Table of contents', () => {
describe( 'binds event listeners', () => {
test( 'for onHeadingClick', () => {
const toc = mount();
const heading = document.querySelector( `#toc-foo .${toc.LINK_CLASS}` );
const heading = /** @type {HTMLElement} */ ( document.querySelector( `#toc-foo .${toc.LINK_CLASS}` ) );
heading.click();
expect( onToggleClick ).not.toBeCalled();
@ -84,7 +99,7 @@ describe( 'Table of contents', () => {
} );
test( 'for onToggleClick', () => {
const toc = mount();
const toggle = document.querySelector( `#toc-bar .${toc.TOGGLE_CLASS}` );
const toggle = /** @type {HTMLElement} */ ( document.querySelector( `#toc-bar .${toc.TOGGLE_CLASS}` ) );
toggle.click();
expect( onHeadingClick ).not.toBeCalled();
@ -92,6 +107,15 @@ describe( 'Table of contents', () => {
} );
} );
test( 'renders toggles for top level parent sections', () => {
const toc = mount();
expect( fooSection.getElementsByClassName( toc.TOGGLE_CLASS ).length ).toEqual( 0 );
expect( barSection.getElementsByClassName( toc.TOGGLE_CLASS ).length ).toEqual( 1 );
expect( bazSection.getElementsByClassName( toc.TOGGLE_CLASS ).length ).toEqual( 0 );
expect( quxSection.getElementsByClassName( toc.TOGGLE_CLASS ).length ).toEqual( 0 );
expect( quuxSection.getElementsByClassName( toc.TOGGLE_CLASS ).length ).toEqual( 0 );
} );
describe( 'when changing sections', () => {
test( 'applies correct class', () => {
const toc = mount( { 'vector-is-collapse-sections-enabled': true } );
@ -100,24 +124,28 @@ describe( 'Table of contents', () => {
expect( barSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( false );
expect( bazSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( false );
expect( quxSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( false );
expect( quuxSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( false );
toc.changeActiveSection( 'toc-bar' );
expect( fooSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( false );
expect( barSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( true );
expect( bazSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( false );
expect( quxSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( false );
expect( quuxSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( false );
toc.changeActiveSection( 'toc-baz' );
expect( fooSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( false );
expect( barSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( true );
expect( bazSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( true );
expect( quxSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( false );
expect( quuxSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( false );
toc.changeActiveSection( 'toc-qux' );
expect( fooSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( false );
expect( barSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( false );
expect( barSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( true );
expect( bazSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( false );
expect( quxSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( true );
expect( quuxSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( false );
} );
} );
@ -133,13 +161,15 @@ describe( 'Table of contents', () => {
expect( fooSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( true );
expect( barSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( true );
expect( bazSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( false );
expect( quxSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( true );
expect( quxSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( false );
expect( quuxSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( true );
toc.expandSection( 'toc-bar' );
expect( fooSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( true );
expect( barSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( true );
expect( bazSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( false );
expect( quxSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( true );
expect( quxSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( false );
expect( quuxSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( true );
} );
test( 'toggles expanded sections', () => {
@ -165,12 +195,14 @@ describe( 'Table of contents', () => {
expect( barSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( false );
expect( bazSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( false );
expect( quxSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( false );
expect( quuxSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( false );
toc.expandSection( 'toc-bar' );
expect( fooSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( true );
expect( barSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( true );
expect( bazSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( false );
expect( quxSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( false );
expect( quuxSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( false );
} );
test( 'toggles expanded sections', () => {