From 301e09916dfd4990b4eb49100b437224a65b9b70 Mon Sep 17 00:00:00 2001 From: bwang Date: Tue, 15 Feb 2022 16:43:04 -0600 Subject: [PATCH] Toggle ToC sections when clicking toggle button Bug: T300167 Change-Id: If1150a9e018b232da900187383aaee9c9cf331a1 --- .../TableOfContents__topSection.mustache | 2 +- resources/skins.vector.es6/main.js | 5 +- resources/skins.vector.es6/tableOfContents.js | 49 ++++++--- .../components/TableOfContents.less | 3 +- .../tableOfContents.test.js.snap | 44 ++++++++ tests/jest/tableOfContents.test.js | 104 +++++++++++++----- 6 files changed, 157 insertions(+), 50 deletions(-) create mode 100644 tests/jest/__snapshots__/tableOfContents.test.js.snap diff --git a/includes/templates/TableOfContents__topSection.mustache b/includes/templates/TableOfContents__topSection.mustache index b3b3eeae..6beffacb 100644 --- a/includes/templates/TableOfContents__topSection.mustache +++ b/includes/templates/TableOfContents__topSection.mustache @@ -9,5 +9,5 @@ (`:empty` means no whitespace). }} - + diff --git a/resources/skins.vector.es6/main.js b/resources/skins.vector.es6/main.js index 207c429b..9f57a17e 100644 --- a/resources/skins.vector.es6/main.js +++ b/resources/skins.vector.es6/main.js @@ -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( { diff --git a/resources/skins.vector.es6/tableOfContents.js b/resources/skins.vector.es6/tableOfContents.js index cc615d92..5cd41dcf 100644 --- a/resources/skins.vector.es6/tableOfContents.js +++ b/resources/skins.vector.es6/tableOfContents.js @@ -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} */ 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 }; }; diff --git a/resources/skins.vector.styles/components/TableOfContents.less b/resources/skins.vector.styles/components/TableOfContents.less index 1bbf7b84..46f575cc 100644 --- a/resources/skins.vector.styles/components/TableOfContents.less +++ b/resources/skins.vector.styles/components/TableOfContents.less @@ -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 diff --git a/tests/jest/__snapshots__/tableOfContents.test.js.snap b/tests/jest/__snapshots__/tableOfContents.test.js.snap new file mode 100644 index 00000000..d78a411a --- /dev/null +++ b/tests/jest/__snapshots__/tableOfContents.test.js.snap @@ -0,0 +1,44 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table of contents renders 1`] = ` +" +" +`; diff --git a/tests/jest/tableOfContents.test.js b/tests/jest/tableOfContents.test.js index f606cff5..f2507aa6 100644 --- a/tests/jest/tableOfContents.test.js +++ b/tests/jest/tableOfContents.test.js @@ -1,38 +1,65 @@ // @ts-nocheck -const tableOfContents = require( '../../resources/skins.vector.es6/tableOfContents.js' ); - -const template = ` - -`; +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(); + } ); +} );