diff --git a/.stylelintrc.json b/.stylelintrc.json index 6a4025a..a402a13 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,10 +1,10 @@ -{ - "extends": [ - "stylelint-config-idiomatic-order", - "stylelint-config-wikimedia" - ], - "rules": { - "selector-max-id": null, - "selector-class-pattern": "^(tabber)" - } -} \ No newline at end of file +{ + "extends": [ + "stylelint-config-idiomatic-order", + "stylelint-config-wikimedia" + ], + "rules": { + "selector-max-id": null, + "selector-class-pattern": "^(tabber)" + } +} diff --git a/modules/ext.tabberNeue.js b/modules/ext.tabberNeue.js index 19f9b2a..73319f3 100644 --- a/modules/ext.tabberNeue.js +++ b/modules/ext.tabberNeue.js @@ -5,13 +5,16 @@ * @param {number} count */ function initTabber( tabber, count ) { + var ACTIVETABCLASS = 'tabber__tab--active'; + var tabPanels = tabber.querySelectorAll( ':scope > .tabber__section > .tabber__panel' ); var config = require( './config.json' ), header = tabber.querySelector( '.tabber__header' ), tabList = document.createElement( 'nav' ), prevButton = document.createElement( 'div' ), - nextButton = document.createElement( 'div' ); + nextButton = document.createElement( 'div' ), + indicator = document.createElement( 'div' ); var buildTabs = function () { var fragment = new DocumentFragment(); @@ -26,10 +29,14 @@ function initTabber( tabber, count ) { // check if the hash is already used before var hashCount = 0; - hashList.forEach( function(h) { hashCount += ( h == hash ) ? 1 : 0; } ); + hashList.forEach( + function ( h ) { + hashCount += ( h === hash ) ? 1 : 0; + } + ); // append counter if the same hash already used - hash += ( 1 == hashCount ) ? '' : ( '-' + hashCount ); + hash += ( hashCount === 1 ) ? '' : ( '-' + hashCount ); tabPanel.setAttribute( 'id', hash ); tabPanel.setAttribute( 'role', 'tabpanel' ); @@ -53,8 +60,9 @@ function initTabber( tabber, count ) { tabList.setAttribute( 'role', 'tablist' ); prevButton.classList.add( 'tabber__header__prev' ); nextButton.classList.add( 'tabber__header__next' ); + indicator.classList.add( 'tabber__indicator' ); - header.append( prevButton, tabList, nextButton ); + header.append( prevButton, tabList, nextButton, indicator ); }; var updateSectionHeight = function ( section, activePanel ) { @@ -85,6 +93,12 @@ function initTabber( tabber, count ) { } }; + var updateIndicator = function () { + var activeTab = tabList.querySelector( '.' + ACTIVETABCLASS ); + indicator.style.width = activeTab.offsetWidth + 'px'; + indicator.style.transform = 'translateX(' + ( activeTab.offsetLeft - tabList.scrollLeft ) + 'px)'; + }; + var resizeObserver = null; if ( window.ResizeObserver ) { resizeObserver = new ResizeObserver( mw.util.debounce( 250, onElementResize ) ); @@ -161,6 +175,7 @@ function initTabber( tabber, count ) { // Also triggered by side-scrolling using other means other than the buttons tabList.addEventListener( 'scroll', function () { updateButtons(); + updateIndicator(); } ); // Add class to enable animation @@ -215,8 +230,7 @@ function initTabber( tabber, count ) { * @param {boolean} scrollIntoView */ function showPanel( targetHash, allowRemoteLoad, scrollIntoView ) { - var ACTIVETABCLASS = 'tabber__tab--active', - ACTIVEPANELCLASS = 'tabber__panel--active', + var ACTIVEPANELCLASS = 'tabber__panel--active', targetPanel = document.getElementById( targetHash ), targetTab = document.getElementById( 'tab-' + targetHash ), section = targetPanel.parentNode, @@ -225,13 +239,13 @@ function initTabber( tabber, count ) { var loadTransclusion = function () { var loading = document.createElement( 'div' ), - indicator = document.createElement( 'div' ); + loadingIndicator = document.createElement( 'div' ); targetPanel.setAttribute( 'aria-live', 'polite' ); targetPanel.setAttribute( 'aria-busy', 'true' ); loading.setAttribute( 'class', 'tabber__transclusion--loading' ); - indicator.setAttribute( 'class', 'tabber__loading-indicator' ); - loading.appendChild( indicator ); + loadingIndicator.setAttribute( 'class', 'tabber__loading-indicator' ); + loading.appendChild( loadingIndicator ); targetPanel.textContent = ''; targetPanel.appendChild( loading ); loadPage( targetPanel, targetPanel.dataset.tabberLoadUrl ); @@ -264,6 +278,8 @@ function initTabber( tabber, count ) { targetPanel.classList.add( ACTIVEPANELCLASS ); targetPanel.setAttribute( 'aria-hidden', false ); + updateIndicator(); + // Lazyload transclusion if needed if ( allowRemoteLoad && targetPanel.dataset.tabberPendingLoad && @@ -357,7 +373,7 @@ function initTabber( tabber, count ) { // Switch to the first tab if no targetHash or no tab is detected and do not scroll to it // TODO: Remove the polyfill with CSS.escape when we are dropping IE support if ( !targetHash || !tabList.querySelector( '#tab-' + targetHash.replace( /[^a-zA-Z0-9-_]/g, '\\$&' ) ) ) { - targetHash = tabList.firstElementChild.getAttribute( 'id' ).substring( 4 ); + targetHash = tabList.firstElementChild.getAttribute( 'id' ).slice( 4 ); scrollIntoView = false; } @@ -373,7 +389,7 @@ function initTabber( tabber, count ) { // Respond to clicks on the nav tabs Array.prototype.forEach.call( tabList.children, function ( tab ) { tab.addEventListener( 'click', function ( event ) { - var targetHash = tab.getAttribute( 'href' ).substring( 1 ); + var targetHash = tab.getAttribute( 'href' ).slice( 1 ); event.preventDefault(); if ( !config || config.updateLocationOnTabChange ) { // Add hash to the end of the URL diff --git a/modules/ext.tabberNeue.less b/modules/ext.tabberNeue.less index 1dff432..d2a132d 100644 --- a/modules/ext.tabberNeue.less +++ b/modules/ext.tabberNeue.less @@ -31,6 +31,7 @@ &__header { position: relative; display: flex; + flex-direction: column; /* defend against
needing 100% */ flex-shrink: 0; box-shadow: inset 0 -1px 0 0 #a2a9b1; @@ -89,6 +90,13 @@ } } + &__indicator { + border-radius: 2px; + background: #36c; + block-size: 2px; + inline-size: 0; + } + &__header, &__section { scrollbar-width: none; @@ -120,7 +128,6 @@ &--active, &--active:visited { - box-shadow: inset 0 -2px 0 0 #36c; color: #36c; } } @@ -207,12 +214,10 @@ .tabber { &__tab { &:hover { - box-shadow: inset 0 -2px 0 0 #447ff5; color: #447ff5; } &:active { - box-shadow: inset 0 -2px 0 0 #2a4b8d; color: #2a4b8d; } } @@ -234,13 +239,26 @@ // Smooth scrolling through a large number of panels hurt performance on mobile // Also it will trigger unnessecary lazyloading as lazyload content show up momentarily -@media ( prefers-reduced-motion: no-preference ) and ( min-width: 720px ) { +@media ( prefers-reduced-motion: no-preference ) { + .tabber { + &__header { + scroll-behavior: smooth; + } + + &__indicator { + // Placeholder animation + // TODO: Allow indicator to sync with panel scrolling + transition: transform 250ms ease, width 250ms ease; + } + } + .tabber--animate { .tabber { - &__header, &__section, &__tabs { - scroll-behavior: smooth; + @media ( min-width: 720px ) { + scroll-behavior: smooth; + } } } }