diff --git a/includes/Tabber.php b/includes/Tabber.php
index 19eeb1a..23b0390 100644
--- a/includes/Tabber.php
+++ b/includes/Tabber.php
@@ -108,8 +108,9 @@ class Tabber {
}
return "
" .
- '' .
- // '' .
+ '' .
'
';
}
@@ -215,11 +216,13 @@ class Tabber {
* @return string HTML
*/
private static function getTabHTML( array $tabData ): string {
+ $tabpanelId = "tabber-tabpanel-{$tabData['id']}";
return Html::rawElement( 'a', [
'class' => 'tabber__tab',
'id' => "tabber-tab-{$tabData['id']}",
- 'href' => "#tabber-tabpanel-{$tabData['id']}",
+ 'href' => "#$tabpanelId",
'role' => 'tab',
+ 'aria-controls' => $tabpanelId
], $tabData['label'] );
}
@@ -239,8 +242,10 @@ class Tabber {
}
return Html::rawElement( 'article', [
'class' => 'tabber__panel',
- // 'id' => "tabber-tabpanel-{$tabData['id']}",
- 'data-mw-tabber-title' => $tabData['label'],
+ 'id' => "tabber-tabpanel-{$tabData['id']}",
+ 'role' => 'tabpanel',
+ 'tabindex' => 0,
+ 'aria-labelledby' => "tabber-tab-{$tabData['id']}"
], $content );
}
diff --git a/modules/ext.tabberNeue.codex/ext.tabberNeue.codex.js b/modules/ext.tabberNeue.codex/ext.tabberNeue.codex.js
index a6467c9..9f3951b 100644
--- a/modules/ext.tabberNeue.codex/ext.tabberNeue.codex.js
+++ b/modules/ext.tabberNeue.codex/ext.tabberNeue.codex.js
@@ -43,14 +43,14 @@ function main() {
tabbers.forEach( initApp );
}
-mw.hook( 'wikipage.content' ).add( function () {
+mw.hook( 'wikipage.content' ).add( () => {
main();
} );
/*
* Add hooks for Tabber when Visual Editor is used.
*/
-mw.loader.using( 'ext.visualEditor.desktopArticleTarget.init', function () {
+mw.loader.using( 'ext.visualEditor.desktopArticleTarget.init', () => {
// After saving edits
mw.hook( 'postEdit.afterRemoval' ).add( () => {
main();
diff --git a/modules/ext.tabberNeue.init/ext.tabberNeue.init.less b/modules/ext.tabberNeue.init/ext.tabberNeue.init.less
index 18c424f..76ba762 100644
--- a/modules/ext.tabberNeue.init/ext.tabberNeue.init.less
+++ b/modules/ext.tabberNeue.init/ext.tabberNeue.init.less
@@ -1,6 +1,11 @@
.tabber {
&__header {
box-shadow: inset 0 -1px 0 0 var( --border-color-base, #a2a9b1 );
+
+ &__prev,
+ &__next {
+ display: none;
+ }
}
&__tabs {
diff --git a/modules/ext.tabberNeue/Transclude.js b/modules/ext.tabberNeue/Transclude.js
index ace0270..b8e95db 100644
--- a/modules/ext.tabberNeue/Transclude.js
+++ b/modules/ext.tabberNeue/Transclude.js
@@ -49,6 +49,7 @@ class Transclude {
*/
async fetchDataFromUrl() {
try {
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
const response = await fetch( this.url, { method: 'GET', timeout: 5000, credentials: 'same-origin' } );
if ( !response.ok ) {
throw new Error( `Network response was not ok: ${ response.status } - ${ response.statusText }` );
diff --git a/modules/ext.tabberNeue/ext.tabberNeue.js b/modules/ext.tabberNeue/ext.tabberNeue.js
index 5a38a1d..bae88f8 100644
--- a/modules/ext.tabberNeue/ext.tabberNeue.js
+++ b/modules/ext.tabberNeue/ext.tabberNeue.js
@@ -87,28 +87,6 @@ class TabberAction {
} );
}
- /**
- * Animate and update the indicator position and width based on the active tab.
- *
- * @param {Element} indicator - The indicator element (optional, defaults to the first '.tabber__indicator' found in the parent).
- * @param {Element} activeTab - The currently active tab.
- * @param {Element} tablist - The parent element containing the tabs.
- */
- static animateIndicator( indicator, activeTab, tablist ) {
- const tablistScrollLeft = Util.roundScrollLeft( tablist.scrollLeft );
- const width = Util.getElementSize( activeTab, 'width' );
- const transformValue = activeTab.offsetLeft - tablistScrollLeft;
-
- indicator.classList.add( 'tabber__indicator--visible' );
- tablist.classList.add( 'tabber__tabs--animate' );
- indicator.style.width = width + 'px';
- indicator.style.transform = `translateX(${ transformValue }px)`;
- setTimeout( () => {
- indicator.classList.remove( 'tabber__indicator--visible' );
- tablist.classList.remove( 'tabber__tabs--animate' );
- }, 250 );
- }
-
/**
* Returns the tabpanel element based on the tab element
*
@@ -136,12 +114,13 @@ class TabberAction {
transclude.loadPage();
}
+ const activeTabpanelHeight = Util.getElementSize(
+ activeTabpanel,
+ 'height'
+ );
+ section.style.height = activeTabpanelHeight + 'px';
+
window.requestAnimationFrame( () => {
- const activeTabpanelHeight = Util.getElementSize(
- activeTabpanel,
- 'height'
- );
- section.style.height = activeTabpanelHeight + 'px';
// Scroll to tab
section.scrollLeft = activeTabpanel.offsetLeft;
} );
@@ -155,7 +134,6 @@ class TabberAction {
/**
* Sets the active tab in the tabber element.
* Updates the attributes of tabs and tab panels to reflect the active state.
- * Animates the indicator to the active tab and sets the active tab panel.
*
* @param {Element} activeTab - The tab element to set as active.
* @return {Promise} - A promise that resolves once the active tab is set.
@@ -164,9 +142,6 @@ class TabberAction {
return new Promise( ( resolve ) => {
const activeTabpanel = TabberAction.getTabpanel( activeTab );
const tabberEl = activeTabpanel.closest( '.tabber' );
- const indicator = tabberEl.querySelector(
- ':scope > .tabber__header > .tabber__indicator'
- );
const currentActiveTab = tabberEl.querySelector( ':scope > .tabber__header > .tabber__tabs > .tabber__tab[aria-selected="true"]' );
let currentActiveTabpanel;
@@ -204,12 +179,6 @@ class TabberAction {
Util.setAttributes( activeTab, activeTabAttributes );
Util.setAttributes( activeTabpanel, activeTabpanelAttributes );
-
- TabberAction.animateIndicator(
- indicator,
- activeTab,
- activeTab.parentElement
- );
TabberAction.setActiveTabpanel( activeTabpanel, currentActiveTabpanel );
resolve();
@@ -286,9 +255,6 @@ class TabberEvent {
this.tabs = this.tablist.querySelectorAll( ':scope > .tabber__tab' );
this.activeTab = this.tablist.querySelector( '[aria-selected="true"]' );
this.activeTabpanel = TabberAction.getTabpanel( this.activeTab );
- this.indicator = this.tabber.querySelector(
- ':scope > .tabber__header > .tabber__indicator'
- );
this.tabFocus = 0;
this.debouncedUpdateHeaderOverflow = mw.util.debounce(
() => TabberAction.updateHeaderOverflow( this.tablist ),
@@ -454,187 +420,31 @@ class TabberEvent {
}
/**
- * Class responsible for creating tabs, headers, and indicators for a tabber element.
+ * Class responsible for initalizing a tabber element.
*
* @class TabberBuilder
*/
class TabberBuilder {
constructor( tabber ) {
this.tabber = tabber;
- this.header = this.tabber.querySelector( ':scope > .tabber__header' );
- this.tablist = document.createElement( 'nav' );
- this.indicator = document.createElement( 'div' );
+ this.tablist = tabber.querySelector( ':scope > .tabber__header > .tabber__tabs' );
}
/**
* Sets the attributes of a tab element.
- *
- * @param {Element} tab - The tab element to set attributes for.
- * @param {string} tabId - The ID of the tab element.
*/
- setTabAttributes( tab, tabId ) {
+ setTabsAttributes() {
const tabAttributes = {
- class: 'tabber__tab',
- href: '#' + tabId,
- id: 'tab-' + tabId,
- role: 'tab',
tabindex: '-1',
- 'aria-selected': false,
- 'aria-controls': tabId
+ 'aria-selected': false
};
-
- Util.setAttributes( tab, tabAttributes );
- }
-
- /**
- * Creates a tab element with the given title attribute and tab ID.
- *
- * @param {string} titleAttr - The title attribute for the tab element.
- * @param {string} tabId - The ID of the tab element.
- * @return {Element} The created tab element.
- */
- createTab( titleAttr, tabId ) {
- const tab = document.createElement( 'a' );
-
- if ( config.parseTabName ) {
- tab.innerHTML = titleAttr;
- } else {
- tab.textContent = titleAttr;
+ for ( const tab of this.tablist.children ) {
+ Util.setAttributes( tab, tabAttributes );
}
-
- this.setTabAttributes( tab, tabId );
-
- return tab;
}
/**
- * Sets the attributes of a tab panel element.
- *
- * @param {Element} tabpanel - The tab panel element to set attributes for.
- * @param {string} tabId - The ID of the tab panel element.
- */
- setTabpanelAttributes( tabpanel, tabId ) {
- const tabpanelAttributes = {
- id: tabId,
- role: 'tabpanel',
- tabindex: '-1',
- 'aria-hidden': 'true',
- 'aria-labelledby': `tab-${ tabId }`
- };
-
- Util.setAttributes( tabpanel, tabpanelAttributes );
- }
-
- /**
- * Creates a tab element based on the provided tab panel.
- *
- * @param {Element} tabpanel - The tab panel element to create a tab element for.
- * @return {Element|false} The created tab element, or false if the title attribute is missing
- * or malformed.
- */
- createTabElement( tabpanel ) {
- const titleAttr = tabpanel.dataset.mwTabberTitle;
-
- if ( !titleAttr ) {
- mw.log.error(
- '[TabberNeue] Missing or malformed `data-mw-tabber-title` attribute'
- );
- return false;
- }
-
- let tabId;
- if ( config.parseTabName ) {
- tabId = Util.extractTextFromHtml( titleAttr );
- } else {
- tabId = titleAttr;
- }
-
- tabId = Hash.build( tabId, config.useLegacyTabIds );
-
- this.setTabpanelAttributes( tabpanel, tabId );
-
- return this.createTab( titleAttr, tabId );
- }
-
- /**
- * Creates tabs for the tabber.
- *
- * This method creates tab elements for each tab panel in the tabber.
- * It appends the created tabs to the tablist element, adds necessary attributes,
- * and sets the role attribute for accessibility.
- *
- * @return {Promise} A promise that resolves once all tabs are created and appended to the tablist.
- */
- createTabs() {
- return new Promise( ( resolve ) => {
- const fragment = document.createDocumentFragment();
- const tabpanels = this.tabber.querySelectorAll(
- ':scope > .tabber__section > .tabber__panel'
- );
- tabpanels.forEach( ( tabpanel ) => {
- fragment.append( this.createTabElement( tabpanel ) );
- } );
-
- this.tablist.append( fragment );
- this.tablist.classList.add( 'tabber__tabs' );
- this.tablist.setAttribute( 'role', 'tablist' );
- resolve();
- } );
- }
-
- /**
- * Creates the indicator element for the tabber.
- *
- * This method creates a div element to serve as the indicator for the active tab.
- * The indicator element is given a specific CSS class for styling and is appended to the tabber header.
- *
- * @return {Promise} A promise that resolves once the indicator element is created.
- */
- createIndicator() {
- return new Promise( ( resolve ) => {
- const indicator = document.createElement( 'div' );
- indicator.classList.add( 'tabber__indicator' );
- this.header.append( indicator );
- resolve();
- } );
- }
-
- /**
- * Creates the header for the tabber.
- *
- * This method creates two button elements, one for navigating to the previous tab and one for navigating to the next tab.
- * Each button element is created with the specified class and aria-label attributes.
- * The created buttons are appended to the header of the tabber.
- *
- * @return {Promise} A promise that resolves once the header is created.
- */
- createHeader() {
- return new Promise( ( resolve ) => {
- /**
- * Creates a button element with the specified class and aria-label.
- *
- * @param {string} className - The class name for the button element.
- * @param {string} ariaLabel - The aria-label attribute for the button element.
- * @return {Element} The created button element.
- */
- const createButton = ( className, ariaLabel ) => {
- const button = document.createElement( 'button' );
- // eslint-disable-next-line mediawiki/class-doc
- button.classList.add( className );
- button.setAttribute( 'aria-label', ariaLabel );
- return button;
- };
-
- const prevButton = createButton( 'tabber__header__prev', mw.message( 'tabberneue-button-prev' ).text() );
- const nextButton = createButton( 'tabber__header__next', mw.message( 'tabberneue-button-next' ).text() );
-
- this.header.append( prevButton, this.tablist, nextButton );
- resolve();
- } );
- }
-
- /**
- * Initializes the tabber by creating tabs, header, and indicator elements sequentially.
+ * Sets the tabs attributes
* Sets the active tab based on the URL hash, and updates the header overflow.
* Attaches event listeners for tabber interaction.
*
@@ -642,19 +452,14 @@ class TabberBuilder {
* @return {void}
*/
async init( urlHash ) {
- // Create tabs, header, and indicator elements sequentially
- await this.createTabs();
- await this.createHeader();
- await this.createIndicator();
-
- const activeTab = this.tablist.querySelector( `#tab-${ CSS.escape( urlHash ) }` ) || this.tablist.firstElementChild;
+ const activeTab = this.tablist.querySelector( `#tabber-tab-${ CSS.escape( urlHash ) }` ) || this.tablist.firstElementChild;
+ this.setTabsAttributes();
await TabberAction.setActiveTab( activeTab );
TabberAction.updateHeaderOverflow( this.tablist );
// Start attaching event
const tabberEvent = new TabberEvent( this.tabber, this.tablist );
tabberEvent.init();
-
this.tabber.classList.remove( 'tabber--init' );
this.tabber.classList.add( 'tabber--live' );
}
@@ -686,7 +491,7 @@ async function load( tabberEls ) {
TabberAction.toggleAnimation( true );
window.addEventListener( 'hashchange', ( event ) => {
const newHash = window.location.hash.slice( 1 );
- const tab = document.getElementById( `tab-${ CSS.escape( newHash ) }` );
+ const tab = document.getElementById( `tabber-tab-${ CSS.escape( newHash ) }` );
if ( tab ) {
event.preventDefault();
tab.click();
@@ -702,7 +507,7 @@ async function load( tabberEls ) {
* each tabber element.
*/
function main() {
- const tabberEls = document.querySelectorAll( '.tabber:not(.tabber--live)' );
+ const tabberEls = document.querySelectorAll( '.tabber--init' );
if ( tabberEls.length === 0 ) {
return;
diff --git a/modules/ext.tabberNeue/ext.tabberNeue.less b/modules/ext.tabberNeue/ext.tabberNeue.less
index 4f90da9..63e3bd4 100644
--- a/modules/ext.tabberNeue/ext.tabberNeue.less
+++ b/modules/ext.tabberNeue/ext.tabberNeue.less
@@ -9,12 +9,6 @@
--tabber-background-color-button-quiet--active: var( --background-color-button-quiet--active, rgba( 0, 24, 73, 0.082 ) ); // @background-color-button-quiet--active
--tabber-border-color-base: var( --border-color-base, #a2a9b1 ); // @border-color-base
--tabber-height-indicator: 2px;
- position: relative;
- display: flex;
-
- /* establish primary containing box */
- overflow: hidden;
- flex-direction: column;
&__header {
position: relative;
@@ -43,10 +37,10 @@
& &__prev,
& &__next {
position: absolute;
+ display: none; // Required as all:unset also unset display:none
z-index: 1;
top: 0;
bottom: 0;
- display: none;
width: 20px;
border-radius: 4px;
cursor: pointer;
@@ -83,18 +77,6 @@
}
}
- &__indicator {
- display: none;
- margin-top: ~'calc( var( --tabber-height-indicator ) * -1 )';
- background: var( --tabber-color-progressive );
- block-size: var( --tabber-height-indicator );
- inline-size: 0;
-
- &--visible {
- display: block;
- }
- }
-
&__tab {
&[ aria-selected='true' ] {
box-shadow: 0 -2px 0 var( --color-progressive, #36c ) inset;
diff --git a/modules/ve-tabberNeue/ve.ce.MWTabberNode.js b/modules/ve-tabberNeue/ve.ce.MWTabberNode.js
index fe1e8f9..c5a6292 100644
--- a/modules/ve-tabberNeue/ve.ce.MWTabberNode.js
+++ b/modules/ve-tabberNeue/ve.ce.MWTabberNode.js
@@ -64,49 +64,18 @@ ve.ce.MWTabberNode.prototype.onSetup = function () {
*/
ve.ce.MWTabberNode.prototype.renderHeader = function ( tabber ) {
const nestedTabbers = tabber.querySelectorAll( '.tabber__panel:first-child .tabber' );
-
const renderSingleHeader = function ( element ) {
- const
- tabPanels = element.querySelectorAll( ':scope > .tabber__section > .tabber__panel' ),
- header = element.querySelector( ':scope > .tabber__header' ),
- tabList = document.createElement( 'nav' ),
- indicator = document.createElement( 'div' ),
- fragment = new DocumentFragment();
-
- Array.prototype.forEach.call( tabPanels, function ( tabPanel, index ) {
- const tab = document.createElement( 'a' );
-
- tab.innerText = tabPanel.getAttribute( 'data-mw-tabber-title' );
- tab.classList.add( 'tabber__tab' );
-
- // Make first tab active
- if ( index === 0 ) {
- tab.setAttribute( 'aria-selected', true );
- }
-
- fragment.append( tab );
- } );
-
- tabList.append( fragment );
-
- tabList.classList.add( 'tabber__tabs' );
- indicator.classList.add( 'tabber__indicator' );
-
- header.append( tabList, indicator );
-
- indicator.style.width = tabList.firstElementChild.offsetWidth + 'px';
-
- element.classList.add( 'tabber--live' );
+ const firstTab = element.querySelector( ':scope > .tabber__header > .tabber__tabs > .tabber__tab' );
+ firstTab.setAttribute( 'aria-selected', true );
};
if ( nestedTabbers.length > 0 ) {
- Array.prototype.forEach.call( nestedTabbers, function ( nestedTabber ) {
+ Array.prototype.forEach.call( nestedTabbers, ( nestedTabber ) => {
renderSingleHeader( nestedTabber );
} );
}
renderSingleHeader( tabber );
-
lastHeader = tabber.firstElementChild;
};
diff --git a/modules/ve-tabberNeue/ve.ce.MWTabberTranscludeNode.js b/modules/ve-tabberNeue/ve.ce.MWTabberTranscludeNode.js
index 4ad017b..46c9046 100644
--- a/modules/ve-tabberNeue/ve.ce.MWTabberTranscludeNode.js
+++ b/modules/ve-tabberNeue/ve.ce.MWTabberTranscludeNode.js
@@ -70,7 +70,7 @@ ve.ce.MWTabberTranscludeNode.prototype.renderHeader = function ( tabber ) {
indicator = document.createElement( 'div' ),
fragment = new DocumentFragment();
- Array.prototype.forEach.call( tabPanels, function ( tabPanel, index ) {
+ Array.prototype.forEach.call( tabPanels, ( tabPanel, index ) => {
const tab = document.createElement( 'a' );
tab.innerText = tabPanel.getAttribute( 'data-mw-tabber-title' );
diff --git a/package-lock.json b/package-lock.json
index dfc3556..465c347 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,5 +1,5 @@
{
- "name": "mediawiki-extensions-TabberNeue",
+ "name": "TabberNeue",
"lockfileVersion": 3,
"requires": true,
"packages": {
@@ -1003,9 +1003,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001621",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001621.tgz",
- "integrity": "sha512-+NLXZiviFFKX0fk8Piwv3PfLPGtRqJeq2TiNoUff/qB5KJgwecJTvCXDpmlyP/eCI/GUEmp/h/y5j0yckiiZrA==",
+ "version": "1.0.30001680",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz",
+ "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==",
"dev": true,
"funding": [
{
@@ -1020,7 +1020,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
- ]
+ ],
+ "license": "CC-BY-4.0"
},
"node_modules/chalk": {
"version": "4.1.2",