diff --git a/bundlesize.config.json b/bundlesize.config.json
index c99039610..8ef1adb95 100644
--- a/bundlesize.config.json
+++ b/bundlesize.config.json
@@ -5,7 +5,7 @@
},
{
"resourceModule": "skins.vector.styles",
- "maxSize": "9.62 kB"
+ "maxSize": "9.7 kB"
},
{
"resourceModule": "skins.vector.legacy.js",
diff --git a/includes/Hooks.php b/includes/Hooks.php
index ec09a101a..c658ca601 100644
--- a/includes/Hooks.php
+++ b/includes/Hooks.php
@@ -381,14 +381,6 @@ class Hooks {
$bodyAttrs['class'] .= ' skin-vector-search-vue';
}
- if (
- VectorServices::getFeatureManager()->isFeatureEnabled(
- Constants::FEATURE_STICKY_HEADER
- )
- ) {
- $bodyAttrs['class'] .= ' skin-vector-sticky-header';
- }
-
$config = $sk->getConfig();
// Should we disable the max-width styling?
if ( !self::isSkinVersionLegacy() && $sk->getTitle() && self::shouldDisableMaxWidth(
diff --git a/includes/SkinVector.php b/includes/SkinVector.php
index 1cafe934e..8bc11f72d 100644
--- a/includes/SkinVector.php
+++ b/includes/SkinVector.php
@@ -44,6 +44,10 @@ class SkinVector extends SkinMustache {
/** @var int */
private const MENU_TYPE_DROPDOWN = 2;
private const MENU_TYPE_PORTAL = 3;
+ private const NO_ICON = [
+ 'icon' => 'none',
+ 'class' => 'sticky-header-icon'
+ ];
/**
* T243281: Code used to track clicks to opt-out link.
@@ -296,6 +300,24 @@ class SkinVector extends SkinMustache {
Hooks::onSkinTemplateNavigation( $skin, $content_navigation );
}
+ /**
+ * Generate data needed to generate the sticky header.
+ * Lack of i18n is intentional and will be done as part of follow up work.
+ * @return array
+ */
+ private function getStickyHeaderData() {
+ return [
+ 'title' => 'Audre Lorde',
+ 'heading' => 'Introduction',
+ 'primary-action' => 'Primary action',
+ 'data-icon-start' => self::NO_ICON,
+ 'data-icon-end' => self::NO_ICON,
+ 'data-icons' => [
+ self::NO_ICON, self::NO_ICON, self::NO_ICON, self::NO_ICON
+ ]
+ ];
+ }
+
/**
* @inheritDoc
*/
@@ -338,7 +360,9 @@ class SkinVector extends SkinMustache {
'sidebar-visible' => $this->isSidebarVisible(),
- 'is-language-in-header' => $this->isLanguagesInHeader(),
+ 'data-vector-sticky-header' => VectorServices::getFeatureManager()->isFeatureEnabled(
+ Constants::FEATURE_STICKY_HEADER
+ ) ? $this->getStickyHeaderData() : false,
] );
if ( $skin->getUser()->isRegistered() ) {
diff --git a/includes/templates/Icon.mustache b/includes/templates/Icon.mustache
new file mode 100644
index 000000000..e3496b8c8
--- /dev/null
+++ b/includes/templates/Icon.mustache
@@ -0,0 +1 @@
+
diff --git a/includes/templates/StickyHeader.mustache b/includes/templates/StickyHeader.mustache
new file mode 100644
index 000000000..7f102b742
--- /dev/null
+++ b/includes/templates/StickyHeader.mustache
@@ -0,0 +1,29 @@
+
diff --git a/includes/templates/skin.mustache b/includes/templates/skin.mustache
index 5a1ac2a36..8abc65d4f 100644
--- a/includes/templates/skin.mustache
+++ b/includes/templates/skin.mustache
@@ -42,7 +42,6 @@
{{#sidebar-visible}}checked{{/sidebar-visible}}>
{{>Header}}
-
{{>Navigation}}
@@ -98,3 +97,6 @@
{{! END mw-page-container-inner }}
{{! END mw-page-container }}
+{{#data-vector-sticky-header}}
+{{>StickyHeader}}
+{{/data-vector-sticky-header}}
diff --git a/resources/common/variables.less b/resources/common/variables.less
index 937e37f71..37a46858f 100644
--- a/resources/common/variables.less
+++ b/resources/common/variables.less
@@ -152,3 +152,9 @@
// Transitions
@transition-duration-base: 100ms;
+
+//
+// Layout
+//
+@max-width-page-container: unit( 1650px / @font-size-browser, em ); // 103.125em @ 16
+@padding-horizontal-page-container: unit( 30px / @font-size-browser, em ); // 1.875em @ 16
diff --git a/resources/skins.vector.js/skin.js b/resources/skins.vector.js/skin.js
index 08f01c2bf..547cdfb48 100644
--- a/resources/skins.vector.js/skin.js
+++ b/resources/skins.vector.js/skin.js
@@ -1,5 +1,6 @@
var collapsibleTabs = require( '../skins.vector.legacy.js/collapsibleTabs.js' ),
vector = require( '../skins.vector.legacy.js/vector.js' ),
+ stickyHeader = require( './stickyHeader.js' ),
languageButton = require( './languageButton.js' ),
initSearchLoader = require( './searchLoader.js' ).initSearchLoader,
dropdownMenus = require( './dropdownMenus.js' ),
@@ -72,6 +73,7 @@ function main( window ) {
initSearchLoader( document );
searchToggle();
languageButton();
+ stickyHeader();
}
main( window );
diff --git a/resources/skins.vector.js/stickyHeader.js b/resources/skins.vector.js/stickyHeader.js
new file mode 100644
index 000000000..bb2cfd37d
--- /dev/null
+++ b/resources/skins.vector.js/stickyHeader.js
@@ -0,0 +1,8 @@
+module.exports = function () {
+ var header = document.getElementById( 'vector-sticky-header' );
+ if ( !header ) {
+ return;
+ }
+ // TODO: Use IntersectionObserver
+ header.classList.add( 'vector-sticky-header-visible' );
+};
diff --git a/resources/skins.vector.styles/components/StickyHeader.less b/resources/skins.vector.styles/components/StickyHeader.less
new file mode 100644
index 000000000..b218f7381
--- /dev/null
+++ b/resources/skins.vector.styles/components/StickyHeader.less
@@ -0,0 +1,73 @@
+@import '../../common/variables.less';
+@import 'mediawiki.mixins.less';
+
+.vector-sticky-header {
+ width: 100%;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: @z-index-header;
+ transform: translateY( -100% );
+ transition: transform 250ms linear;
+ display: flex;
+ align-items: center;
+ max-width: @max-width-page-container + @padding-horizontal-page-container + @padding-horizontal-page-container;
+ margin: 0 auto;
+ background: @background-color-base;
+ background-color: #fffffff7;
+ border-bottom: 1px solid @colorGray14;
+ // FIXME: Should this adapt to different thresholds? Ask Alex!
+ padding: 6px 8px 6px 10px;
+ justify-content: space-between;
+ box-sizing: border-box;
+
+ @media ( min-width: @width-breakpoint-desktop ) {
+ padding: 6px 25px;
+ }
+
+ &-visible {
+ transform: translateY( 0% );
+ }
+
+ //
+ // Layout
+ //
+ &-start {
+ display: flex;
+ align-items: center;
+ }
+
+ &-end {
+ display: flex;
+ align-items: center;
+ }
+
+ //
+ // Components
+ //
+ &-icons,
+ &-context-bar {
+ display: flex;
+ align-items: center;
+ white-space: nowrap;
+ margin: 0 15px;
+ padding-left: 30px;
+ }
+
+ &-context-bar {
+ border-left: 1px solid #c8c8c8;
+ }
+
+ &-context-bar-primary {
+ padding-right: 15px;
+ font-size: unit( 22 / @font-size-browser, em );
+ }
+
+ &-context-bar-secondary {
+ &:before {
+ padding-right: 15px;
+ content: '|';
+ }
+ }
+}
diff --git a/resources/skins.vector.styles/layouts/screen.less b/resources/skins.vector.styles/layouts/screen.less
index daec540c3..32a6472cf 100644
--- a/resources/skins.vector.styles/layouts/screen.less
+++ b/resources/skins.vector.styles/layouts/screen.less
@@ -65,9 +65,7 @@
// Page container
-@max-width-page-container: unit( 1650px / @font-size-browser, em ); // 103.125em @ 16
@min-width-page-container--padded: @max-width-page-container + ( 2 * @padding-horizontal-page-container ); // 106.875em
-@padding-horizontal-page-container: unit( 30px / @font-size-browser, em ); // 1.875em @ 16
// Content containers
@@ -147,12 +145,6 @@ body {
// allow z-index to apply so search results overlay article
position: relative;
z-index: @z-index-header;
-
- .skin-vector-sticky-header & {
- position: sticky;
- top: 0;
- background: @background-color-base;
- }
}
/* Searchbox */
diff --git a/resources/skins.vector.styles/skin.less b/resources/skins.vector.styles/skin.less
index 89db5feb9..d5e91d4fd 100644
--- a/resources/skins.vector.styles/skin.less
+++ b/resources/skins.vector.styles/skin.less
@@ -16,6 +16,7 @@
@import './components/Sidebar.less';
@import './components/LanguageButton.less';
@import './components/UserLinks.less';
+ @import './components/StickyHeader.less';
}
@media all {
diff --git a/skin.json b/skin.json
index 685c80887..47fc526a7 100644
--- a/skin.json
+++ b/skin.json
@@ -192,6 +192,7 @@
"name": "resources/skins.vector.js/config.json",
"callback": "Vector\\Hooks::getVectorResourceLoaderConfig"
},
+ "resources/skins.vector.js/stickyHeader.js",
"resources/skins.vector.js/dropdownMenus.js",
"resources/skins.vector.js/sidebar.js",
"resources/skins.vector.legacy.js/collapsibleTabs.js",
diff --git a/stories/StickyHeader.stories.data.js b/stories/StickyHeader.stories.data.js
new file mode 100644
index 000000000..eae6cbebc
--- /dev/null
+++ b/stories/StickyHeader.stories.data.js
@@ -0,0 +1,25 @@
+import template from '!!raw-loader!../includes/templates/StickyHeader.mustache';
+import Icon from '!!raw-loader!../includes/templates/Icon.mustache';
+
+const NO_ICON = {
+ icon: 'none',
+ class: 'sticky-header-icon'
+};
+
+const data = {
+ title: 'Audre Lorde',
+ heading: 'Introduction',
+ 'primary-action': 'Primary action',
+ 'is-visible': true,
+ 'data-icon-start': NO_ICON,
+ 'data-icon-end': NO_ICON,
+ 'data-icons': [
+ NO_ICON, NO_ICON, NO_ICON, NO_ICON
+ ]
+};
+
+export const STICKY_HEADER_TEMPLATE_PARTIALS = {
+ Icon
+};
+
+export { template, data };
diff --git a/stories/StickyHeader.stories.js b/stories/StickyHeader.stories.js
new file mode 100644
index 000000000..48052b407
--- /dev/null
+++ b/stories/StickyHeader.stories.js
@@ -0,0 +1,13 @@
+import mustache from 'mustache';
+import '../resources/skins.vector.styles/components/StickyHeader.less';
+
+import { template, data,
+ STICKY_HEADER_TEMPLATE_PARTIALS } from './StickyHeader.stories.data';
+
+export default {
+ title: 'StickyHeader'
+};
+
+export const stickyHeader = () => mustache.render(
+ template, data, STICKY_HEADER_TEMPLATE_PARTIALS
+);