mirror of
https://gerrit.wikimedia.org/r/mediawiki/skins/Vector.git
synced 2024-11-23 23:33:54 +00:00
Merge "Add hash fragment support to table of contents"
This commit is contained in:
commit
e47d30a119
|
@ -168,42 +168,40 @@ const setupTableOfContents = ( tocElement, bodyContent, initSectionObserverFn )
|
|||
return null;
|
||||
}
|
||||
|
||||
const handleTocSectionChange = () => {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
sectionObserver.pause();
|
||||
|
||||
// T297614: We want the link that the user has clicked inside the TOC or the
|
||||
// section that corresponds to the hashchange event to be "active" (e.g.
|
||||
// bolded) regardless of whether the browser's scroll position corresponds
|
||||
// to that section. Therefore, we need to temporarily ignore section
|
||||
// observer until the browser has finished scrolling to the section (if
|
||||
// needed).
|
||||
//
|
||||
// However, because the scroll event happens asynchronously after the user
|
||||
// clicks on a link and may not even happen at all (e.g. the user has
|
||||
// scrolled all the way to the bottom and clicks a section that is already
|
||||
// in the viewport), determining when we should resume section observer is a
|
||||
// bit tricky.
|
||||
//
|
||||
// Because a scroll event may not even be triggered after clicking the link,
|
||||
// we instead allow the browser to perform a maximum number of repaints
|
||||
// before resuming sectionObserver. Per T297614#7687656, Firefox 97.0 wasn't
|
||||
// consistently activating the table of contents section that the user
|
||||
// clicked even after waiting 2 frames. After further investigation, it
|
||||
// sometimes waits up to 3 frames before painting the new scroll position so
|
||||
// we have that as the limit.
|
||||
deferUntilFrame( () => {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
sectionObserver.resume();
|
||||
}, 3 );
|
||||
};
|
||||
|
||||
const tableOfContents = initTableOfContents( {
|
||||
container: tocElement,
|
||||
onHeadingClick: ( id ) => {
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
sectionObserver.pause();
|
||||
|
||||
tableOfContents.expandSection( id );
|
||||
tableOfContents.changeActiveSection( id );
|
||||
|
||||
// T297614: We want the link that the user has clicked inside the TOC to
|
||||
// be "active" (e.g. bolded) regardless of whether the browser's scroll
|
||||
// position corresponds to that section. Therefore, we need to temporarily
|
||||
// ignore section observer until the browser has finished scrolling to the
|
||||
// section (if needed).
|
||||
//
|
||||
// However, because the scroll event happens asyncronously after the user
|
||||
// clicks on a link and may not even happen at all (e.g. the user has
|
||||
// scrolled all the way to the bottom and clicks a section that is already
|
||||
// in the viewport), determining when we should resume section observer is
|
||||
// a bit tricky.
|
||||
//
|
||||
// Because a scroll event may not even be triggered after clicking the
|
||||
// link, we instead allow the browser to perform a maximum number of
|
||||
// repaints before resuming sectionObserver. Per T297614#7687656, Firefox
|
||||
// 97.0 wasn't consistently activating the table of contents section that
|
||||
// the user clicked even after waiting 2 frames. After further
|
||||
// investigation, it sometimes waits up to 3 frames before painting the
|
||||
// new scroll position so we have that as the limit.
|
||||
//
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
deferUntilFrame( () => sectionObserver.resume(), 3 );
|
||||
},
|
||||
onToggleClick: ( id ) => {
|
||||
tableOfContents.toggleExpandSection( id );
|
||||
},
|
||||
onHeadingClick: handleTocSectionChange,
|
||||
onHashChange: handleTocSectionChange,
|
||||
onTogglePinned: () => {
|
||||
updateTocLocation();
|
||||
pinnableElement.setFocusAfterToggle( TOC_ID );
|
||||
|
@ -234,6 +232,46 @@ const setupTableOfContents = ( tocElement, bodyContent, initSectionObserverFn )
|
|||
} );
|
||||
} );
|
||||
mw.hook( 've.deactivationComplete' ).add( updateElements );
|
||||
|
||||
const setInitialActiveSection = () => {
|
||||
const hash = location.hash.slice( 1 );
|
||||
// If hash fragment is blank, determine the active section with section
|
||||
// observer.
|
||||
if ( hash === '' ) {
|
||||
sectionObserver.calcIntersection();
|
||||
return;
|
||||
}
|
||||
|
||||
// T325086: If hash fragment is present and corresponds to a toc section,
|
||||
// expand the section.
|
||||
// @ts-ignore
|
||||
const hashSection = /** @type {HTMLElement|null} */ ( mw.util.getTargetFromFragment( `${TOC_SECTION_ID_PREFIX}${hash}` ) );
|
||||
if ( hashSection ) {
|
||||
tableOfContents.expandSection( hashSection.id );
|
||||
}
|
||||
|
||||
// T325086: If hash fragment corresponds to a section AND the user is at
|
||||
// bottom of page, activate the section. Otherwise, use section observer to
|
||||
// calculate the active section.
|
||||
//
|
||||
// Note that even if a hash fragment is present, it's possible for the
|
||||
// browser to scroll to a position that is different from the position of
|
||||
// the section that corresponds to the hash fragment. This can happen when
|
||||
// the browser remembers a prior scroll position after refreshing the page,
|
||||
// for example.
|
||||
if (
|
||||
hashSection &&
|
||||
Math.round( window.innerHeight + window.scrollY ) >= document.body.scrollHeight
|
||||
) {
|
||||
tableOfContents.changeActiveSection( hashSection.id );
|
||||
} else {
|
||||
// Fallback to section observer's calculation for the active section.
|
||||
sectionObserver.calcIntersection();
|
||||
}
|
||||
};
|
||||
|
||||
setInitialActiveSection();
|
||||
|
||||
return tableOfContents;
|
||||
};
|
||||
|
||||
|
|
|
@ -50,9 +50,9 @@ module.exports = function sectionObserver( props ) {
|
|||
onIntersection: () => {}
|
||||
}, props );
|
||||
|
||||
let /** @type {boolean} */ inThrottle = false;
|
||||
let /** @type {number | undefined} */ timeoutId;
|
||||
let /** @type {HTMLElement | undefined} */ current;
|
||||
// eslint-disable-next-line compat/compat
|
||||
|
||||
const observer = new IntersectionObserver( ( entries ) => {
|
||||
let /** @type {IntersectionObserverEntry | undefined} */ closestNegativeEntry;
|
||||
let /** @type {IntersectionObserverEntry | undefined} */ closestPositiveEntry;
|
||||
|
@ -105,6 +105,9 @@ module.exports = function sectionObserver( props ) {
|
|||
observer.disconnect();
|
||||
} );
|
||||
|
||||
/**
|
||||
* Calculate the intersection of each observed element.
|
||||
*/
|
||||
function calcIntersection() {
|
||||
// IntersectionObserver will asynchronously calculate the boundingClientRect
|
||||
// of each observed element off the main thread after `observe` is called.
|
||||
|
@ -120,12 +123,10 @@ module.exports = function sectionObserver( props ) {
|
|||
|
||||
function handleScroll() {
|
||||
// Throttle the scroll event handler to fire at a rate limited by `props.throttleMs`.
|
||||
if ( !inThrottle ) {
|
||||
inThrottle = true;
|
||||
|
||||
setTimeout( () => {
|
||||
if ( !timeoutId ) {
|
||||
timeoutId = window.setTimeout( () => {
|
||||
calcIntersection();
|
||||
inThrottle = false;
|
||||
timeoutId = undefined;
|
||||
}, props.throttleMs );
|
||||
}
|
||||
}
|
||||
|
@ -143,6 +144,8 @@ module.exports = function sectionObserver( props ) {
|
|||
*/
|
||||
function pause() {
|
||||
unbindScrollListener();
|
||||
clearTimeout( timeoutId );
|
||||
timeoutId = undefined;
|
||||
// Assume current is no longer valid while paused.
|
||||
current = undefined;
|
||||
}
|
||||
|
@ -173,17 +176,17 @@ module.exports = function sectionObserver( props ) {
|
|||
}
|
||||
|
||||
bindScrollListener();
|
||||
// Calculate intersection on page load.
|
||||
calcIntersection();
|
||||
|
||||
/**
|
||||
* @typedef {Object} SectionObserver
|
||||
* @property {calcIntersection} calcIntersection
|
||||
* @property {pause} pause
|
||||
* @property {resume} resume
|
||||
* @property {unmount} unmount
|
||||
* @property {setElements} setElements
|
||||
*/
|
||||
return {
|
||||
calcIntersection,
|
||||
pause,
|
||||
resume,
|
||||
unmount,
|
||||
|
|
|
@ -9,7 +9,9 @@ const templateTocLine = require( /** @type {string} */ ( './templates/TableOfCon
|
|||
* TableOfContents Config object for filling mustache templates
|
||||
*/
|
||||
const tableOfContentsConfig = require( /** @type {string} */ ( './tableOfContentsConfig.json' ) );
|
||||
const deferUntilFrame = require( './deferUntilFrame.js' );
|
||||
|
||||
const SECTION_ID_PREFIX = 'toc-';
|
||||
const SECTION_CLASS = 'vector-toc-list-item';
|
||||
const ACTIVE_SECTION_CLASS = 'vector-toc-list-item-active';
|
||||
const EXPANDED_SECTION_CLASS = 'vector-toc-list-item-expanded';
|
||||
|
@ -20,10 +22,23 @@ const TOGGLE_CLASS = 'vector-toc-toggle';
|
|||
const TOC_CONTENTS_ID = 'mw-panel-toc-list';
|
||||
|
||||
/**
|
||||
* Fired when the user clicks a toc link. Note that this callback takes
|
||||
* precedence over the onHashChange callback. The onHashChange callback will not
|
||||
* be called when the user clicks a toc link.
|
||||
*
|
||||
* @callback onHeadingClick
|
||||
* @param {string} id The id of the clicked list item.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when the page's hash fragment has changed. Note that if the user clicks
|
||||
* a link inside the TOC, the `onHeadingClick` callback will fire instead of the
|
||||
* `onHashChange` callback to avoid redundant behavior.
|
||||
*
|
||||
* @callback onHashChange
|
||||
* @param {string} id The id of the list item that corresponds to the hash change event.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback onToggleClick
|
||||
* @param {string} id The id of the list item corresponding to the arrow.
|
||||
|
@ -43,7 +58,9 @@ const TOC_CONTENTS_ID = 'mw-panel-toc-list';
|
|||
* @typedef {Object} TableOfContentsProps
|
||||
* @property {HTMLElement} container The container element for the table of contents.
|
||||
* @property {onHeadingClick} onHeadingClick Called when an arrow is clicked.
|
||||
* @property {onToggleClick} onToggleClick Called when a list item is clicked.
|
||||
* @property {onHashChange} onHashChange Called when a hash change event
|
||||
* matches the id of a LINK_CLASS anchor element.
|
||||
* @property {onToggleClick} [onToggleClick] Called when an arrow is clicked.
|
||||
* @property {onTogglePinned} onTogglePinned Called when pinned toggle buttons are clicked.
|
||||
*/
|
||||
|
||||
|
@ -235,7 +252,7 @@ module.exports = function tableOfContents( props ) {
|
|||
}
|
||||
|
||||
const topSection = /** @type {HTMLElement} */ ( tocSection.closest( `.${TOP_SECTION_CLASS}` ) );
|
||||
const toggle = tocSection.querySelector( `.${TOGGLE_CLASS}` );
|
||||
const toggle = topSection.querySelector( `.${TOGGLE_CLASS}` );
|
||||
|
||||
if ( topSection && toggle && expandedSections.indexOf( topSection ) < 0 ) {
|
||||
toggle.setAttribute( 'aria-expanded', 'true' );
|
||||
|
@ -328,6 +345,44 @@ module.exports = function tableOfContents( props ) {
|
|||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for hash change event.
|
||||
*/
|
||||
function handleHashChange() {
|
||||
const hash = location.hash.slice( 1 );
|
||||
const listItem =
|
||||
// @ts-ignore
|
||||
/** @type {HTMLElement|null} */ ( mw.util.getTargetFromFragment( `${SECTION_ID_PREFIX}${hash}` ) );
|
||||
|
||||
if ( !listItem ) {
|
||||
return;
|
||||
}
|
||||
|
||||
expandSection( listItem.id );
|
||||
changeActiveSection( listItem.id );
|
||||
|
||||
props.onHashChange( listItem.id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind event listener for hash change events that match the hash of
|
||||
* LINK_CLASS.
|
||||
*
|
||||
* Note that if the user clicks a link inside the TOC, the onHeadingClick
|
||||
* callback will fire instead of the onHashChange callback, since it takes
|
||||
* precedence.
|
||||
*/
|
||||
function bindHashChangeListener() {
|
||||
window.addEventListener( 'hashchange', handleHashChange );
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbinds event listener for hash change events.
|
||||
*/
|
||||
function unbindHashChangeListener() {
|
||||
window.removeEventListener( 'hashchange', handleHashChange );
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind event listener for clicking on show/hide Table of Contents links.
|
||||
*/
|
||||
|
@ -358,12 +413,27 @@ module.exports = function tableOfContents( props ) {
|
|||
// In case section link contains HTML,
|
||||
// test if click occurs on any child elements.
|
||||
if ( e.target.closest( `.${LINK_CLASS}` ) ) {
|
||||
// Temporarily unbind the hash change listener to avoid redundant
|
||||
// behavior caused by firing both the onHeadingClick callback and the
|
||||
// onHashChange callback. Instead, only fire the onHeadingClick
|
||||
// callback.
|
||||
unbindHashChangeListener();
|
||||
|
||||
expandSection( tocSection.id );
|
||||
changeActiveSection( tocSection.id );
|
||||
props.onHeadingClick( tocSection.id );
|
||||
|
||||
deferUntilFrame( () => {
|
||||
bindHashChangeListener();
|
||||
}, 3 );
|
||||
}
|
||||
// Toggle button does not contain child elements,
|
||||
// so classList check will suffice.
|
||||
if ( e.target.classList.contains( TOGGLE_CLASS ) ) {
|
||||
props.onToggleClick( tocSection.id );
|
||||
toggleExpandSection( tocSection.id );
|
||||
if ( props.onToggleClick ) {
|
||||
props.onToggleClick( tocSection.id );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -385,6 +455,7 @@ module.exports = function tableOfContents( props ) {
|
|||
// Bind event listeners.
|
||||
bindSubsectionToggleListeners();
|
||||
bindPinnedToggleListeners();
|
||||
bindHashChangeListener();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -541,6 +612,17 @@ module.exports = function tableOfContents( props ) {
|
|||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the hash change event listener to prevent memory leaks. This
|
||||
* should be called when the table of contents is permanently no longer
|
||||
* needed.
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
function unmount() {
|
||||
unbindHashChangeListener();
|
||||
}
|
||||
|
||||
initialize();
|
||||
|
||||
/**
|
||||
|
@ -550,6 +632,7 @@ module.exports = function tableOfContents( props ) {
|
|||
* @property {expandSection} expandSection
|
||||
* @property {toggleExpandSection} toggleExpandSection
|
||||
* @property {updateTocToggleStyles} updateTocToggleStyles
|
||||
* @property {unmount} unmount
|
||||
* @property {string} ACTIVE_SECTION_CLASS
|
||||
* @property {string} ACTIVE_TOP_SECTION_CLASS
|
||||
* @property {string} EXPANDED_SECTION_CLASS
|
||||
|
@ -562,6 +645,7 @@ module.exports = function tableOfContents( props ) {
|
|||
changeActiveSection,
|
||||
toggleExpandSection,
|
||||
updateTocToggleStyles,
|
||||
unmount,
|
||||
ACTIVE_SECTION_CLASS,
|
||||
ACTIVE_TOP_SECTION_CLASS,
|
||||
EXPANDED_SECTION_CLASS,
|
||||
|
|
|
@ -159,7 +159,8 @@ const sectionObserverFn = () => ( {
|
|||
resume: () => {},
|
||||
mount: () => {},
|
||||
unmount: () => {},
|
||||
setElements: () => {}
|
||||
setElements: () => {},
|
||||
calcIntersection: () => {}
|
||||
} );
|
||||
|
||||
describe( 'Table of contents re-rendering', () => {
|
||||
|
|
|
@ -15,6 +15,7 @@ let /** @type {HTMLElement} */ container,
|
|||
/** @type {HTMLElement} */ quxSection,
|
||||
/** @type {HTMLElement} */ quuxSection;
|
||||
const onHeadingClick = jest.fn();
|
||||
const onHashChange = jest.fn();
|
||||
const onToggleClick = jest.fn();
|
||||
const onTogglePinned = jest.fn();
|
||||
|
||||
|
@ -110,30 +111,46 @@ function mount( templateProps = {} ) {
|
|||
return initTableOfContents( {
|
||||
container,
|
||||
onHeadingClick,
|
||||
onHashChange,
|
||||
onToggleClick,
|
||||
onTogglePinned
|
||||
} );
|
||||
}
|
||||
|
||||
describe( 'Table of contents', () => {
|
||||
/**
|
||||
* @type {module:TableOfContents~TableOfContents}
|
||||
*/
|
||||
let toc;
|
||||
|
||||
beforeEach( () => {
|
||||
// @ts-ignore
|
||||
global.window.matchMedia = jest.fn( () => ( {} ) );
|
||||
} );
|
||||
|
||||
afterEach( () => {
|
||||
if ( toc ) {
|
||||
toc.unmount();
|
||||
toc = undefined;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
mw.util.getTargetFromFragment = undefined;
|
||||
} );
|
||||
|
||||
describe( 'renders', () => {
|
||||
test( 'when `vector-is-collapse-sections-enabled` is false', () => {
|
||||
const toc = mount();
|
||||
toc = mount();
|
||||
expect( document.body.innerHTML ).toMatchSnapshot();
|
||||
expect( barSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( true );
|
||||
} );
|
||||
test( 'when `vector-is-collapse-sections-enabled` is true', () => {
|
||||
const toc = mount( { 'vector-is-collapse-sections-enabled': true } );
|
||||
toc = mount( { 'vector-is-collapse-sections-enabled': true } );
|
||||
expect( document.body.innerHTML ).toMatchSnapshot();
|
||||
expect( barSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( false );
|
||||
} );
|
||||
test( 'toggles for top level parent sections', () => {
|
||||
const toc = mount();
|
||||
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 );
|
||||
|
@ -144,26 +161,46 @@ describe( 'Table of contents', () => {
|
|||
|
||||
describe( 'binds event listeners', () => {
|
||||
test( 'for onHeadingClick', () => {
|
||||
const toc = mount();
|
||||
toc = mount();
|
||||
const heading = /** @type {HTMLElement} */ ( document.querySelector( `#toc-foo .${toc.LINK_CLASS}` ) );
|
||||
heading.click();
|
||||
|
||||
expect( onToggleClick ).not.toBeCalled();
|
||||
expect( onHashChange ).not.toBeCalled();
|
||||
expect( onHeadingClick ).toBeCalled();
|
||||
} );
|
||||
test( 'for onToggleClick', () => {
|
||||
const toc = mount();
|
||||
toc = mount();
|
||||
const toggle = /** @type {HTMLElement} */ ( document.querySelector( `#toc-bar .${toc.TOGGLE_CLASS}` ) );
|
||||
toggle.click();
|
||||
|
||||
expect( onHeadingClick ).not.toBeCalled();
|
||||
expect( onHashChange ).not.toBeCalled();
|
||||
expect( onToggleClick ).toBeCalled();
|
||||
} );
|
||||
test( 'for onHashChange', () => {
|
||||
// @ts-ignore
|
||||
mw.util.getTargetFromFragment = jest.fn().mockImplementation( ( hash ) => {
|
||||
return hash === 'toc-foo' ? fooSection : null;
|
||||
} );
|
||||
mount();
|
||||
|
||||
// Jest doesn't trigger a hashchange event when setting a hash location.
|
||||
location.hash = 'foo';
|
||||
window.dispatchEvent( new HashChangeEvent( 'hashchange', {
|
||||
oldURL: 'http://example.com',
|
||||
newURL: 'http://example.com#foo'
|
||||
} ) );
|
||||
|
||||
expect( onHeadingClick ).not.toBeCalled();
|
||||
expect( onToggleClick ).not.toBeCalled();
|
||||
expect( onHashChange ).toBeCalled();
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'applies correct classes', () => {
|
||||
test( 'when changing active sections', () => {
|
||||
const toc = mount( { 'vector-is-collapse-sections-enabled': true } );
|
||||
toc = mount( { 'vector-is-collapse-sections-enabled': true } );
|
||||
let activeSections;
|
||||
let activeTopSections;
|
||||
|
||||
|
@ -190,13 +227,13 @@ describe( 'Table of contents', () => {
|
|||
} );
|
||||
|
||||
test( 'when expanding sections', () => {
|
||||
const toc = mount();
|
||||
toc = mount();
|
||||
toc.expandSection( 'toc-bar' );
|
||||
expect( barSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( true );
|
||||
} );
|
||||
|
||||
test( 'when toggling sections', () => {
|
||||
const toc = mount();
|
||||
toc = mount();
|
||||
toc.toggleExpandSection( 'toc-bar' );
|
||||
expect( barSection.classList.contains( toc.EXPANDED_SECTION_CLASS ) ).toEqual( false );
|
||||
toc.toggleExpandSection( 'toc-bar' );
|
||||
|
@ -206,14 +243,14 @@ describe( 'Table of contents', () => {
|
|||
|
||||
describe( 'applies the correct aria attributes', () => {
|
||||
test( 'when initialized', () => {
|
||||
const toc = mount();
|
||||
toc = mount();
|
||||
const toggleButton = /** @type {HTMLElement} */ ( barSection.querySelector( `.${toc.TOGGLE_CLASS}` ) );
|
||||
|
||||
expect( toggleButton.getAttribute( 'aria-expanded' ) ).toEqual( 'true' );
|
||||
} );
|
||||
|
||||
test( 'when expanding sections', () => {
|
||||
const toc = mount();
|
||||
toc = mount();
|
||||
const toggleButton = /** @type {HTMLElement} */ ( barSection.querySelector( `.${toc.TOGGLE_CLASS}` ) );
|
||||
|
||||
toc.expandSection( 'toc-bar' );
|
||||
|
@ -221,7 +258,7 @@ describe( 'Table of contents', () => {
|
|||
} );
|
||||
|
||||
test( 'when toggling sections', () => {
|
||||
const toc = mount();
|
||||
toc = mount();
|
||||
const toggleButton = /** @type {HTMLElement} */ ( barSection.querySelector( `.${toc.TOGGLE_CLASS}` ) );
|
||||
|
||||
toc.toggleExpandSection( 'toc-bar' );
|
||||
|
@ -232,9 +269,43 @@ describe( 'Table of contents', () => {
|
|||
} );
|
||||
} );
|
||||
|
||||
describe( 'when the hash fragment changes', () => {
|
||||
test( 'expands and activates corresponding section', () => {
|
||||
// @ts-ignore
|
||||
mw.util.getTargetFromFragment = jest.fn().mockImplementation( ( hash ) => {
|
||||
return hash === 'toc-qux' ? quxSection : null;
|
||||
} );
|
||||
toc = mount( { 'vector-is-collapse-sections-enabled': true } );
|
||||
expect(
|
||||
quxSection.classList.contains( toc.ACTIVE_SECTION_CLASS )
|
||||
).toEqual( false );
|
||||
|
||||
// Jest doesn't trigger a hashchange event when setting a hash location.
|
||||
location.hash = 'qux';
|
||||
window.dispatchEvent( new HashChangeEvent( 'hashchange', {
|
||||
oldURL: 'http://example.com',
|
||||
newURL: 'http://example.com#qux'
|
||||
} ) );
|
||||
|
||||
const activeSections = container.querySelectorAll( `.${toc.ACTIVE_SECTION_CLASS}` );
|
||||
const activeTopSections = container.querySelectorAll( `.${toc.ACTIVE_TOP_SECTION_CLASS}` );
|
||||
expect( activeSections.length ).toEqual( 1 );
|
||||
expect( activeTopSections.length ).toEqual( 1 );
|
||||
expect(
|
||||
barSection.classList.contains( toc.ACTIVE_TOP_SECTION_CLASS )
|
||||
).toEqual( true );
|
||||
expect(
|
||||
barSection.classList.contains( toc.EXPANDED_SECTION_CLASS )
|
||||
).toEqual( true );
|
||||
expect(
|
||||
quxSection.classList.contains( toc.ACTIVE_SECTION_CLASS )
|
||||
).toEqual( true );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'reloadTableOfContents', () => {
|
||||
test( 're-renders toc when wikipage.tableOfContents hook is fired with empty sections', async () => {
|
||||
const toc = mount();
|
||||
toc = mount();
|
||||
await toc.reloadTableOfContents( [] );
|
||||
|
||||
expect( document.body.innerHTML ).toMatchSnapshot();
|
||||
|
@ -281,7 +352,7 @@ describe( 'Table of contents', () => {
|
|||
};
|
||||
} );
|
||||
|
||||
const toc = mount();
|
||||
toc = mount();
|
||||
|
||||
const toggleButton = /** @type {HTMLElement} */ ( barSection.querySelector( `.${toc.TOGGLE_CLASS}` ) );
|
||||
// Collapse section.
|
||||
|
|
Loading…
Reference in a new issue