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
This commit is contained in:
Roan Kattouw 2019-06-24 18:10:51 -07:00
parent e919258047
commit ec17da76c1
4 changed files with 126 additions and 38 deletions

View File

@ -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;

View File

@ -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 <https://github.com/othree/jquery.rtl-scroll-type>.
// Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
// Adaptation copied from OO.ui.Element.static.getScrollLeft
$definer = $( '<div>' ).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
};

View File

@ -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 ) {

View File

@ -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",