mirror of
https://gerrit.wikimedia.org/r/mediawiki/skins/Vector.git
synced 2024-09-23 18:29:49 +00:00
Merge "Toggle ToC sections when clicking toggle button"
This commit is contained in:
commit
9b7d5ad473
|
@ -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>
|
||||
|
|
|
@ -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( {
|
||||
|
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
44
tests/jest/__snapshots__/tableOfContents.test.js.snap
Normal file
44
tests/jest/__snapshots__/tableOfContents.test.js.snap
Normal 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>
|
||||
"
|
||||
`;
|
|
@ -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();
|
||||
} );
|
||||
} );
|
||||
|
|
Loading…
Reference in a new issue