diff --git a/bundlesize.config.json b/bundlesize.config.json index c990396..8ef1adb 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 ec09a10..c658ca6 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 5490db8..c1815b2 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 0000000..e3496b8 --- /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 0000000..7f102b7 --- /dev/null +++ b/includes/templates/StickyHeader.mustache @@ -0,0 +1,29 @@ +
+
+
+ {{#data-icon-start}} + {{>Icon}} + {{/data-icon-start}} +
+
+
{{title}}
+
{{heading}}
+
+
+
+
+ {{#data-icons}} + {{>Icon}} + {{/data-icons}} +
+
+ {{primary-action}} +
+
+ {{#data-icon-end}} + {{>Icon}} + {{/data-icon-end}} +
+
+
diff --git a/includes/templates/skin.mustache b/includes/templates/skin.mustache index 5a1ac2a..8abc65d 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 937e37f..37a4685 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 08f01c2..547cdfb 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 0000000..bb2cfd3 --- /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 0000000..b218f73 --- /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 d9ce73b..593ca45 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 89db5fe..d5e91d4 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 bb43e9a..df27612 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 0000000..eae6cbe --- /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 0000000..48052b4 --- /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 +);