Merge "Echo uses Button template"

This commit is contained in:
jenkins-bot 2023-08-08 17:44:22 +00:00 committed by Gerrit Code Review
commit 0c69779b73
9 changed files with 351 additions and 129 deletions

View file

@ -1,5 +1,5 @@
<{{^tag-name}}button{{/tag-name}}{{#tag-name}}{{.}}{{/tag-name}}
{{#attributes}}{{key}}="{{value}}"{{/attributes}}
{{#array-attributes}}{{key}}="{{value}}"{{/array-attributes}}
class="{{classes}} mw-ui-button {{#data-icon}}mw-ui-icon-element mw-ui-quiet{{/data-icon}}">
{{#data-icon}}{{>IconLegacy}}{{/data-icon}} <span>{{ label }}</span>
</{{^tag-name}}button{{/tag-name}}{{#tag-name}}{{.}}{{/tag-name}}>

View file

@ -19,14 +19,15 @@
{{>SearchBox}}
{{/data-minerva-search-box}}
<nav class="minerva-user-navigation" aria-label="{{msg-minerva-user-navigation}}">
{{^is-anon}}
<div class="minerva-user-notifications"
{{#is-minerva-echo-single-button}}id="pt-notifications-alert"{{/is-minerva-echo-single-button}}>
{{#data-portlets.data-notifications}}
<ul>{{{html-items}}}</ul>
{{/data-portlets.data-notifications}}
{{#data-minerva-notifications}}
<div class="minerva-notifications">
<ul>
{{#array-buttons}}
<li>{{>Button}}</li>
{{/array-buttons}}
</ul>
</div>
{{/is-anon}}
{{/data-minerva-notifications}}
{{#html-minerva-user-menu}}{{{html-minerva-user-menu}}}{{/html-minerva-user-menu}}
</nav>
</div>

View file

@ -46,15 +46,6 @@ use Title;
* @ingroup Skins
*/
class SkinMinerva extends SkinMustache {
private const NOTIFICATION_BUTTON_CLASSES = [
'user-button',
'mw-ui-button',
'mw-ui-quiet',
'mw-ui-icon',
'mw-ui-icon-element',
'mw-ui-icon-wikimedia-bellOutline-base20'
];
/** @const LEAD_SECTION_NUMBER integer which corresponds to the lead section
* in editing mode
*/
@ -141,16 +132,10 @@ class SkinMinerva extends SkinMustache {
*/
private function getNotificationFallbackButton() {
return [
'link-class' => [
'mw-ui-icon',
'mw-ui-icon-element',
'mw-ui-icon-wikimedia-bellOutline-base20',
'mw-ui-button',
'mw-ui-quiet'
],
'href' => SpecialPage::getTitleFor( 'Mytalk' )->getLocalURL(
[ 'returnto' => $this->getTitle()->getPrefixedText() ]
),
'icon' => 'wikimedia-bellOutline-base20',
'href' => SpecialPage::getTitleFor( 'Mytalk' )->getLocalURL(
[ 'returnto' => $this->getTitle()->getPrefixedText() ]
),
];
}
@ -160,9 +145,6 @@ class SkinMinerva extends SkinMustache {
* @return array
*/
private function getCombinedNotificationButton( array $alert, array $notice ) {
// Remove id="pt-notifications-alert", added to the wrapper in Header.mustache
$alert['id'] = '';
// Sum the notifications from the two original buttons
$notifCount = ( $alert['data']['counter-num'] ?? 0 ) + ( $notice['data']['counter-num'] ?? 0 );
$alert['data']['counter-num'] = $notifCount;
@ -207,20 +189,11 @@ class SkinMinerva extends SkinMustache {
$linkClass = $alert['link-class'] ?? [];
$hasSeenAlerts = is_array( $linkClass ) && in_array( 'mw-echo-unseen-notifications', $linkClass );
$alertText = $alert['data']['counter-text'] ?? $alertCount;
$alert['html'] =
Html::rawElement( 'div', [ 'class' => 'circle' ],
Html::element( 'span', [
'data-notification-count' => $alertCount,
], $alertText )
);
$alert['icon'] = 'circle';
$alert['class'] = 'notification-count';
if ( $hasSeenAlerts || $hasUnseenNotices ) {
$alert['class'] .= ' notification-unseen mw-echo-unseen-notifications';
}
$alert['link-class'] = array_merge(
$alert['link-class'],
self::NOTIFICATION_BUTTON_CLASSES
);
return $alert;
}
@ -232,16 +205,13 @@ class SkinMinerva extends SkinMustache {
*/
private function getNotificationButton( array $alert ) {
$linkClass = $alert['link-class'];
$linkClass = array_merge(
$linkClass,
self::NOTIFICATION_BUTTON_CLASSES
);
$alert['link-class'] = array_filter(
$linkClass,
static function ( $class ) {
return $class !== 'oo-ui-icon-bellOutline';
}
);
$alert['icon'] = 'wikimedia-bellOutline-base20';
return $alert;
}
@ -255,7 +225,6 @@ class SkinMinerva extends SkinMustache {
// There are some SkinTemplate modifications that occur after the execution of this hook
// to add rel attributes and ID attributes.
// The only one Minerva needs is this one so we manually add it.
$isSpecialPage = $skin->getTitle()->isSpecialPage();
foreach ( array_keys( $contentNavigationUrls['associated-pages'] ) as $id ) {
if ( in_array( $id, [ 'user_talk', 'talk' ] ) ) {
$contentNavigationUrls['associated-pages'][ $id ]['rel'] = 'discussion';
@ -263,6 +232,12 @@ class SkinMinerva extends SkinMustache {
}
$skinOptions = $this->getSkinOptions();
$this->contentNavigationUrls = $contentNavigationUrls;
//
// Echo Technical debt!!
// * Convert the Echo button into a single button
// * Switch out the icon.
//
if ( $this->getUser()->isRegistered() ) {
if ( count( $contentNavigationUrls['notifications'] ) === 0 ) {
// Shown to logged in users when Echo is not installed:
@ -284,10 +259,9 @@ class SkinMinerva extends SkinMustache {
// Correct the icon to be the bell filled rather than the outline to match
// Echo's badge.
$linkClass = $alert['link-class'] ?? [];
$alert['link-class'] = array_map( static function ( $class ) {
return $class === 'oo-ui-icon-bellOutline' ?
'oo-ui-icon-bell' : $class;
}, $linkClass );
$alert['link-class'] = array_filter( $linkClass, static function ( $class ) {
return $class !== 'oo-ui-icon-bellOutline';
} );
$contentNavigationUrls['notifications']['notifications-alert'] = $alert;
}
}
@ -327,6 +301,7 @@ class SkinMinerva extends SkinMustache {
}
$allLanguages = $data['data-portlets']['data-languages']['array-items'] ?? [];
$allVariants = $data['data-portlets']['data-variants']['array-items'] ?? [];
$notifications = $data['data-portlets']['data-notifications']['array-items'] ?? [];
return $data + [
'has-minerva-languages' => !empty( $allLanguages ) || !empty( $allVariants ),
@ -338,7 +313,7 @@ class SkinMinerva extends SkinMustache {
],
'label' => $this->msg( 'searchbutton' )->escaped(),
'classes' => 'skin-minerva-search-trigger',
'attributes' => [
'array-attributes' => [
[
'key' => 'id',
'value' => 'searchIcon',
@ -352,7 +327,7 @@ class SkinMinerva extends SkinMustache {
],
'tag-name' => 'label',
'classes' => 'toggle-list__toggle mw-ui-icon-flush-left',
'attributes' => [
'array-attributes' => [
[
'key' => 'for',
'value' => 'main-menu-input',
@ -379,7 +354,9 @@ class SkinMinerva extends SkinMustache {
'html-minerva-tagline' => $this->getTaglineHtml(),
'html-minerva-user-menu' => $this->getPersonalToolsMenu( $nav['user-menu'] ),
'is-minerva-beta' => $this->getSkinOptions()->get( SkinOptions::BETA_MODE ),
'is-minerva-echo-single-button' => $skinOptions->get( SkinOptions::SINGLE_ECHO_BUTTON ),
'data-minerva-notifications' => $notifications ? [
'array-buttons' => $this->getNotificationButtons( $notifications ),
] : null,
'data-minerva-tabs' => $this->getTabsData( $nav ),
'data-minerva-page-actions' => $this->getPageActions( $nav ),
'data-minerva-secondary-actions' => $this->getSecondaryActions( $nav ),
@ -388,6 +365,59 @@ class SkinMinerva extends SkinMustache {
];
}
/**
* Prepares the notification badges for the Button template.
*
* @internal
* @param array $notifications
* @return array
*/
public static function getNotificationButtons( array $notifications ) {
$btns = [];
foreach ( $notifications as $notification ) {
$linkData = $notification['array-links'][ 0 ] ?? [];
$icon = $linkData['icon'] ?? null;
if ( !$icon ) {
continue;
}
$id = $notification['id'] ?? null;
$classes = '';
$attributes = [];
// We don't want to output multiple attributes.
// Iterate through the attributes and pull out ID and class which
// will be defined separately.
foreach ( $linkData[ 'array-attributes' ] as $keyValuePair ) {
if ( $keyValuePair['key'] === 'class' ) {
$classes = $keyValuePair['value'];
} elseif ( $keyValuePair['key'] === 'id' ) {
// ignore. We want to use the LI `id` instead.
} else {
$attributes[] = $keyValuePair;
}
}
// add LI ID to end for use on the button.
if ( $id ) {
$attributes[] = [
'key' => 'id',
'value' => $id,
];
}
$btns[] = [
'tag-name' => 'a',
// FIXME: Move preg_replace when Echo no longer provides this class.
'classes' => preg_replace( '/oo-ui-icon-(bellOutline|tray)/', '', $classes ),
'array-attributes' => $attributes,
'data-icon' => [
'icon' => $icon,
],
'label' => $linkData['text'] ?? '',
];
}
return $btns;
}
/**
* Tabs are available if a page has page actions but is not the talk page of
* the main page.
@ -796,7 +826,7 @@ class SkinMinerva extends SkinMustache {
*/
protected function getLanguageButton() {
return [
'attributes' => [
'array-attributes' => [
[
'key' => 'href',
'value' => '#p-lang'
@ -816,7 +846,7 @@ class SkinMinerva extends SkinMustache {
*/
protected function getTalkButton( $talkTitle, $label ) {
return [
'attributes' => [
'array-attributes' => [
[
'key' => 'href',
'value' => $talkTitle->getLinkURL(),

View file

@ -35,6 +35,7 @@
"message": "The method `always` if used with Deferred objects is incompatible with ES6 Promises. Please use `then`."
}
],
"unit-disallowed-list": "off",
"object-property-newline": "error",
"mediawiki/class-doc": "off",
"no-use-before-define": "off",

View file

@ -1,46 +1,39 @@
@import 'mediawiki.skin.variables.less';
@import '../../minerva.less/minerva.variables.less';
// stylelint-disable selector-max-id
// The top level user account button in the upper right near the notifications button.
.minerva-user-navigation {
display: flex;
// Keep space for at least two buttons.
min-width: 2 * @menuButtonIconSize;
min-width: auto;
// Support Firefox: Needs `min-height` to vertically align icons in menu. See T233517.
min-height: @siteHeaderHeight;
height: 100%;
// FIXME: for cached HTML. Merge with `.minerva-search-form + .minerva-user-navigation`.
width: 100%;
width: auto;
// Center vertically.
align-items: center;
// Layout from right / end.
justify-content: flex-end;
// Show the list relative the button.
position: relative;
.cdx-mixin-button-layout-flush( 'end', true, 'large' );
// align the last icon (i.e. notification or use menu) with the container edge.
& > *:last-child {
margin-right: -@icon-padding-md;
}
// desktop mode modifications
.minerva-user-notifications {
.minerva-notifications {
ul {
display: flex;
align-items: center;
}
li#pt-notifications-notice,
li#pt-notifications-alert {
display: block;
/* Will be leveraged by future Echo version. */
li {
position: relative;
}
// Hide notices for Minerva mobile mode.
#pt-notifications-notice {
.mw-mf & {
display: none;
}
}
}
}
// Different rule for cached HTML
// Can be merged into `.minerva-user-navigation` rule one week after this
// change was introduced and minerva-search-form class is present everywhere.
.minerva-search-form + .minerva-user-navigation {
min-width: auto;
width: auto;
}

View file

@ -14,61 +14,59 @@
}
}
// FIXME: Belongs in Echo extension.
.notification-count {
//
// Technical debt relating to Minerva mobile having a single
// Echo icon rather than 2.
//
//
// Difference 1) Never show count for seen notifications
.mw-mf .mw-echo-notification-badge-nojs::after {
content: none;
}
//
// Difference 2) Replace bell icon with a red circle
//
.mw-mf .mw-echo-unseen-notifications {
display: inline-block;
margin: auto;
background: @background-color-light;
color: @color-subtle;
cursor: pointer;
.circle {
.mw-ui-icon-circle {
border-radius: @border-radius-circle;
border: @border-width-thick @border-style-base #54595d;
margin: auto;
height: @icon-size-md;
width: @icon-size-md;
background: @background-color-destructive;
border-color: @border-color-destructive;
/* stylelint-disable declaration-block-no-duplicate-properties */
// Center the text number inside the circle
display: block; // Fallback for old iOS
text-align: center; // Fallback for old iOS
display: -webkit-box;
display: flex;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
/* stylelint-enable declaration-block-no-duplicate-properties */
span {
font-weight: bold;
font-size: 13px;
line-height: 1;
letter-spacing: -0.5px;
&::before {
content: none;
}
}
&.notification-count {
padding: @icon-padding-md;
}
&.notification-unseen {
&::after {
color: @color-inverted;
.circle {
background: @background-color-destructive;
border-color: @border-color-destructive;
}
}
// FIXME: There must be a better way of doing this
&.max {
right: 0.2em;
width: 2em;
height: 2em;
line-height: 2em;
font-size: 0.7em;
content: attr( data-counter-text );
position: absolute;
left: 0;
bottom: 0;
right: 0;
width: 100%;
height: 100%;
top: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
letter-spacing: -0.5px;
padding: 0;
border: 0;
background-color: transparent;
}
&:hover {

View file

@ -349,6 +349,28 @@ module.exports = function () {
} );
}
/**
* Wires up the notification badge to Echo extension
*/
function setupEcho() {
const echoBtn = document.querySelector( '.minerva-notifications .mw-echo-notification-badge-nojs' );
if ( echoBtn ) {
echoBtn.addEventListener( 'click', function ( ev ) {
router.navigate( '#/notifications' );
// prevent navigation to original Special:Notifications URL
// DO NOT USE stopPropagation or you'll break click tracking in WikimediaEvents
ev.preventDefault();
// Mark as read.
echoBtn.dataset.counterNum = 0;
echoBtn.dataset.counterText = mw.msg( 'echo-badge-count',
mw.language.convertNumber( 0 )
);
} );
}
}
$( function () {
var
// eslint-disable-next-line no-jquery/no-global-selector
@ -450,5 +472,12 @@ module.exports = function () {
if ( permissions.watch && !mw.user.isNamed() ) {
ctaDrawers.initWatchstarCta( $watch );
}
// If Echo is installed, wire it up.
const echoState = mw.loader.getState( 'ext.echo.mobile' );
// If Echo is installed, set it up.
if ( echoState !== null && echoState !== 'registered' ) {
setupEcho();
}
} );
};

View file

@ -1,20 +1,47 @@
/* stylelint-disable selector-max-id */
#pt-notifications-notice,
#pt-notifications-alert {
.mw-echo-notifications-badge {
margin: 0 auto;
/* stylelint-disable unit-disallowed-list, selector-max-id */
@import 'mediawiki.skin.variables.less';
/**
* Mixin based on Vector mixin
* https://github.com/wikimedia/mediawiki-skins-Vector/blob/master/skinStyles/ext.echo.styles.badge.less
* Used in desktop version of Minerva.
*/
.mixin-notification-badge() {
position: relative;
// When 99+ allow counter so spill outside icon
&.cdx-button {
overflow: visible;
}
a.mw-ui-icon-wikimedia-bellOutline-base20 {
color: transparent;
&::after {
position: absolute;
left: 55%;
top: 43%;
font-size: 0.75rem;
padding: 0.5rem 0.25rem;
border: 1px solid #fff;
border-radius: @border-radius-base;
background-color: #72777d;
content: attr( data-counter-text );
color: #fff;
}
// The number of notifications shouldn't show if there are none.
&[ data-counter-num='0' ]::after {
content: none;
}
}
li#pt-notifications-notice,
li#pt-notifications-alert {
list-style: none;
width: 1.25em;
height: 1.25em;
padding: 0.75em;
vertical-align: middle;
.mw-echo-notification-badge-nojs {
.mixin-notification-badge();
}
// Special colors for unseen notifications
#pt-notifications-alert.mw-echo-unseen-notifications::after {
background-color: @color-destructive;
}
#pt-notifications-notice.mw-echo-unseen-notifications::after {
background-color: @color-progressive;
}

View file

@ -14,6 +14,28 @@ use Wikimedia\TestingAccessWrapper;
* @group MinervaNeue
*/
class SkinMinervaTest extends MediaWikiIntegrationTestCase {
private const ATTRIBUTE_NOTIFICATION_HREF = [
'key' => 'href',
'value' => '/wiki/Special:Notifications',
];
private const ATTRIBUTE_NOTIFICATION_DATA_EVENT_NAME = [
'key' => 'data-event-name',
'value' => 'ui.notifications',
];
private const ATTRIBUTE_NOTIFICATION_DATA_COUNTER_TEXT = [
'key' => 'data-counter-text',
'value' => "13",
];
private const ATTRIBUTE_NOTIFICATION_DATA_COUNTER_NUM = [
'key' => 'data-counter-num',
'value' => 13,
];
private const ATTRIBUTE_NOTIFICATION_TITLE = [
'key' => 'title',
'value' => "Your alerts",
];
/**
* @param array $options
*/
@ -105,4 +127,125 @@ class SkinMinervaTest extends MediaWikiIntegrationTestCase {
],
];
}
public static function provideGetNotificationButtons() {
return [
[
[],
[]
],
//
// CIRCLE
//
[
[
'tag-name' => 'a',
'classes' => 'mw-echo-notifications-badge mw-echo-notification-badge-nojs '
. ' mw-echo-unseen-notifications',
'array-attributes' => [
self::ATTRIBUTE_NOTIFICATION_HREF,
self::ATTRIBUTE_NOTIFICATION_DATA_COUNTER_TEXT,
self::ATTRIBUTE_NOTIFICATION_DATA_COUNTER_NUM,
self::ATTRIBUTE_NOTIFICATION_TITLE,
[
'key' => 'id',
'value' => 'pt-notifications-alert',
],
],
'data-icon' => [
'icon' => 'circle'
],
'label' => 'Alerts (13)',
],
[
[
'name' => 'notifications-alert',
'id' => 'pt-notifications-alert',
'class' => 'notification-count notification-unseen mw-echo-unseen-notifications mw-list-item',
'array-links' => [
[
'icon' => 'circle',
'array-attributes' => [
self::ATTRIBUTE_NOTIFICATION_HREF,
self::ATTRIBUTE_NOTIFICATION_DATA_COUNTER_TEXT,
self::ATTRIBUTE_NOTIFICATION_DATA_COUNTER_NUM,
self::ATTRIBUTE_NOTIFICATION_TITLE,
[
'key' => 'class',
'value' => 'mw-echo-notifications-badge '
. 'mw-echo-notification-badge-nojs oo-ui-icon-bellOutline '
. 'mw-echo-unseen-notifications',
],
],
'text' => 'Alerts (13)'
]
]
]
]
],
//
// BELL
//
[
[
'tag-name' => 'a',
'classes' => 'mw-echo-notifications-badge mw-echo-notification-badge-nojs',
'array-attributes' => [
self::ATTRIBUTE_NOTIFICATION_HREF,
self::ATTRIBUTE_NOTIFICATION_DATA_COUNTER_TEXT,
self::ATTRIBUTE_NOTIFICATION_DATA_COUNTER_NUM,
self::ATTRIBUTE_NOTIFICATION_TITLE,
[
'key' => 'id',
'value' => 'pt-notifications-alert',
],
],
'data-icon' => [
'icon' => 'wikimedia-bellOutline-base20'
],
'label' => 'Alerts (13)',
],
[
[
'html-item' => 'n/a',
'name' => 'notifications-alert',
'html' => 'HTML',
'id' => 'pt-notifications-alert',
'class' => 'mw-list-item',
'array-links' => [
[
'icon' => 'wikimedia-bellOutline-base20',
'array-attributes' => [
self::ATTRIBUTE_NOTIFICATION_HREF,
self::ATTRIBUTE_NOTIFICATION_DATA_COUNTER_TEXT,
self::ATTRIBUTE_NOTIFICATION_DATA_COUNTER_NUM,
self::ATTRIBUTE_NOTIFICATION_TITLE,
[
'key' => 'class',
'value' => 'mw-echo-notifications-badge mw-echo-notification-badge-nojs',
],
],
'text' => 'Alerts (13)'
]
]
]
]
]
];
}
/**
* @dataProvider provideGetNotificationButtons
* @param array $expected
* @param array $from
* @covers ::getNotificationButtons
*/
public function testGetNotificationButtons( $expected, $from ) {
$btns = SkinMinerva::getNotificationButtons( $from );
$this->assertEquals( $expected['classes'] ?? '', $btns[0]['classes'] ?? '' );
$this->assertEquals( $expected['data-attributes'] ?? [], $btns[0]['data-attributes'] ?? [] );
$this->assertEquals( $expected['data-icon'] ?? [], $btns[0]['data-icon'] ?? [] );
$this->assertEquals( $expected['data-label'] ?? '', $btns[0]['data-label'] ?? '' );
}
}