Disable night mode if gadget detected

While our implementation of night mode is in beta, we want to respect
the existing night mode gadget and disable night mode in favor of the
gadget, providing a notice with an option to disable the gadget and
reload the page

Additionally, raise the max bundle size to account for the additional
code added

Note: the tests still aren't exactly where I'd like them to be, but
hopefully they raise confidence a little bit with reviewing this patch

Additional changes:
* Upgrade to latest version of TypeScript types and remove several
@ts-ignore statements

Bug: T365083
Change-Id: I9583ee7ebf8c810ddd504193d568034c954d28f2
This commit is contained in:
Steph Toyofuku 2024-05-28 16:30:23 -07:00 committed by Jdlrobson
parent 5340f59b61
commit 4a0c2cb684
11 changed files with 259 additions and 17 deletions

View file

@ -28,7 +28,7 @@
},
{
"resourceModule": "skins.vector.js",
"maxSize": "16.4 kB"
"maxSize": "17.0 kB"
},
{
"resourceModule": "skins.vector.legacy.js",

View file

@ -88,5 +88,7 @@
"vector-main-menu-unpinned-popup": "The main menu has moved here.",
"vector-appearance-unpinned-popup": "The appearance menu has moved here.",
"vector-2022-beta-preview-label": "Accessibility for Reading (Vector 2022)",
"vector-2022-beta-preview-description": "Get early access to the new reading accessibility features, such as typography improvements and dark mode."
"vector-2022-beta-preview-description": "Get early access to the new reading accessibility features, such as typography improvements and dark mode.",
"vector-night-mode-gadget-names": "dark-mode|dark-mode-toggle|dark-mode-toggle-pagestyles",
"vector-night-mode-gadget-warning": "You're using a dark mode gadget that interferes with this feature. [[Special:Preferences#mw-prefsection-gadgets|Disable the gadget]] to use dark mode."
}

View file

@ -105,5 +105,7 @@
"vector-main-menu-unpinned-popup": "Text to show in popup body when Main menu ({{msg-mw|vector-main-menu-label}}) has been moved.",
"vector-appearance-unpinned-popup": "Text to show in popup body when Appearance menu ({{msg-mw|vector-appearance-label}}) has been moved.",
"vector-2022-beta-preview-label": "label for beta feature preview link in beta features under special preferences",
"vector-2022-beta-preview-description": "label for beta feature preview description in beta features under special preferences"
"vector-2022-beta-preview-description": "label for beta feature preview description in beta features under special preferences",
"vector-night-mode-gadget-names": "pipe-separated list of gadget names associated with the night mode gadget",
"vector-night-mode-gadget-warning": "a notice that night mode has been disabled due to a conflicting gadget, with a link to the gadgets page to disable"
}

14
package-lock.json generated
View file

@ -15,7 +15,7 @@
"@wikimedia/codex": "1.6.0",
"@wikimedia/codex-icons": "1.6.0",
"@wikimedia/mw-node-qunit": "7.2.0",
"@wikimedia/types-wikimedia": "0.4.3",
"@wikimedia/types-wikimedia": "0.4.4",
"eslint-config-wikimedia": "0.27.0",
"eslint-plugin-no-jquery": "2.7.0",
"grunt-banana-checker": "0.13.0",
@ -2792,9 +2792,9 @@
"dev": true
},
"node_modules/@wikimedia/types-wikimedia": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@wikimedia/types-wikimedia/-/types-wikimedia-0.4.3.tgz",
"integrity": "sha512-c9qY4NUNLsc5OHpFIPd2EMMtqqI5g5PYMSg/ivaDxbn4gJf+1xbFzEC1kQCraCoIWFu9kvXdsMx+ZfhRsSkUaA==",
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@wikimedia/types-wikimedia/-/types-wikimedia-0.4.4.tgz",
"integrity": "sha512-OQ5WZ02E1XKNNljhFMBfgYdsBmJGp7vCmZZR4ZofPOd4sobvfWCWpW87ezPSePwJkB5YScWLQWNSaAeVoiXW7A==",
"dev": true
},
"node_modules/abab": {
@ -15242,9 +15242,9 @@
}
},
"@wikimedia/types-wikimedia": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@wikimedia/types-wikimedia/-/types-wikimedia-0.4.3.tgz",
"integrity": "sha512-c9qY4NUNLsc5OHpFIPd2EMMtqqI5g5PYMSg/ivaDxbn4gJf+1xbFzEC1kQCraCoIWFu9kvXdsMx+ZfhRsSkUaA==",
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@wikimedia/types-wikimedia/-/types-wikimedia-0.4.4.tgz",
"integrity": "sha512-OQ5WZ02E1XKNNljhFMBfgYdsBmJGp7vCmZZR4ZofPOd4sobvfWCWpW87ezPSePwJkB5YScWLQWNSaAeVoiXW7A==",
"dev": true
},
"abab": {

View file

@ -26,7 +26,7 @@
"@wikimedia/codex": "1.6.0",
"@wikimedia/codex-icons": "1.6.0",
"@wikimedia/mw-node-qunit": "7.2.0",
"@wikimedia/types-wikimedia": "0.4.3",
"@wikimedia/types-wikimedia": "0.4.4",
"eslint-config-wikimedia": "0.27.0",
"eslint-plugin-no-jquery": "2.7.0",
"grunt-banana-checker": "0.13.0",

View file

@ -0,0 +1,95 @@
/**
* T365083 - Disable night mode if night mode gadget is enabled
*
* While our implementation of night mode is still in beta, we want to respect the existing gadget
* and disable our version to avoid a double invert - that said, we will still provide a prompt for
* the user to disable the gadget so they can try our night mode
*/
/**
* Are any of the gadgets associated with the broader night mode gadget enabled?
* Note: This is localized to the names of the gadget in our particular language
*
* @return {boolean}
*/
function isNightModeGadgetEnabled() {
return mw.msg( 'vector-night-mode-gadget-names' ).split( '|' ).some( ( gadget ) => {
const state = mw.loader.getState( `ext.gadget.${ gadget }` );
// the state is null if it's not installed or we're on the preference page, otherwise it's
// registered if the user doesn't have it turned on - all other states we consider enabled
return state !== null && state !== 'registered';
} );
}
/**
* Manually mark the page we're on as excluded
*/
function disableNightModeForGadget() {
document.documentElement.classList.remove( 'skin-theme-clientpref-night' );
document.documentElement.classList.remove( 'skin-theme-clientpref-os' );
document.documentElement.classList.add( 'skin-theme-clientpref--excluded' );
}
/**
* Modify the link to disable the gadget so that, when clicked, it will disable the night mode
* gadget rather than simply take you to the page
* Note: The gadget names are similarly localized to the current language
*
* @param {Element} container an html element containing a link
*/
function alterDisableLink( container ) {
const link = container.querySelector( 'a' );
// if we can't find a link, nothing we can do
if ( !link ) {
return;
}
link.removeAttribute( 'title' );
link.removeAttribute( 'href' );
link.style.display = 'inline';
link.addEventListener( 'click', () => {
const disableOptions = Object();
mw.msg( 'vector-night-mode-gadget-names' ).split( '|' ).forEach( ( gadgetName ) => {
disableOptions[ `gadget-${ gadgetName }` ] = 0;
} );
const api = new mw.Api();
api.saveOptions( disableOptions ).then( () => {
window.location.reload();
} );
} );
}
/**
* Modify the default exclusion message to indicate that we've disabled night mode on the page due
* to a conflicting gadget, providing a link to disable the gadget in favor of our night mode
*/
function alterExclusionMessage() {
const noticeContainer = document.querySelector( '.exclusion-notice' );
// if there's no exclusion notice, nothing we can do
if ( !noticeContainer ) {
return;
}
mw.loader.using( 'mediawiki.jqueryMsg' ).then( () => {
// remove existing message
noticeContainer.textContent = '';
mw.message( 'vector-night-mode-gadget-warning' ).parseDom().appendTo( noticeContainer );
alterDisableLink( noticeContainer );
} );
}
module.exports = {
isNightModeGadgetEnabled,
disableNightModeForGadget,
alterDisableLink,
alterExclusionMessage
};

View file

@ -16,7 +16,6 @@ function save( feature, enabled ) {
case 'limited-width':
case 'appearance-pinned':
// Save the setting under the new system
// @ts-ignore https://github.com/wikimedia/typescript-types/pull/44
mw.user.clientPrefs.set( `vector-feature-${ feature }`, enabled ? '1' : '0' );
break;
default:

View file

@ -68,7 +68,6 @@ function hide( popupWidget ) {
*/
function show( popupWidget, timeout = 4000 ) {
popupWidget.toggle( true );
// @ts-ignore https://github.com/wikimedia/typescript-types/pull/40
popupWidget.toggleClipping( true );
// hide the popup after timeout ms
if ( timeout === false ) {

View file

@ -13,6 +13,7 @@ const languageButton = require( './languageButton.js' ),
setupIntersectionObservers = require( './setupIntersectionObservers.js' ),
menuTabs = require( './menuTabs.js' ),
legacyMessageBoxStyles = require( './legacyMessageBoxStyles.js' ),
{ isNightModeGadgetEnabled, disableNightModeForGadget, alterExclusionMessage } = require( './disableNightModeIfGadget.js' ),
teleportTarget = /** @type {HTMLElement} */require( /** @type {string} */ ( 'mediawiki.page.ready' ) ).teleportTarget;
/**
@ -87,7 +88,16 @@ function main( window ) {
// @ts-ignore issues relating to delete operator are not relevant here.
delete clientPreferenceConfig[ 'skin-theme' ];
}
clientPreferences.render( appearanceMenuSelector, clientPreferenceConfig );
// while we're in beta, temporarily check if the night mode gadget is installed and
// disable our night mode if so
if ( isNightModeGadgetEnabled() ) {
disableNightModeForGadget();
clientPreferences.render( appearanceMenuSelector, clientPreferenceConfig );
alterExclusionMessage();
} else {
clientPreferences.render( appearanceMenuSelector, clientPreferenceConfig );
}
} );
}

View file

@ -459,7 +459,8 @@
"resources/skins.vector.js/languageButton.js",
"resources/skins.vector.js/echo.js",
"resources/skins.vector.js/searchLoader.js",
"resources/skins.vector.js/menuTabs.js"
"resources/skins.vector.js/menuTabs.js",
"resources/skins.vector.js/disableNightModeIfGadget.js"
],
"dependencies": [
"skins.vector.clientPreferences",
@ -469,7 +470,8 @@
"mediawiki.cookie",
"mediawiki.experiments",
"skins.vector.icons.js",
"mediawiki.util"
"mediawiki.util",
"mediawiki.jqueryMsg"
],
"messages": [
"vector-limited-width-toggle-on-popup",
@ -483,7 +485,9 @@
"vector-toc-unpinned-popup",
"vector-page-tools-unpinned-popup",
"vector-main-menu-unpinned-popup",
"vector-appearance-unpinned-popup"
"vector-appearance-unpinned-popup",
"vector-night-mode-gadget-names",
"vector-night-mode-gadget-warning"
]
},
"skins.vector.legacy.js": {

View file

@ -0,0 +1,131 @@
const {
isNightModeGadgetEnabled,
disableNightModeForGadget,
alterDisableLink,
alterExclusionMessage
} = require( '../../../resources/skins.vector.js/disableNightModeIfGadget.js' );
describe( 'isNightModeGadgetEnabled', () => {
beforeEach( () => {
// https://github.com/wikimedia/mw-node-qunit/pull/38
mw.loader.getState = () => null;
} );
it( 'should return false if no gadgets are installed', () => {
expect( isNightModeGadgetEnabled() ).toBeFalsy();
} );
it( 'should return false if the gadgets are installed but not enabled', () => {
// https://github.com/wikimedia/mw-node-qunit/pull/38
mw.loader.getState = () => 'registered';
expect( isNightModeGadgetEnabled() ).toBeFalsy();
} );
it( 'should return true if the gadgets are enabled', () => {
// https://github.com/wikimedia/mw-node-qunit/pull/38
mw.loader.getState = () => 'ready';
expect( isNightModeGadgetEnabled() ).toBeTruthy();
} );
} );
describe( 'disableNightModeForGadget', () => {
beforeEach( () => {
document.documentElement.classList.remove( 'skin-theme-clientpref--excluded' );
document.documentElement.classList.remove( 'skin-theme-clientpref-night' );
document.documentElement.classList.remove( 'skin-theme-clientpref-os' );
} );
it( 'should disable night mode', () => {
document.documentElement.classList.add( 'skin-theme-clientpref-night' );
disableNightModeForGadget();
expect( document.documentElement.classList.contains( 'skin-theme-clientpref-night' ) ).toBeFalsy();
} );
it( 'should disable automatic mode', () => {
document.documentElement.classList.add( 'skin-theme-clientpref-os' );
disableNightModeForGadget();
expect( document.documentElement.classList.contains( 'skin-theme-clientpref-os' ) ).toBeFalsy();
} );
it( 'should add the excluded class', () => {
disableNightModeForGadget();
expect( document.documentElement.classList.contains( 'skin-theme-clientpref--excluded' ) ).toBeTruthy();
} );
} );
describe( 'alterDisableLink', () => {
it( 'should leave the surrounding element unaltered', () => {
const p = document.createElement( 'p' );
const a = document.createElement( 'a' );
p.appendChild( a );
p.textContent = 'test';
alterDisableLink( p );
expect( p.textContent ).toBe( 'test' );
} );
it( 'should strip the title and href attributes', () => {
const p = document.createElement( 'p' );
const a = document.createElement( 'a' );
p.appendChild( a );
a.href = 'test.com';
a.title = 'test';
alterDisableLink( p );
expect( a.href ).toBe( '' );
expect( a.title ).toBe( '' );
} );
it( 'should make the link display inline', () => {
const p = document.createElement( 'p' );
const a = document.createElement( 'a' );
p.appendChild( a );
alterDisableLink( p );
expect( a.style.display ).toBe( 'inline' );
} );
// actual click test to be added after https://github.com/wikimedia/mw-node-qunit/pull/39
} );
describe( 'alterExclusionMessage', () => {
beforeEach( () => {
jest.spyOn( mw.loader, 'using' ).mockImplementation( () => ( {
then: ( fn ) => fn()
} ) );
// https://github.com/wikimedia/mw-node-qunit/pull/40
jest.spyOn( mw, 'message' ).mockImplementation( () => ( {
parseDom: () => ( {
appendTo: () => {}
} )
} ) );
} );
afterEach( () => {
jest.restoreAllMocks();
} );
it( 'should remove the existing text from the notice', () => {
const p = document.createElement( 'p' );
document.documentElement.appendChild( p );
p.className = 'exclusion-notice';
p.textContent = 'test';
alterExclusionMessage();
expect( p.textContent ).toBe( '' );
} );
} );