diff --git a/jest.config.js b/jest.config.js index 24fd95bd..1b96b1c7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -32,7 +32,7 @@ module.exports = { branches: 25, functions: 29, lines: 32, - statements: 33 + statements: 32 } }, diff --git a/resources/skins.vector.es6/tableOfContents.js b/resources/skins.vector.es6/tableOfContents.js index 412c49fe..d88a976e 100644 --- a/resources/skins.vector.es6/tableOfContents.js +++ b/resources/skins.vector.es6/tableOfContents.js @@ -3,7 +3,8 @@ 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 TOP_SECTION_CLASS = 'sidebar-toc-level-1'; +const ACTIVE_TOP_SECTION_CLASS = 'sidebar-toc-level-1-active'; const LINK_CLASS = 'sidebar-toc-link'; const TOGGLE_CLASS = 'sidebar-toc-toggle'; const TOC_COLLAPSED_CLASS = 'vector-toc-collapsed'; @@ -114,9 +115,9 @@ module.exports = function tableOfContents( props ) { /** * Sets an `ACTIVE_SECTION_CLASS` on the element with an id that matches `id`. - * If the element is not a top level heading (e.g. element with the - * `PARENT_SECTION_CLASS`), the top level heading will also have the - * `ACTIVE_SECTION_CLASS`; + * Sets an `ACTIVE_TOP_SECTION_CLASS` on the top level heading (e.g. element with the + * `TOP_SECTION_CLASS`). + * If the element is a top level heading, the element will also have both classes. * * @param {string} id The id of the element to be activated in the Table of Contents. */ @@ -135,17 +136,13 @@ module.exports = function tableOfContents( props ) { return; } - const topSection = /** @type {HTMLElement} */ ( selectedTocSection.closest( `.${PARENT_SECTION_CLASS}` ) ); + const topSection = /** @type {HTMLElement} */ ( selectedTocSection.closest( `.${TOP_SECTION_CLASS}` ) ); - if ( selectedTocSection === topSection ) { - activeTopSection = topSection; - activeTopSection.classList.add( ACTIVE_SECTION_CLASS ); - } else { - activeTopSection = topSection; - activeSubSection = selectedTocSection; - activeTopSection.classList.add( ACTIVE_SECTION_CLASS ); - activeSubSection.classList.add( ACTIVE_SECTION_CLASS ); - } + // Assign the active top and sub sections, apply classes + activeTopSection = topSection; + activeSubSection = ( selectedTocSection === topSection ) ? topSection : selectedTocSection; + activeTopSection.classList.add( ACTIVE_TOP_SECTION_CLASS ); + activeSubSection.classList.add( ACTIVE_SECTION_CLASS ); } /** @@ -158,7 +155,7 @@ module.exports = function tableOfContents( props ) { activeSubSection = undefined; } if ( activeTopSection ) { - activeTopSection.classList.remove( ACTIVE_SECTION_CLASS ); + activeTopSection.classList.remove( ACTIVE_TOP_SECTION_CLASS ); activeTopSection = undefined; } } @@ -235,7 +232,7 @@ module.exports = function tableOfContents( props ) { return; } - const parentSection = /** @type {HTMLElement} */ ( tocSection.closest( `.${PARENT_SECTION_CLASS}` ) ); + const parentSection = /** @type {HTMLElement} */ ( tocSection.closest( `.${TOP_SECTION_CLASS}` ) ); const toggle = tocSection.querySelector( `.${TOGGLE_CLASS}` ); if ( parentSection && toggle && expandedSections.indexOf( parentSection ) < 0 ) { @@ -277,7 +274,7 @@ module.exports = function tableOfContents( props ) { */ function isTopLevelSection( id ) { const section = document.getElementById( id ); - return !!section && section.classList.contains( PARENT_SECTION_CLASS ); + return !!section && section.classList.contains( TOP_SECTION_CLASS ); } /** @@ -319,7 +316,7 @@ module.exports = function tableOfContents( props ) { * Set aria-expanded attribute for all toggle buttons. */ function initializeExpandedStatus() { - const parentSections = props.container.querySelectorAll( `.${PARENT_SECTION_CLASS}` ); + const parentSections = props.container.querySelectorAll( `.${TOP_SECTION_CLASS}` ); parentSections.forEach( ( section ) => { const expanded = section.classList.contains( EXPANDED_SECTION_CLASS ); const toggle = section.querySelector( `.${TOGGLE_CLASS}` ); @@ -530,6 +527,7 @@ module.exports = function tableOfContents( props ) { * @property {expandSection} expandSection * @property {toggleExpandSection} toggleExpandSection * @property {string} ACTIVE_SECTION_CLASS + * @property {string} ACTIVE_TOP_SECTION_CLASS * @property {string} EXPANDED_SECTION_CLASS * @property {string} LINK_CLASS * @property {string} TOGGLE_CLASS @@ -539,6 +537,7 @@ module.exports = function tableOfContents( props ) { changeActiveSection, toggleExpandSection, ACTIVE_SECTION_CLASS, + ACTIVE_TOP_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 a8bbfdff..b3f481ab 100644 --- a/resources/skins.vector.styles/components/TableOfContents.less +++ b/resources/skins.vector.styles/components/TableOfContents.less @@ -68,6 +68,7 @@ height: @toc-subsection-toggle-icon-size; font-size: 0.75em; // reduces size of toggle icon to 12px @ 16 transition: @transition-duration-base; + color: transparent; cursor: pointer; } @@ -77,12 +78,27 @@ display: block; } - .sidebar-toc-list-item-active > .sidebar-toc-link { - // Highlight active section + // Highlight and bold active sections, active top sections that are unexpanded + // and active top sections that are the only active element. + .sidebar-toc-list-item-active, + .sidebar-toc-level-1-active:not( .sidebar-toc-list-item-expanded ), + .sidebar-toc-list-item-active.sidebar-toc-level-1-active { + > .sidebar-toc-link { + // Highlight active section + color: @color-base; + font-weight: bold; + + .sidebar-toc-text { + // Increase width to prevent line wrapping due to bold text + // Avoid applying on link element to avoid focus indicator changing size + width: ~'calc( 100% + @{sidebar-toc-right-padding} )'; + } + } + } + + // Highlight active top sections that contain an active section + .sidebar-toc-level-1-active:not( .sidebar-toc-list-item-active ) > .sidebar-toc-link { color: @color-base; - font-weight: bold; - // increase width to prevent line wrapping due to bold text - width: ~'calc( 100% + @{sidebar-toc-right-padding} )'; } .sidebar-toc-text { @@ -142,7 +158,6 @@ .sidebar-toc-toggle { display: block; - color: transparent; } .sidebar-toc-level-1.sidebar-toc-list-item-expanded .sidebar-toc-toggle { diff --git a/tests/jest/tableOfContents.test.js b/tests/jest/tableOfContents.test.js index c5bf9ff7..bef9b42b 100644 --- a/tests/jest/tableOfContents.test.js +++ b/tests/jest/tableOfContents.test.js @@ -4,7 +4,8 @@ const tableOfContentsTemplate = fs.readFileSync( 'includes/templates/TableOfCont const tableOfContentsLineTemplate = fs.readFileSync( 'includes/templates/TableOfContents__line.mustache', 'utf8' ); const initTableOfContents = require( '../../resources/skins.vector.es6/tableOfContents.js' ); -let /** @type {HTMLElement} */ fooSection, +let /** @type {HTMLElement} */ container, + /** @type {HTMLElement} */ fooSection, /** @type {HTMLElement} */ barSection, /** @type {HTMLElement} */ bazSection, /** @type {HTMLElement} */ quxSection, @@ -82,19 +83,20 @@ function render( templateProps = {} ) { */ function mount( templateProps = {} ) { document.body.innerHTML = render( templateProps ); - const toc = initTableOfContents( { - container: /** @type {HTMLElement} */ ( document.getElementById( 'mw-panel-toc' ) ), - onHeadingClick, - onToggleClick, - onToggleCollapse - } ); + container = /** @type {HTMLElement} */ ( document.getElementById( 'mw-panel-toc' ) ); 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; + + return initTableOfContents( { + container, + onHeadingClick, + onToggleClick, + onToggleCollapse + } ); } describe( 'Table of contents', () => { @@ -146,33 +148,29 @@ describe( 'Table of contents', () => { describe( 'applies correct classes', () => { test( 'when changing active sections', () => { const toc = mount( { 'vector-is-collapse-sections-enabled': true } ); - toc.changeActiveSection( 'toc-foo' ); - expect( fooSection.classList.contains( toc.ACTIVE_SECTION_CLASS ) ).toEqual( true ); - 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 ); + let activeSections; + let activeTopSections; - 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 ); + /** + * @param {string} id + * @param {HTMLElement} activeSection + * @param {HTMLElement} activeTopSection + */ + function testActiveClasses( id, activeSection, activeTopSection ) { + toc.changeActiveSection( id ); + activeSections = container.querySelectorAll( `.${toc.ACTIVE_SECTION_CLASS}` ); + activeTopSections = container.querySelectorAll( `.${toc.ACTIVE_TOP_SECTION_CLASS}` ); + expect( activeSections.length ).toEqual( 1 ); + expect( activeTopSections.length ).toEqual( 1 ); + expect( activeSections[ 0 ] ).toEqual( activeSection ); + expect( activeTopSections[ 0 ] ).toEqual( activeTopSection ); + } - 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( 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 ); + testActiveClasses( 'toc-foo', fooSection, fooSection ); + testActiveClasses( 'toc-bar', barSection, barSection ); + testActiveClasses( 'toc-baz', bazSection, barSection ); + testActiveClasses( 'toc-qux', quxSection, barSection ); + testActiveClasses( 'toc-quux', quuxSection, quuxSection ); } ); test( 'when expanding sections', () => {