Merge "Toggle ToC sections when clicking toggle button"

This commit is contained in:
jenkins-bot 2022-02-16 22:30:25 +00:00 committed by Gerrit Code Review
commit 9b7d5ad473
6 changed files with 157 additions and 50 deletions

View File

@ -9,5 +9,5 @@
(`:empty` means no whitespace).
}}
<ul class="sidebar-toc-list">{{#array-sections}}{{>TableOfContents__line}}{{/array-sections}}</ul>
<span class="mw-ui-icon mw-ui-icon-wikimedia-downTriangle mw-ui-icon-small sidebar-toc-toggle"></span>
<button class="mw-ui-icon mw-ui-icon-wikimedia-downTriangle mw-ui-icon-small sidebar-toc-toggle"></button>
</li>

View File

@ -81,7 +81,7 @@ const main = () => {
const tableOfContents = initTableOfContents( {
container: tocElement,
onSectionClick: ( id ) => {
onHeadingClick: ( id ) => {
// eslint-disable-next-line no-use-before-define
sectionObserver.pause();
@ -111,6 +111,9 @@ const main = () => {
//
// eslint-disable-next-line no-use-before-define
deferUntilFrame( () => sectionObserver.resume(), 3 );
},
onToggleClick: ( id ) => {
tableOfContents.toggleExpandSection( id );
}
} );
const sectionObserver = initSectionObserver( {

View File

@ -1,22 +1,34 @@
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';
const PARENT_SECTION_CLASS = 'sidebar-toc-level-1';
const LINK_CLASS = 'sidebar-toc-link';
const LIST_ITEM_CLASS = 'sidebar-toc-list-item';
const TOGGLE_CLASS = 'sidebar-toc-toggle';
/**
* Called when a list item is clicked.
*
* @callback onHeadingClick
* @param {string} id The id of the clicked list item.
*/
/**
* Called when an arrow is clicked.
*
* @callback onToggleClick
* @param {string} id The id of the list item corresponding to the arrow.
*/
/**
* Initializes the sidebar's Table of Contents.
*
* @param {Object} props
* @param {HTMLElement} props.container
* @param {OnSectionClick} [props.onSectionClick]
* @param {onHeadingClick} props.onHeadingClick
* @param {onToggleClick} props.onToggleClick
* @return {TableOfContents}
*/
module.exports = function tableOfContents( props ) {
props = Object.assign( {
onSectionClick: () => {}
}, props );
let /** @type {HTMLElement | undefined} */ activeTopSection;
let /** @type {HTMLElement | undefined} */ activeSubSection;
let /** @type {Array<HTMLElement>} */ expandedSections = [];
@ -177,27 +189,26 @@ module.exports = function tableOfContents( props ) {
}
}
/**
* Called when a list item is clicked.
*
* @callback OnSectionClick
* @param {string} id The id of the clicked list item.
*/
function bindClickListener() {
props.container.addEventListener( 'click', function ( e ) {
if (
!( e.target instanceof HTMLElement && e.target.classList.contains( LINK_CLASS ) )
!( e.target instanceof HTMLElement )
) {
return;
}
const tocSection =
/** @type {HTMLElement | null} */ ( e.target.closest( `.${LIST_ITEM_CLASS}` ) );
/** @type {HTMLElement | null} */ ( e.target.closest( `.${SECTION_CLASS}` ) );
if ( tocSection && tocSection.id ) {
// @ts-ignore
props.onSectionClick( tocSection.id );
if ( e.target.classList.contains( LINK_CLASS ) ) {
props.onHeadingClick( tocSection.id );
}
if ( e.target.classList.contains( TOGGLE_CLASS ) ) {
props.onToggleClick( tocSection.id );
}
}
} );
}
@ -210,12 +221,16 @@ module.exports = function tableOfContents( props ) {
* @property {toggleExpandSection} toggleExpandSection
* @property {string} ACTIVE_SECTION_CLASS
* @property {string} EXPANDED_SECTION_CLASS
* @property {string} LINK_CLASS
* @property {string} TOGGLE_CLASS
*/
return {
expandSection,
changeActiveSection,
toggleExpandSection,
ACTIVE_SECTION_CLASS,
EXPANDED_SECTION_CLASS
EXPANDED_SECTION_CLASS,
LINK_CLASS,
TOGGLE_CLASS
};
};

View File

@ -78,9 +78,10 @@
font-weight: bold;
}
// For no-js users, toggling is disabled and icon is hidden
.sidebar-toc .sidebar-toc-toggle {
// For no-js users, toggling is disabled and icon is hidden
display: none;
cursor: pointer;
}
// Collapse ToC sections by default, excluding no-js or prefers-reduced-motion

View File

@ -0,0 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Table of contents renders 1`] = `
"<nav id=\\"mw-panel-toc\\" class=\\"sidebar-toc\\" role=\\"navigation\\" aria-labelledby=\\"sidebar-toc-header\\">
<div class=\\"sidebar-toc-header\\">
<h2 class=\\"sidebar-toc-title\\" aria-hidden=\\"true\\">Contents</h2>
</div>
<ul id=\\"table-of-contents\\">
<li id=\\"toc-foo\\" class=\\"sidebar-toc-list-item sidebar-toc-level-1\\">
<a class=\\"sidebar-toc-link\\" href=\\"#foo\\">
<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>
</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\\">
<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\\">
<a class=\\"sidebar-toc-link\\" href=\\"#qux\\">
<div class=\\"sidebar-toc-text\\">
<span class=\\"sidebar-toc-numb\\">3</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>
</li>
</ul>
</nav>
"
`;

View File

@ -1,38 +1,65 @@
// @ts-nocheck
const tableOfContents = require( '../../resources/skins.vector.es6/tableOfContents.js' );
const template = `
<ul>
<li id="toc-foo" class="sidebar-toc-level-1">
<a href="#foo">foo</a>
</li>
<li id="toc-bar" class="sidebar-toc-level-1">
<a href="#bar">bar</a>
<ul>
<li id="toc-baz">
<a href="#baz">baz</a>
</li>
</ul>
</li>
<li id="toc-qux" class="sidebar-toc-level-1">
<a href="#qux">qux</a>
</li>
</ul>
`;
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 toc, fooSection, barSection, bazSection, quxSection;
const onHeadingClick = jest.fn();
const onToggleClick = jest.fn();
const templateData = {
'array-sections': [ {
toclevel: 1,
number: '1',
line: 'foo',
anchor: 'foo',
'array-sections': null
}, {
toclevel: 1,
number: '2',
line: 'bar',
anchor: 'bar',
'array-sections': [ {
toclevel: 2,
number: '2.1',
line: 'baz',
anchor: 'baz',
'array-sections': null
} ]
}, {
toclevel: 1,
number: '3',
line: 'qux',
anchor: 'qux',
'array-sections': null
} ]
};
/* eslint-disable camelcase */
const renderedHTML = mustache.render( tableOfContentsTemplate, templateData, {
TableOfContents__topSection: tableOfContentsTopSectionTemplate,
TableOfContents__line: tableOfContentsLineTemplate
} );
/* eslint-enable camelcase */
beforeEach( () => {
document.body.innerHTML = template;
toc = tableOfContents( { container: document.body } );
fooSection = document.getElementById( 'toc-foo' );
barSection = document.getElementById( 'toc-bar' );
bazSection = document.getElementById( 'toc-baz' );
quxSection = document.getElementById( 'toc-qux' );
document.body.innerHTML = renderedHTML;
toc = initTableOfContents( {
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' );
} );
test( 'Table of contents renders', () => {
expect( document.body.innerHTML ).toMatchSnapshot();
} );
test( 'Table of contents changes the active sections', () => {
@ -98,3 +125,20 @@ test( 'Table of contents toggles expanded sections', () => {
fooSection.classList.contains( toc.EXPANDED_SECTION_CLASS )
).toEqual( false );
} );
describe( 'Table of contents binds event listeners', () => {
test( 'for onHeadingClick', () => {
const heading = document.querySelector( `#toc-foo .${toc.LINK_CLASS}` );
heading.click();
expect( onToggleClick ).not.toBeCalled();
expect( onHeadingClick ).toBeCalled();
} );
test( 'for onToggleClick', () => {
const toggle = document.querySelector( `#toc-bar .${toc.TOGGLE_CLASS}` );
toggle.click();
expect( onHeadingClick ).not.toBeCalled();
expect( onToggleClick ).toBeCalled();
} );
} );