Client preferences supports toggle switches

Bug: T350418
Change-Id: I359924874e7232eaee73b7dc3678b9e8e26794ac
这个提交包含在:
Jon Robson 2024-01-17 16:59:41 -08:00
父节点 7d5caf3f66
当前提交 8c49b1eb49
共有 4 个文件被更改,包括 181 次插入35 次删除

查看文件

@ -32,10 +32,10 @@ module.exports = {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 31,
functions: 39,
lines: 38,
statements: 38
branches: 35,
functions: 45,
lines: 48,
statements: 48
}
},

查看文件

@ -2,6 +2,7 @@
* @typedef {Object} ClientPreference
* @property {string[]} options that are valid for this client preference
* @property {string} preferenceKey for registered users.
* @property {string} [type] defaults to radio. Supported: radio, switch
*/
let /** @type {MwApi} */ api;
/**
@ -62,6 +63,48 @@ function toggleDocClassAndSave( featureName, value, config ) {
}
}
/**
* @param {string} featureName
* @param {string} value
* @return {string}
*/
const getInputId = ( featureName, value ) => `skin-client-pref-${ featureName }-value-${ value }`;
/**
* @param {string} type
* @param {string} featureName
* @param {string} value
* @return {HTMLInputElement}
*/
function makeInputElement( type, featureName, value ) {
const input = document.createElement( 'input' );
const name = `skin-client-pref-${ featureName }-group`;
const id = getInputId( featureName, value );
input.name = name;
input.id = id;
input.type = type;
if ( type === 'checkbox' ) {
input.checked = value === '1';
} else {
input.value = value;
}
input.setAttribute( 'data-event-name', id );
return input;
}
/**
* @param {string} featureName
* @param {string} value
* @return {HTMLLabelElement}
*/
function makeLabelElement( featureName, value ) {
const label = document.createElement( 'label' );
// eslint-disable-next-line mediawiki/msg-doc
label.textContent = mw.msg( `${ featureName }-${ value }-label` );
label.setAttribute( 'for', getInputId( featureName, value ) );
return label;
}
/**
* @param {Element} parent
* @param {string} featureName
@ -70,27 +113,17 @@ function toggleDocClassAndSave( featureName, value, config ) {
* @param {Record<string,ClientPreference>} config
*/
function appendRadioToggle( parent, featureName, value, currentValue, config ) {
const input = document.createElement( 'input' );
const name = `skin-client-pref-${ featureName }-group`;
const id = `skin-client-pref-${ featureName }-value-${ value }`;
input.name = name;
input.id = id;
input.type = 'radio';
input.value = value;
const input = makeInputElement( 'radio', featureName, value );
input.classList.add( 'cdx-radio__input' );
if ( currentValue === value ) {
input.checked = true;
}
const icon = document.createElement( 'span' );
icon.classList.add( 'cdx-radio__icon' );
const label = document.createElement( 'label' );
const label = makeLabelElement( featureName, value );
label.classList.add( 'cdx-radio__label' );
// eslint-disable-next-line mediawiki/msg-doc
label.textContent = mw.msg( `${ featureName }-${ value }-label` );
label.setAttribute( 'for', id );
const container = document.createElement( 'div' );
container.classList.add( 'cdx-radio' );
input.setAttribute( 'data-event-name', id );
container.appendChild( input );
container.appendChild( icon );
container.appendChild( label );
@ -100,6 +133,34 @@ function appendRadioToggle( parent, featureName, value, currentValue, config ) {
} );
}
/**
* @param {Element} form
* @param {string} featureName
* @param {HTMLElement} labelElement
* @param {string} currentValue
* @param {Record<string,ClientPreference>} config
*/
function appendToggleSwitch( form, featureName, labelElement, currentValue, config ) {
const input = makeInputElement( 'checkbox', featureName, currentValue );
input.classList.add( 'cdx-toggle-switch__input' );
const switcher = document.createElement( 'span' );
switcher.classList.add( 'cdx-toggle-switch__switch' );
const grip = document.createElement( 'span' );
grip.classList.add( 'cdx-toggle-switch__switch__grip' );
switcher.appendChild( grip );
const label = labelElement || makeLabelElement( featureName, currentValue );
label.classList.add( 'cdx-toggle-switch__label' );
const toggleSwitch = document.createElement( 'span' );
toggleSwitch.classList.add( 'cdx-toggle-switch' );
toggleSwitch.appendChild( input );
toggleSwitch.appendChild( switcher );
toggleSwitch.appendChild( label );
input.addEventListener( 'change', () => {
toggleDocClassAndSave( featureName, input.checked ? '1' : '0', config );
} );
form.appendChild( toggleSwitch );
}
/**
* @param {string} className
* @return {Element}
@ -110,6 +171,16 @@ function createRow( className ) {
return row;
}
/**
* Get the label for the feature.
*
* @param {string} featureName
* @return {MwMessage}
*/
const getFeatureLabelMsg = ( featureName ) =>
// eslint-disable-next-line mediawiki/msg-doc
mw.message( `${ featureName }-name` );
/**
* adds a toggle button
*
@ -117,7 +188,7 @@ function createRow( className ) {
* @param {Record<string,ClientPreference>} config
* @return {Element|null}
*/
function makeClientPreferenceBinaryToggle( featureName, config ) {
function makeControl( featureName, config ) {
const pref = config[ featureName ];
if ( !pref ) {
return null;
@ -130,9 +201,21 @@ function makeClientPreferenceBinaryToggle( featureName, config ) {
}
const row = createRow( '' );
const form = document.createElement( 'form' );
pref.options.forEach( ( value ) => {
appendRadioToggle( form, featureName, value, currentValue, config );
} );
const type = pref.type || 'radio';
switch ( type ) {
case 'radio':
pref.options.forEach( ( value ) => {
appendRadioToggle( form, featureName, value, currentValue, config );
} );
break;
case 'switch': {
const labelElement = document.createElement( 'label' );
labelElement.textContent = getFeatureLabelMsg( featureName ).text();
appendToggleSwitch( form, featureName, labelElement, currentValue, config );
break;
} default:
throw new Error( 'Unknown client preference! Only switch or radio are supported.' );
}
row.appendChild( form );
return row;
}
@ -143,8 +226,7 @@ function makeClientPreferenceBinaryToggle( featureName, config ) {
* @param {Record<string,ClientPreference>} config
*/
function makeClientPreference( parent, featureName, config ) {
// eslint-disable-next-line mediawiki/msg-doc
const labelMsg = mw.message( `${ featureName }-name` );
const labelMsg = getFeatureLabelMsg( featureName );
// If the user is not debugging messages and no language exists exit as its a hidden client preference.
if ( !labelMsg.exists() && mw.config.get( 'wgUserLanguage' ) !== 'qqx' ) {
return;
@ -152,18 +234,18 @@ function makeClientPreference( parent, featureName, config ) {
const id = `skin-client-prefs-${ featureName }`;
// @ts-ignore TODO: upstream patch URL
const portlet = mw.util.addPortlet( id, labelMsg.text() );
const labelElement = portlet.querySelector( 'label' );
// eslint-disable-next-line mediawiki/msg-doc
const descriptionMsg = mw.message( `${ featureName }-description` );
if ( descriptionMsg.exists() ) {
const desc = document.createElement( 'div' );
desc.classList.add( 'mw-portlet-description' );
const desc = document.createElement( 'span' );
desc.classList.add( 'skin-client-pref-description' );
desc.textContent = descriptionMsg.text();
const refNode = portlet.querySelector( 'label' );
if ( refNode && refNode.parentNode ) {
refNode.parentNode.insertBefore( desc, refNode.nextSibling );
if ( labelElement && labelElement.parentNode ) {
labelElement.appendChild( desc );
}
}
const row = makeClientPreferenceBinaryToggle( featureName, config );
const row = makeControl( featureName, config );
parent.appendChild( portlet );
if ( row ) {
const tmp = mw.util.addPortletLink( id, '', '' );
@ -190,13 +272,11 @@ function render( selector, config ) {
return Promise.reject();
}
return new Promise( ( resolve ) => {
mw.loader.using( 'codex-styles' ).then( () => {
getVisibleClientPreferences( config ).forEach( ( pref ) => {
makeClientPreference( node, pref, config );
} );
mw.requestIdleCallback( () => {
resolve( node );
} );
getVisibleClientPreferences( config ).forEach( ( pref ) => {
makeClientPreference( node, pref, config );
} );
mw.requestIdleCallback( () => {
resolve( node );
} );
} );
}

查看文件

@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`clientPreferences render empty 1`] = `""`;
exports[`clientPreferences render font size 1`] = `"<div><li><div class=\\"\\"><form><div class=\\"cdx-radio\\"><input name=\\"skin-client-pref-vector-feature-custom-font-size-group\\" id=\\"skin-client-pref-vector-feature-custom-font-size-value-0\\" type=\\"radio\\" value=\\"0\\" data-event-name=\\"skin-client-pref-vector-feature-custom-font-size-value-0\\" class=\\"cdx-radio__input\\"><span class=\\"cdx-radio__icon\\"></span><label for=\\"skin-client-pref-vector-feature-custom-font-size-value-0\\" class=\\"cdx-radio__label\\">vector-feature-custom-font-size-0-label</label></div><div class=\\"cdx-radio\\"><input name=\\"skin-client-pref-vector-feature-custom-font-size-group\\" id=\\"skin-client-pref-vector-feature-custom-font-size-value-1\\" type=\\"radio\\" value=\\"1\\" data-event-name=\\"skin-client-pref-vector-feature-custom-font-size-value-1\\" class=\\"cdx-radio__input\\"><span class=\\"cdx-radio__icon\\"></span><label for=\\"skin-client-pref-vector-feature-custom-font-size-value-1\\" class=\\"cdx-radio__label\\">vector-feature-custom-font-size-1-label</label></div><div class=\\"cdx-radio\\"><input name=\\"skin-client-pref-vector-feature-custom-font-size-group\\" id=\\"skin-client-pref-vector-feature-custom-font-size-value-2\\" type=\\"radio\\" value=\\"2\\" data-event-name=\\"skin-client-pref-vector-feature-custom-font-size-value-2\\" class=\\"cdx-radio__input\\"><span class=\\"cdx-radio__icon\\"></span><label for=\\"skin-client-pref-vector-feature-custom-font-size-value-2\\" class=\\"cdx-radio__label\\">vector-feature-custom-font-size-2-label</label></div></form></div></li></div>"`;
exports[`clientPreferences render toggle 1`] = `"<div><li><div class=\\"\\"><form><span class=\\"cdx-toggle-switch\\"><input name=\\"skin-client-pref-expandAll-group\\" id=\\"skin-client-pref-expandAll-value-1\\" type=\\"checkbox\\" data-event-name=\\"skin-client-pref-expandAll-value-1\\" class=\\"cdx-toggle-switch__input\\"><span class=\\"cdx-toggle-switch__switch\\"><span class=\\"cdx-toggle-switch__switch__grip\\"></span></span><label class=\\"cdx-toggle-switch__label\\">msg:expandAll-name</label></span></form></div></li></div>"`;

查看文件

@ -0,0 +1,59 @@
const clientPreferences = require( '../../resources/skins.vector.clientPreferences/clientPreferences.js' );
let cp;
describe( 'clientPreferences', () => {
beforeEach( () => {
document.body.innerHTML = '';
cp = document.createElement( 'div' );
cp.id = 'cp';
document.body.appendChild( cp );
mw.requestIdleCallback = ( callback ) => callback();
mw.user.clientPrefs = {
get: jest.fn( () => '1' )
};
const portlet = document.createElement( 'div' );
mw.util.addPortlet = jest.fn( () => portlet );
mw.util.addPortletLink = jest.fn( () => {
const li = document.createElement( 'li' );
const a = document.createElement( 'a' );
li.appendChild( a );
portlet.appendChild( li );
return li;
} );
mw.message = jest.fn( ( key ) => ( {
text: () => `msg:${ key }`,
exists: () => true
} ) );
} );
test( 'render empty', () => {
return clientPreferences.render( '#cp', {} ).then( () => {
expect( cp.innerHTML ).toMatchSnapshot();
} );
} );
test( 'render font size', () => {
document.documentElement.setAttribute( 'class', 'vector-feature-custom-font-size-clientpref-2' );
return clientPreferences.render( '#cp', {
'vector-feature-custom-font-size': {
options: [ '0', '1', '2' ],
preferenceKey: 'vector-font-size'
}
} ).then( () => {
expect( cp.innerHTML ).toMatchSnapshot();
} );
} );
test( 'render toggle', () => {
document.documentElement.setAttribute( 'class', 'expandAll-clientpref-1' );
return clientPreferences.render( '#cp', {
expandAll: {
options: [ '0', '1' ],
preferenceKey: 'expandAll',
type: 'switch'
}
} ).then( () => {
expect( cp.innerHTML ).toMatchSnapshot();
} );
} );
} );