diff --git a/.storybook/main.js b/.storybook/main.js index 2d25cacb1..a65c3f634 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -4,10 +4,6 @@ const webpack = require( "webpack" ); module.exports = { stories: ['./stories/*.stories.js' ], webpackFinal: async (config, { configType }) => { - config.module.rules.push({ - test: /\.css$/, - use: ['style-loader', 'css-loader'] - }); config.module.rules.push({ test: /\.less$/, use: ['style-loader', 'css-loader', { diff --git a/.storybook/stories/index.stories.js b/.storybook/stories/index.stories.js index ce36e6d36..0760ac637 100644 --- a/.storybook/stories/index.stories.js +++ b/.storybook/stories/index.stories.js @@ -21,6 +21,7 @@ import { storiesOf } from '@storybook/html'; * Popups dependencies */ import { createPointerMasks } from '../../src/ui/renderer.js'; +import '../../node_modules/@wikimedia/codex/dist/codex.style.css'; /** * Popups helpers diff --git a/extension.json b/extension.json index 10d765535..bc14cc35c 100644 --- a/extension.json +++ b/extension.json @@ -25,7 +25,8 @@ "UserGetDefaultOptions": "PopupsHooks", "MakeGlobalVariablesScript": "PopupsHooks", "LocalUserCreated": "PopupsHooks", - "GetBetaFeaturePreferences": "PopupsHooks" + "GetBetaFeaturePreferences": "PopupsHooks", + "ResourceLoaderRegisterModules": "PopupsHooks" }, "HookHandlers": { "PopupsHooks": { @@ -229,12 +230,12 @@ "mediawiki.jqueryMsg", "mediawiki.storage", "mediawiki.Title", - "mediawiki.ui.button", "mediawiki.ui.checkbox", "mediawiki.ui.icon", "mediawiki.Uri", "mediawiki.user", - "mediawiki.util" + "mediawiki.util", + "codex-search-styles" ] } }, diff --git a/includes/PopupsHooks.php b/includes/PopupsHooks.php index 49fed0ae9..2a3f47007 100644 --- a/includes/PopupsHooks.php +++ b/includes/PopupsHooks.php @@ -28,6 +28,8 @@ use MediaWiki\Hook\MakeGlobalVariablesScriptHook; use MediaWiki\MediaWikiServices; use MediaWiki\Preferences\Hook\GetPreferencesHook; use MediaWiki\ResourceLoader\Hook\ResourceLoaderGetConfigVarsHook; +use MediaWiki\ResourceLoader\Hook\ResourceLoaderRegisterModulesHook; +use MediaWiki\ResourceLoader\ResourceLoader; use MediaWiki\User\Hook\UserGetDefaultOptionsHook; use MediaWiki\User\UserOptionsManager; use OutputPage; @@ -43,6 +45,7 @@ class PopupsHooks implements GetPreferencesHook, BeforePageDisplayHook, ResourceLoaderGetConfigVarsHook, + ResourceLoaderRegisterModulesHook, MakeGlobalVariablesScriptHook, UserGetDefaultOptionsHook, LocalUserCreatedHook @@ -320,4 +323,25 @@ class PopupsHooks implements } } + /** + * ResourceLoaderRegisterModules hook handler. + * + * Provides support for MLEB where needed. + * + * @see https://www.mediawiki.org/wiki/Manual:Hooks/ResourceLoaderRegisterModules + * + * @param ResourceLoader $resourceLoader + */ + public function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ): void { + if ( !$resourceLoader->getModule( 'codex-search-styles' ) ) { + // We're running an older version of MediaWiki. + $resourceLoader->register( [ + 'codex-search-styles' => [ + 'localBasePath' => dirname( __DIR__ ), + 'remoteExtPath' => 'UniversalLanguageSelector', + 'styles' => 'resources/codex.mleb.css', + ] + ] ); + } + } } diff --git a/package-lock.json b/package-lock.json index 8473d0d90..fbd33d4af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,8 +18,8 @@ "@wdio/local-runner": "7.4.6", "@wdio/mocha-framework": "7.30.2", "@wdio/sync": "7.4.6", - "@wikimedia/codex": "0.11.0", - "@wikimedia/codex-icons": "0.11.0", + "@wikimedia/codex": "0.14.0", + "@wikimedia/codex-icons": "0.14.0", "@wikimedia/mw-node-qunit": "7.0.0", "babel-loader": "8.0.4", "browserslist-config-wikimedia": "0.2.0", @@ -6550,9 +6550,9 @@ } }, "node_modules/@wikimedia/codex": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@wikimedia/codex/-/codex-0.11.0.tgz", - "integrity": "sha512-DYI9ewtEqNykzYp1nMcyrtbKpGbVPTWkbxV9QJIDIDe2GayA8XbfK/7u0V5cnTU90iqCLwqsEnQZiXX4QDvsWg==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@wikimedia/codex/-/codex-0.14.0.tgz", + "integrity": "sha512-qx3ADc2I5yDg448WVZ6TodGdV2vCs4SLgH6KdDx0obAr+1vsmBUyBhaVenJOL2WrMDq1rAxV1gx7YrbgOoU/MQ==", "dev": true, "engines": { "node": ">=16", @@ -6563,9 +6563,9 @@ } }, "node_modules/@wikimedia/codex-icons": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@wikimedia/codex-icons/-/codex-icons-0.11.0.tgz", - "integrity": "sha512-aXpGI5UsWiWk1uag5I2+YlJinflmUVEyFoDH8IOt+Nko7t/z6EbJCHoMV482pVl9Ygbk2/ukHQ3LOFyS0Dv8xQ==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@wikimedia/codex-icons/-/codex-icons-0.14.0.tgz", + "integrity": "sha512-xbHV+5OMhIXrtfweeDAXO/HN69fn+bSUZtns6hbMgWsAK8t3hYyhQRzUeFJxrS0tApG4X9NQxIQ6mOmjZwz/rA==", "dev": true, "engines": { "node": ">=16", @@ -32567,16 +32567,16 @@ } }, "@wikimedia/codex": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@wikimedia/codex/-/codex-0.11.0.tgz", - "integrity": "sha512-DYI9ewtEqNykzYp1nMcyrtbKpGbVPTWkbxV9QJIDIDe2GayA8XbfK/7u0V5cnTU90iqCLwqsEnQZiXX4QDvsWg==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@wikimedia/codex/-/codex-0.14.0.tgz", + "integrity": "sha512-qx3ADc2I5yDg448WVZ6TodGdV2vCs4SLgH6KdDx0obAr+1vsmBUyBhaVenJOL2WrMDq1rAxV1gx7YrbgOoU/MQ==", "dev": true, "requires": {} }, "@wikimedia/codex-icons": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@wikimedia/codex-icons/-/codex-icons-0.11.0.tgz", - "integrity": "sha512-aXpGI5UsWiWk1uag5I2+YlJinflmUVEyFoDH8IOt+Nko7t/z6EbJCHoMV482pVl9Ygbk2/ukHQ3LOFyS0Dv8xQ==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@wikimedia/codex-icons/-/codex-icons-0.14.0.tgz", + "integrity": "sha512-xbHV+5OMhIXrtfweeDAXO/HN69fn+bSUZtns6hbMgWsAK8t3hYyhQRzUeFJxrS0tApG4X9NQxIQ6mOmjZwz/rA==", "dev": true }, "@wikimedia/mw-node-qunit": { diff --git a/package.json b/package.json index 6dd3dbde9..f77cca6b8 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,8 @@ "@wdio/local-runner": "7.4.6", "@wdio/mocha-framework": "7.30.2", "@wdio/sync": "7.4.6", - "@wikimedia/codex": "0.11.0", - "@wikimedia/codex-icons": "0.11.0", + "@wikimedia/codex": "0.14.0", + "@wikimedia/codex-icons": "0.14.0", "@wikimedia/mw-node-qunit": "7.0.0", "babel-loader": "8.0.4", "browserslist-config-wikimedia": "0.2.0", diff --git a/resources/dist/index.js b/resources/dist/index.js index 24e2c9442..36cd466f0 100644 Binary files a/resources/dist/index.js and b/resources/dist/index.js differ diff --git a/resources/dist/index.js.map.json b/resources/dist/index.js.map.json index 3403433b9..433d6edc5 100644 Binary files a/resources/dist/index.js.map.json and b/resources/dist/index.js.map.json differ diff --git a/src/ui/index.less b/src/ui/index.less index 0033473ee..45981f625 100644 --- a/src/ui/index.less +++ b/src/ui/index.less @@ -9,3 +9,15 @@ position: absolute; top: -1000px; } + +.cdx-button.cdx-button--icon-only { + // Hide text in icon only buttons + span + span { + .mixin-screen-reader-text(); + } +} + +// FIXME: Remove after Iac69f7b14b732b0bbaa6ba265379ed4c4cdb3f7e is merged +.cdx-button--fake-button { + justify-content: center; +} diff --git a/src/ui/renderer.js b/src/ui/renderer.js index 4b5646a15..1631f7439 100644 --- a/src/ui/renderer.js +++ b/src/ui/renderer.js @@ -306,10 +306,10 @@ export function bindBehavior( preview, behavior ) { preview.el.addEventListener( 'click', behavior.click ); - const icon = preview.el.querySelector( '.mwe-popups-settings-icon' ); - if ( icon ) { - icon.href = behavior.settingsUrl; - icon.addEventListener( 'click', ( event ) => { + const button = preview.el.querySelector( '.mwe-popups-settings-button' ); + if ( button ) { + button.href = behavior.settingsUrl; + button.addEventListener( 'click', ( event ) => { event.stopPropagation(); behavior.showSettings( event ); diff --git a/src/ui/templates/pagePreview/pagePreview.js b/src/ui/templates/pagePreview/pagePreview.js index a142f90a4..5527ce564 100644 --- a/src/ui/templates/pagePreview/pagePreview.js +++ b/src/ui/templates/pagePreview/pagePreview.js @@ -3,7 +3,7 @@ */ import { renderPopup } from '../popup/popup'; -import { createNodeFromTemplate } from '../templateUtil'; +import { escapeHTML, createNodeFromTemplate } from '../templateUtil'; const defaultExtractWidth = 215; const templateHTML = ` @@ -11,8 +11,9 @@ const templateHTML = ` @@ -37,9 +38,14 @@ export function renderPagePreview( extract.setAttribute( 'dir', model.languageDirection ); extract.setAttribute( 'lang', model.languageCode ); - el.querySelector( '.mwe-popups-settings-icon' ) + el.querySelector( '.mwe-popups-settings-button' ) .setAttribute( 'title', linkTitle ); + // Set label on settings icon button + const labelText = escapeHTML( mw.msg( 'popups-settings-icon-gear-title' ) ); + const label = el.querySelector( '.mwe-popups-settings-button-label' ); + label.textContent = labelText; + if ( thumbnail ) { el.querySelector( '.mwe-popups-discreet' ).appendChild( thumbnail.el ); } else { diff --git a/src/ui/templates/popup/popup.less b/src/ui/templates/popup/popup.less index 5e4ce9f07..f1daaca34 100644 --- a/src/ui/templates/popup/popup.less +++ b/src/ui/templates/popup/popup.less @@ -62,25 +62,12 @@ } } - .mwe-popups-settings-icon { - display: block; + .mwe-popups-settings-button { float: right; // positions icon near bottom right corner - border-radius: @border-radius-base; - opacity: 0.67; - transition: background-color 100ms, opacity 100ms; - - &:hover { - // TODO: Replace by `@background-color-button-quiet--hover` - // as soon as available in mediawiki.skin.variables. - background-color: rgba( 0, 24, 73, 0.027 ); - } - - &:active { - // TODO: Replace by `@background-color-button-quiet--active` - // as soon as available in mediawiki.skin.variables. - background-color: rgba( 0, 24, 73, 0.082 ); - opacity: 1; - } + pointer-events: auto; // Overrides pointer-events: none on footer to ensure the button is interactive + // !important needed to override 'responsive' button styles on smaller viewports defined in Vector's Button.less + min-width: 32px !important; /* stylelint-disable-line declaration-no-important */ + min-height: 32px !important; /* stylelint-disable-line declaration-no-important */ } .mwe-popups-extract { @@ -409,7 +396,7 @@ /* @noflip */ right: 0; - .mwe-popups-settings-icon { + .mwe-popups-settings-button { /* @noflip */ float: left; } diff --git a/src/ui/templates/referencePreview/referencePreview.js b/src/ui/templates/referencePreview/referencePreview.js index 4f3fa00b0..05fda7df1 100644 --- a/src/ui/templates/referencePreview/referencePreview.js +++ b/src/ui/templates/referencePreview/referencePreview.js @@ -106,12 +106,15 @@ export function renderReferencePreview( // TODO: Remove when not in Beta any more if ( !mw.config.get( 'wgPopupsReferencePreviewsBetaFeature' ) ) { // TODO: Do not remove this but move it up into the templateHTML constant! - const settingsIconLink = document.createElement( 'a' ); - settingsIconLink.classList.add( 'mwe-popups-settings-icon' ); - const settingsIconLabel = document.createElement( 'span' ); - settingsIconLabel.classList.add( 'mw-ui-icon', 'mw-ui-icon-element', 'mw-ui-icon-small', 'mw-ui-icon-settings' ); - settingsIconLink.append( settingsIconLabel ); - el.querySelector( '.mwe-popups-settings' ).appendChild( settingsIconLink ); + const settingsButton = document.createElement( 'button' ); + settingsButton.classList.add( 'cdx-button', 'cdx-button--fake-button', 'cdx-button--fake-button--enabled', 'cdx-button--weight-quiet', 'cdx-button--icon-only', 'mwe-popups-settings-button' ); + const settingsIcon = document.createElement( 'span' ); + settingsIcon.classList.add( 'mw-ui-icon', 'mw-ui-icon-small', 'mw-ui-icon-settings' ); + const settingsButtonLabel = document.createElement( 'span' ); + settingsButtonLabel.textContent = mw.msg( 'popups-settings-icon-gear-title' ); + settingsButton.append( settingsIcon ); + settingsButton.append( settingsButtonLabel ); + el.querySelector( '.mwe-popups-settings' ).appendChild( settingsButton ); } else { // Change the styling when there is no content in the footer (to prevent empty space) el.querySelector( '.mwe-popups-container' ).classList.add( 'footer-empty' ); diff --git a/src/ui/templates/settingsDialog/settingsDialog.js b/src/ui/templates/settingsDialog/settingsDialog.js index 527af7c06..3da9d2cbd 100644 --- a/src/ui/templates/settingsDialog/settingsDialog.js +++ b/src/ui/templates/settingsDialog/settingsDialog.js @@ -58,12 +58,15 @@ export function renderSettingsDialog( model ) {
-
${closeLabel}
+

${heading}

- - + +
@@ -83,7 +86,7 @@ export function renderSettingsDialog( model ) {
diff --git a/src/ui/templates/settingsDialog/settingsDialog.less b/src/ui/templates/settingsDialog/settingsDialog.less index 601d73541..4496a42f8 100644 --- a/src/ui/templates/settingsDialog/settingsDialog.less +++ b/src/ui/templates/settingsDialog/settingsDialog.less @@ -17,7 +17,7 @@ position: relative; display: table; width: 100%; - padding: 5px 7px 5px 0; + padding: 5px 7px; > div { display: table-cell; diff --git a/tests/node-qunit/ui/renderer.test.js b/tests/node-qunit/ui/renderer.test.js index 7a1384e63..e49e9ee15 100644 --- a/tests/node-qunit/ui/renderer.test.js +++ b/tests/node-qunit/ui/renderer.test.js @@ -18,7 +18,7 @@ function createPagePreview( isTall, hasThumbnail, thumbnail ) { el: $( '
' ).append( hasThumbnail ? $( '' ) : '', $( '' ).addClass( 'mwe-popups-extract' ).text( 'extract' ), - $( '' ).addClass( 'mwe-popups-settings-icon' ) + $( '' ).addClass( 'mwe-popups-settings-button' ) )[ 0 ], isTall, hasThumbnail, @@ -166,7 +166,7 @@ QUnit.test( 'createPagePreview', ( assert ) => { 'Language direction is safely espaced' ); assert.strictEqual( - $( preview.el ).find( '.mwe-popups-settings-icon' ).attr( 'title' ), + $( preview.el ).find( '.mwe-popups-settings-button' ).attr( 'title' ), '', 'Title attribute is correct.' ); @@ -458,7 +458,7 @@ QUnit.test( 'bindBehavior - settings link click', function ( assert ) { behavior = createBehavior( this.sandbox ); renderer.bindBehavior( preview, behavior ); - preview.el.querySelector( '.mwe-popups-settings-icon' ).dispatchEvent( new Event( 'click' ) ); + preview.el.querySelector( '.mwe-popups-settings-button' ).dispatchEvent( new Event( 'click' ) ); assert.false( behavior.previewDwell.called, 'Preview dwell is NOT called.' ); assert.false( @@ -475,7 +475,7 @@ QUnit.test( 'bindBehavior - settings link URL', function ( assert ) { renderer.bindBehavior( preview, behavior ); assert.strictEqual( - $( preview.el ).find( '.mwe-popups-settings-icon' ).attr( 'href' ), + $( preview.el ).find( '.mwe-popups-settings-button' ).attr( 'href' ), behavior.settingsUrl, 'Settings link URL is correct.' ); diff --git a/webpack.config.js b/webpack.config.js index 1f39a39de..65651fc5a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -63,6 +63,12 @@ module.exports = ( env, argv ) => ( { options: { removeSVGTagAttrs: false // Keep width and height attributes. } + }, { + test: /\.css$/, + use: [ + 'style-loader', + 'css-loader' + ] } ] }, optimization: { @@ -112,8 +118,8 @@ module.exports = ( env, argv ) => ( { // Minified uncompressed size limits for chunks / assets and entrypoints. Keep these numbers // up-to-date and rounded to the nearest 10th of a kibibyte so that code sizing costs are // well understood. Related to bundlesize minified, gzipped compressed file size tests. - maxAssetSize: 45.8 * 1024, - maxEntrypointSize: 45.8 * 1024, + maxAssetSize: 46.4 * 1024, + maxEntrypointSize: 46.4 * 1024, // The default filter excludes map files but we rename ours. assetFilter: ( filename ) => !filename.endsWith( srcMapExt )