From 97cf735de2363bae3c01fd79976f6420176f0715 Mon Sep 17 00:00:00 2001 From: Jon Robson Date: Fri, 2 Sep 2022 07:57:31 -0700 Subject: [PATCH] Icons: Watchstar and wikilove are upgraded Can be tested by appending ?vectorvisualenhancementnext=1 to URL Bug: T310838 Bug: T234990 Bug: T234550 Depends-On: I76d0d94c9006cc5f5680849ecdd1c382c16e34ba Depends-On: Ib7c3021db014827b4b88cac855afc0b54a360f8c Change-Id: Ie2ffa5c3ecf270c1bb1f315937023ae7ace5ed30 --- includes/Hooks.php | 92 +++++++++++++++---- resources/skins.vector.js/skin.js | 4 + resources/skins.vector.js/watchstar.js | 24 +++++ .../components/MenuTabs.less | 51 ++++++++-- .../components/TabWatchstarLink.less | 15 ++- skin.json | 2 + tests/phpunit/integration/VectorHooksTest.php | 15 ++- 7 files changed, 173 insertions(+), 30 deletions(-) create mode 100644 resources/skins.vector.js/watchstar.js diff --git a/includes/Hooks.php b/includes/Hooks.php index 1e3db76c..78cb6520 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -153,7 +153,7 @@ class Hooks implements } /** - * Transforms watch item inside the action navigation menu + * Moves watch item from actions to views menu. * * @param array &$content_navigation */ @@ -168,15 +168,60 @@ class Hooks implements // Promote watch link from actions to views and add an icon if ( $key !== null ) { - self::appendClassToItem( - $content_navigation['actions'][$key]['class'], - [ 'icon' ] - ); $content_navigation['views'][$key] = $content_navigation['actions'][$key]; unset( $content_navigation['actions'][$key] ); } } + /** + * Adds icons to items in the "views" menu. + * + * @param array &$content_navigation + * @param bool $isLegacy is this the legacy Vector skin? + */ + private static function updateViewsMenuIcons( &$content_navigation, $isLegacy ) { + $featureManager = VectorServices::getFeatureManager(); + $visualEnhancements = $featureManager->isFeatureEnabled( Constants::FEATURE_VISUAL_ENHANCEMENTS ); + + foreach ( $content_navigation['views'] as $key => $item ) { + $icon = $item['icon'] ?? null; + if ( $icon ) { + if ( $isLegacy || !$featureManager->isFeatureEnabled( Constants::FEATURE_VISUAL_ENHANCEMENTS ) ) { + self::appendClassToItem( + $item['class'], + [ 'icon' ] + ); + } else { + // Force the item as a button with hidden text. + $item['button'] = true; + $item['text-hidden'] = true; + $item = self::updateMenuItemData( $item, true ); + } + } else { + self::appendClassToItem( + $item['class'], + [ 'vector-tab-noicon' ] + ); + } + $content_navigation['views'][$key] = $item; + } + } + + /** + * All associated pages menu items do not have icons so are given the vector-tab-noicon class. + * + * @param array &$content_navigation + */ + private static function updateAssociatedPagesMenuIcons( &$content_navigation ) { + foreach ( $content_navigation['associated-pages'] as $key => $item ) { + self::appendClassToItem( + $item['class'], + [ 'vector-tab-noicon' ] + ); + $content_navigation['associated-pages'][$key] = $item; + } + } + /** * Adds class to a property * @@ -376,9 +421,10 @@ class Hooks implements * @param array $item data to update * @param string $buttonClassProp property to append button classes * @param string $iconHtmlProp property to set icon HTML + * @param bool $isSmallIcon when set a small icon will be applied rather than the standard icon size * @return array $item Updated data */ - private static function updateItemData( $item, $buttonClassProp, $iconHtmlProp ) { + private static function updateItemData( $item, $buttonClassProp, $iconHtmlProp, $isSmallIcon = false ) { $hasButton = $item['button'] ?? false; $hideText = $item['text-hidden'] ?? false; $isCollapsible = $item['collapsible'] ?? false; @@ -403,6 +449,9 @@ class Hooks implements // We should seek to remove all these instances. 'mw-ui-icon-wikimedia-' . $icon ]; + if ( $isSmallIcon ) { + $iconElementClasses[] = 'mw-ui-icon-small'; + } self::appendClassToItem( $item[ $buttonClassProp ], $iconElementClasses ); } else { $item[ $iconHtmlProp ] = self::makeIcon( $icon ); @@ -439,12 +488,13 @@ class Hooks implements * Updates template data for Vector menu items. * * @param array $item menu item data to update + * @param bool $isSmallIcon when set a small icon will be applied rather than the standard icon size * @return array $item Updated menu item data */ - public static function updateMenuItemData( $item ) { + public static function updateMenuItemData( $item, $isSmallIcon = false ) { $buttonClassProp = 'link-class'; $iconHtmlProp = 'link-html'; - return self::updateItemData( $item, $buttonClassProp, $iconHtmlProp ); + return self::updateItemData( $item, $buttonClassProp, $iconHtmlProp, $isSmallIcon ); } /** @@ -500,19 +550,27 @@ class Hooks implements $title = $sk->getRelevantTitle(); $skinName = $sk->getSkinName(); - if ( self::isVectorSkin( $skinName ) ) { - if ( - $sk->getConfig()->get( 'VectorUseIconWatch' ) && - $title && $title->canExist() - ) { - self::updateActionsMenu( $content_navigation ); - } - - self::updateUserLinksItems( $sk, $content_navigation ); + // These changes should only happen in Vector. + if ( !$skinName || !self::isVectorSkin( $skinName ) ) { + return; } + + if ( + $sk->getConfig()->get( 'VectorUseIconWatch' ) && + $title && $title->canExist() + ) { + self::updateActionsMenu( $content_navigation ); + } + + self::updateUserLinksItems( $sk, $content_navigation ); if ( $skinName === Constants::SKIN_NAME_MODERN ) { self::createMoreOverflowMenu( $content_navigation ); } + + // The updating of the views menu happens /after/ the overflow menu has been created + // this avoids icons showing in the more overflow menu. + self::updateViewsMenuIcons( $content_navigation, self::isSkinVersionLegacy( $skinName ) ); + self::updateAssociatedPagesMenuIcons( $content_navigation ); } /** diff --git a/resources/skins.vector.js/skin.js b/resources/skins.vector.js/skin.js index ec2b4132..7118df61 100644 --- a/resources/skins.vector.js/skin.js +++ b/resources/skins.vector.js/skin.js @@ -2,6 +2,7 @@ var languageButton = require( './languageButton.js' ), initSearchLoader = require( './searchLoader.js' ).initSearchLoader, dropdownMenus = require( './dropdownMenus.js' ).dropdownMenus, sidebarPersistence = require( './sidebarPersistence.js' ), + watchstar = require( './watchstar.js' ), checkbox = require( './checkbox.js' ); /** @@ -74,6 +75,9 @@ function main( window ) { languageButton(); dropdownMenus(); addNamespacesGadgetSupport(); + if ( document.body.classList.contains( 'vector-feature-visual-enhancement-next-enabled' ) ) { + watchstar(); + } } /** diff --git a/resources/skins.vector.js/watchstar.js b/resources/skins.vector.js/watchstar.js new file mode 100644 index 00000000..a6d6e20d --- /dev/null +++ b/resources/skins.vector.js/watchstar.js @@ -0,0 +1,24 @@ +module.exports = function () { + mw.hook( 'wikipage.watchlistChange' ).add( + function ( /** @type {boolean} */ isWatched, /** @type {string} */ expiry ) { + var watchElement = document.querySelectorAll( '#ca-watch a, #ca-unwatch a' )[ 0 ]; + if ( !watchElement ) { + return; + } + watchElement.classList.remove( + 'mw-ui-icon-wikimedia-unStar', + 'mw-ui-icon-wikimedia-star', + 'mw-ui-icon-wikimedia-halfStar' + ); + if ( isWatched ) { + if ( expiry === 'infinity' ) { + watchElement.classList.add( 'mw-ui-icon-wikimedia-unStar' ); + } else { + watchElement.classList.add( 'mw-ui-icon-wikimedia-halfStar' ); + } + } else { + watchElement.classList.add( 'mw-ui-icon-wikimedia-star' ); + } + } + ); +}; diff --git a/resources/skins.vector.styles/components/MenuTabs.less b/resources/skins.vector.styles/components/MenuTabs.less index ae027839..0cad6b6d 100644 --- a/resources/skins.vector.styles/components/MenuTabs.less +++ b/resources/skins.vector.styles/components/MenuTabs.less @@ -15,8 +15,11 @@ } /* focus and hover have outlines. Text underline interferes with bottom border */ - .mw-list-item a:focus, - .mw-list-item a:hover { + /* FIXME: Remove 2 not selectors when cache has cleared for Ie2ffa5c3ecf270c1bb1f315937023ae7ace5ed30 */ + .mw-list-item a:not( .mw-ui-icon ):focus, + .mw-list-item a:not( .mw-ui-icon ):hover, + .mw-list-item.vector-tab-noicon a:focus, + .mw-list-item.vector-tab-noicon a:hover { text-decoration: none; border-bottom: @border-width-base @border-style-base; } @@ -37,11 +40,18 @@ * Tab list item appearance. Applies to both
  • 's inside .vector-menu-tabs * and dropdown menus inside the article toolbar */ +// FIXME: Remove the body selector once Ie2ffa5c3ecf270c1bb1f315937023ae7ace5ed30 is in production +/* for cached HTML */ body:not( .vector-feature-visual-enhancement-next-enabled ) .vector-menu-tabs .mw-list-item, +.vector-menu-tabs .mw-list-item.vector-tab-noicon, +.mw-article-toolbar-container .vector-menu-dropdown { + margin: 0 @padding-horizontal-tabs; +} + .vector-menu-tabs .mw-list-item, .mw-article-toolbar-container .vector-menu-dropdown { float: left; white-space: nowrap; - margin: 0 @padding-horizontal-tabs; + margin-bottom: 0; /* overrides default `li` styling */ // target links inside of .vector-tab-menu // and dropdown menu headings inside the article toolbar. @@ -51,19 +61,42 @@ .vector-menu-heading { display: inline-flex; position: relative; - // Top & bottom padding to increase clickable area. - padding: 18px 0 7px 0; - // bottom margin to overlap border with toolbar border. - margin-bottom: -1px; cursor: pointer; - border-bottom: @border-width-base @border-style-base transparent; // max-height & box-sizing to make link, watchstar & dropdown height consistent. // NOTE: Was 40px instead of 41, but changed to avoid visual regressions. max-height: unit( 41 / @font-size-tabs / @font-size-browser, em ); box-sizing: border-box; + font-weight: normal; + } + + .vector-menu-heading { // For better compatibility with gadgets (like Twinkle) that append //

    elements as dropdown headings (which was the convention in legacy Vector). font-size: inherit; - font-weight: normal; + } + + /* FIXME: Remove cached HTML selector (> a:not( .mw-ui-icon )) + when Ie2ffa5c3ecf270c1bb1f315937023ae7ace5ed30 is in production */ + &.vector-tab-noicon > a, + & > a:not( .mw-ui-icon ), + .vector-menu-heading { + // Top & bottom padding to increase clickable area. + padding: 18px 0 7px 0; + // bottom margin to overlap border with toolbar border. + margin-bottom: -1px; + } +} + +// With mw-ui-icons, expand watchstar and wikilove links it to cover full touch area +.vector-feature-visual-enhancement-next-enabled { + .vector-menu-tabs { + .mw-list-item { + .mw-ui-icon { + // Align small icons with the bottom of the tabs. + // Height of tab is 41px, and small icon is 36px, + // With 1px border, 41 - 36 + 1; + margin: 4px 0 0 0; + } + } } } diff --git a/resources/skins.vector.styles/components/TabWatchstarLink.less b/resources/skins.vector.styles/components/TabWatchstarLink.less index 35c110ec..f0fa877a 100644 --- a/resources/skins.vector.styles/components/TabWatchstarLink.less +++ b/resources/skins.vector.styles/components/TabWatchstarLink.less @@ -5,7 +5,8 @@ /* Watch/Unwatch Icon Styling */ /* Only use icon if the menu item is not collapsed into the "More" dropdown * (in which case it is inside `.vector-menu-dropdown` instead of `.vector-menu-tabs`). */ -.vector-menu-tabs { +// Note: there's no watchstar for anon users so no need to worry about cached HTML when changing this class +.vector-feature-visual-enhancement-next-disabled .vector-menu-tabs { @size-watchlink-icon: unit( 16 / @font-size-tabs / @font-size-browser, em ); .mw-watchlink.icon a { @@ -59,3 +60,15 @@ transform-origin: 50% 50%; } } + +// Loading watchstar link class. +.vector-feature-visual-enhancement-next-enabled { + .mw-watchlink .loading:before { + .rotation( 500ms ); + /* Suppress the hilarious rotating focus outline on Firefox */ + outline: 0; + cursor: default; + pointer-events: none; + transform-origin: 50% 50%; + } +} diff --git a/skin.json b/skin.json index 6007e297..e9fb2d68 100644 --- a/skin.json +++ b/skin.json @@ -311,6 +311,7 @@ } }, "icons": [ + "heart", "language", "ellipsis", "userAvatar", @@ -387,6 +388,7 @@ "name": "resources/skins.vector.js/config.json", "callback": "MediaWiki\\Skins\\Vector\\Hooks::getVectorResourceLoaderConfig" }, + "resources/skins.vector.js/watchstar.js", "resources/skins.vector.js/dropdownMenus.js", "resources/skins.vector.js/checkbox.js", "resources/skins.vector.js/sidebarPersistence.js", diff --git a/tests/phpunit/integration/VectorHooksTest.php b/tests/phpunit/integration/VectorHooksTest.php index 3bb116d6..f4cd9334 100644 --- a/tests/phpunit/integration/VectorHooksTest.php +++ b/tests/phpunit/integration/VectorHooksTest.php @@ -394,19 +394,24 @@ class VectorHooksTest extends MediaWikiIntegrationTestCase { */ public function testOnSkinTemplateNavigation() { $this->setMwGlobals( [ - 'wgVectorUseIconWatch' => true + 'wgVectorUseIconWatch' => true, + 'wgVectorVisualEnhancementNext' => false, ] ); $skin = new SkinVector22( [ 'name' => 'vector' ] ); $skin->getContext()->setTitle( Title::newFromText( 'Foo' ) ); $contentNavWatch = [ + 'associated-pages' => [], + 'views' => [], 'actions' => [ - 'watch' => [ 'class' => [ 'watch' ] ], + 'watch' => [ 'class' => [ 'watch' ], 'icon' => 'star' ], ] ]; $contentNavUnWatch = [ + 'associated-pages' => [], + 'views' => [], 'actions' => [ 'move' => [ 'class' => [ 'move' ] ], - 'unwatch' => [], + 'unwatch' => [ 'icon' => 'unStar' ], ], ]; @@ -433,6 +438,8 @@ class VectorHooksTest extends MediaWikiIntegrationTestCase { public function testUpdateUserLinksItems() { $vector2022Skin = new SkinVector22( [ 'name' => 'vector-2022' ] ); $contentNav = [ + 'associated-pages' => [], + 'views' => [], 'user-page' => [ 'userpage' => [ 'class' => [], 'icon' => 'userpage' ], ], @@ -442,6 +449,8 @@ class VectorHooksTest extends MediaWikiIntegrationTestCase { ]; $vectorLegacySkin = new SkinVectorLegacy( [ 'name' => 'vector' ] ); $contentNavLegacy = [ + 'associated-pages' => [], + 'views' => [], 'user-page' => [ 'userpage' => [ 'class' => [], 'icon' => 'userpage' ], ]