From ec17da76c18c93dc86762be037d471575b1ac48a Mon Sep 17 00:00:00 2001 From: Roan Kattouw Date: Mon, 24 Jun 2019 18:10:51 -0700 Subject: [PATCH] Improve tab scrolling logic * If the leftmost tab is selected, scroll all the way to the left * If the rightmost tab is selected, scroll all the way to the right * If a tab in the middle is selected, scroll to center it * If the selected tab is wider than the tab container, make sure its start (left edge in LTR, right edge in RTL) is always made visible. As Bartosz reminded me, .scrollLeft in RTL is a cross-browser nightmare (see https://github.com/othree/jquery.rtl-scroll-type), so add a bunch of code working around this. Some of this logic is in OOUI already, but what's there is not enough for what we need here, and we also don't want to load OOUI for this. Bug: T223142 Change-Id: Ica298954b42f9daa4819043ec24bc0266290a927 --- resources/skins.minerva.amc.styles/tabs.less | 4 + resources/skins.minerva.scripts/TabScroll.js | 119 +++++++++++++++++++ resources/skins.minerva.scripts/init.js | 40 +------ skin.json | 1 + 4 files changed, 126 insertions(+), 38 deletions(-) create mode 100644 resources/skins.minerva.scripts/TabScroll.js diff --git a/resources/skins.minerva.amc.styles/tabs.less b/resources/skins.minerva.amc.styles/tabs.less index c77c30d..60d8906 100644 --- a/resources/skins.minerva.amc.styles/tabs.less +++ b/resources/skins.minerva.amc.styles/tabs.less @@ -21,6 +21,10 @@ text-decoration: none; } + &:last-child { + margin-right: 0; + } + // note core doesn't use BEM. &.selected { border-bottom: (@pageActionBorder * 2) solid @colorGray5; diff --git a/resources/skins.minerva.scripts/TabScroll.js b/resources/skins.minerva.scripts/TabScroll.js new file mode 100644 index 0000000..66c3f6f --- /dev/null +++ b/resources/skins.minerva.scripts/TabScroll.js @@ -0,0 +1,119 @@ +var scrollLeftStyle = null; + +function testScrollLeftStyle() { + var definer, $definer; + if ( scrollLeftStyle !== null ) { + return scrollLeftStyle; + } + // Detect which scrollLeft style the browser uses + // Adapted from . + // Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License. + // Adaptation copied from OO.ui.Element.static.getScrollLeft + $definer = $( '
' ).attr( { + dir: 'rtl', + style: 'font-size: 14px; width: 4px; height: 1px; position: absolute; top: -1000px; overflow: scroll;' + } ).text( 'ABCD' ); + $definer.appendTo( 'body' ); + definer = $definer[ 0 ]; + if ( definer.scrollLeft > 0 ) { + // Safari, Chrome + scrollLeftStyle = 'default'; + } else { + definer.scrollLeft = 1; + if ( definer.scrollLeft === 0 ) { + // Firefox, old Opera + scrollLeftStyle = 'negative'; + } else { + // Internet Explorer, Edge + scrollLeftStyle = 'reverse'; + } + } + $definer.remove(); + return scrollLeftStyle; +} + +/** + * When tabs are present and one is selected, scroll the selected tab into view. + * @return {void} + */ +function initTabsScrollPosition() { + var selectedTab, tabContainer, $tabContainer, maxScrollLeft, leftMostChild, rightMostChild, + dir, widthDiff, tabPosition, containerPosition, left, increaseScrollLeft, + // eslint-disable-next-line no-jquery/no-global-selector + $selectedTab = $( '.minerva__tab.selected' ); + + /** + * Set tabContainer.scrollLeft, with adjustments for browser inconsistencies in RTL + * @param {number} sl New .scrollLeft value, in 'default' (WebKit) style + */ + function setScrollLeft( sl ) { + if ( dir === 'ltr' ) { + tabContainer.scrollLeft = sl; + return; + } + + if ( testScrollLeftStyle() === 'reverse' ) { + sl = maxScrollLeft - sl; + } else if ( testScrollLeftStyle() === 'negative' ) { + sl = -( maxScrollLeft - sl ); + } + tabContainer.scrollLeft = sl; + } + + if ( $selectedTab.length !== 1 ) { + return; + } + selectedTab = $selectedTab.get( 0 ); + $tabContainer = $selectedTab.closest( '.minerva__tab-container' ); + tabContainer = $tabContainer.get( 0 ); + maxScrollLeft = tabContainer.scrollWidth - tabContainer.clientWidth; + dir = $tabContainer.css( 'direction' ) || 'ltr'; + leftMostChild = dir === 'ltr' ? tabContainer.firstElementChild : tabContainer.lastElementChild; + rightMostChild = dir === 'ltr' ? tabContainer.lastElementChild : tabContainer.firstElementChild; + // If the tab is wider than the container (doesn't fit), this value will be negative + widthDiff = tabContainer.clientWidth - selectedTab.clientWidth; + + if ( selectedTab === leftMostChild ) { + // The left-most tab is selected. If the tab fits, scroll all the way to the left. + // If the tab doesn't fit, align its start edge with the container's start edge. + if ( dir === 'ltr' || widthDiff >= 0 ) { + setScrollLeft( 0 ); + } else { + setScrollLeft( -widthDiff ); + } + } else if ( selectedTab === rightMostChild ) { + // The right-most tab is selected. If the tab fits, scroll all the way to the right. + // If the tab doesn't fit, align its start edge with the container's start edge. + if ( dir === 'rtl' || widthDiff >= 0 ) { + setScrollLeft( maxScrollLeft ); + } else { + setScrollLeft( maxScrollLeft + widthDiff ); + } + } else { + // The selected tab is not the left-most or right-most, it's somewhere in the middle + tabPosition = $selectedTab.position(); + containerPosition = $tabContainer.position(); + // Position of the left edge of $selectedTab relative to the left edge of $tabContainer + left = tabPosition.left - containerPosition.left; + // Because the calculations above use the existing .scrollLeft from the browser, + // we should not use setScrollLeft() here. Instead, we rely on the fact that scrollLeft + // increases to the left in the 'default' and 'negative' modes, and to the right in + // the 'reverse' mode, so we can add/subtract a delta to/from scrollLeft accordingly. + if ( widthDiff >= 0 ) { + // The tab fits, center it + increaseScrollLeft = left - widthDiff / 2; + } else if ( dir === 'ltr' ) { + // The tab doesn't fit (LTR), align its left edge with the container's left edge + increaseScrollLeft = left; + } else { + // The tab doesn't fit (RTL), align its right edge with the container's right edge + increaseScrollLeft = left - widthDiff; + } + tabContainer.scrollLeft += increaseScrollLeft * + ( testScrollLeftStyle() === 'reverse' ? -1 : 1 ); + } +} + +module.exports = { + initTabsScrollPosition: initTabsScrollPosition +}; diff --git a/resources/skins.minerva.scripts/init.js b/resources/skins.minerva.scripts/init.js index 5b5e603..71055b5 100644 --- a/resources/skins.minerva.scripts/init.js +++ b/resources/skins.minerva.scripts/init.js @@ -16,6 +16,7 @@ issues = require( './page-issues/index.js' ), Toolbar = require( './Toolbar.js' ), ToggleList = require( '../../components/ToggleList/ToggleList.js' ), + TabScroll = require( './TabScroll.js' ), router = require( 'mediawiki.router' ), CtaDrawer = mobile.CtaDrawer, desktopMMV = mw.loader.getState( 'mmv.bootstrap' ), @@ -303,43 +304,6 @@ } ); } - /** - * When tabs are present and one is selected, scroll the selected tab into view. - * @return {void} - */ - function initTabsScrollPosition() { - var $tabContainer, tabPosition, containerPosition, left, right, - // eslint-disable-next-line no-jquery/no-global-selector - $selectedTab = $( '.minerva__tab.selected' ); - if ( $selectedTab.length !== 1 ) { - return; - } - - $tabContainer = $selectedTab.closest( '.minerva__tab-container' ); - tabPosition = $selectedTab.position(); - containerPosition = $tabContainer.position(); - // Position of the left edge of $selectedTab relative to the left edge of $tabContainer - left = tabPosition.left - containerPosition.left; - // Position of the right edge of $selectedTab relative to the left edge of $tabContainer - right = left + $selectedTab.outerWidth(); - - // If $selectedTab is (partly) scrolled out of view, scroll it into view - // This only considers and manipulates the horizontal scroll position within $tabContainer, - // not the vertical scroll position of the viewport - if ( left < 0 ) { - // Left edge of $selectedTab is to the left of the left edge of $tabContainer - // Scroll $tabContainer to the left, by subtracting the difference from its scrollLeft - // (we're subtracting here by adding a negative number) - $tabContainer.scrollLeft( $tabContainer.scrollLeft() + left ); - } else if ( right > $tabContainer.innerWidth() ) { - // Right edge of $selectedTab is to the right of the right edge of $tabContainer - // Scroll $tabContainer to the right, by adding the difference to its scrollLeft - $tabContainer.scrollLeft( - $tabContainer.scrollLeft() + right - $tabContainer.innerWidth() - ); - } - } - $( function () { var $toc, toolbarElement = document.querySelector( Toolbar.selector ), @@ -371,7 +335,7 @@ } initRedlinksCta(); initUserRedLinks(); - initTabsScrollPosition(); + TabScroll.initTabsScrollPosition(); // Setup the issues banner on the page // Pages which dont exist (id 0) cannot have issues if ( !currentPage.isMissing ) { diff --git a/skin.json b/skin.json index c7c3300..46d404d 100644 --- a/skin.json +++ b/skin.json @@ -535,6 +535,7 @@ "resources/skins.minerva.scripts/UriUtil.js", "resources/skins.minerva.scripts/TitleUtil.js", "components/ToggleList/ToggleList.js", + "resources/skins.minerva.scripts/TabScroll.js", "resources/skins.minerva.scripts/Toolbar.js", "resources/skins.minerva.scripts/initLogging.js", "resources/skins.minerva.scripts/mobileRedirect.js",