Special:Preferences: Use OOjs UI
authorBartosz Dziewoński <matma.rex@gmail.com>
Sat, 16 Sep 2017 13:21:50 +0000 (15:21 +0200)
committerBartosz Dziewoński <matma.rex@gmail.com>
Mon, 13 Nov 2017 19:51:09 +0000 (20:51 +0100)
* Change the form to OOUI mode. Tweak some formatting to look better
  with this mode. Change various random links to be OOUI buttons.
* Rewrite custom tabs to use OO.ui.IndexLayout instead.
* Update styles and JS enhancements for OOUI widgets.
* Rename ResourceLoader modules so that old skin-specific styles
  (from $wgResourceModuleSkinStyles) no longer apply. They tend
  to make no sense with the OOUI styling.

Bug: T117781
Change-Id: Ie9396f0146f5020e52710c41e55ec86151ae0095

includes/Preferences.php
includes/specials/SpecialPreferences.php
includes/specials/forms/PreferencesForm.php
resources/Resources.php
resources/src/mediawiki.legacy/oldshared.css
resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js
resources/src/mediawiki.special/mediawiki.special.preferences.styles.css
resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js
resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js
tests/phpunit/includes/PreferencesTest.php
tests/selenium/pageobjects/preferences.page.js

index 94854fa..738f8ee 100644 (file)
@@ -82,6 +82,11 @@ class Preferences {
                        return self::$defaultPreferences;
                }
 
+               OutputPage::setupOOUI(
+                       strtolower( $context->getSkin()->getSkinName() ),
+                       $context->getLanguage()->getDir()
+               );
+
                $defaultPreferences = [];
 
                self::profilePreferences( $user, $context, $defaultPreferences );
@@ -320,14 +325,17 @@ class Preferences {
                if ( $canEditPrivateInfo && $authManager->allowsAuthenticationDataChange(
                        new PasswordAuthenticationRequest(), false )->isGood()
                ) {
-                       $link = $linkRenderer->makeLink( SpecialPage::getTitleFor( 'ChangePassword' ),
-                               $context->msg( 'prefs-resetpass' )->text(), [],
-                               [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
+                       $link = new OOUI\ButtonWidget( [
+                               'href' => SpecialPage::getTitleFor( 'ChangePassword' )->getLinkURL( [
+                                       'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
+                               ] ),
+                               'label' => $context->msg( 'prefs-resetpass' )->text(),
+                       ] );
 
                        $defaultPreferences['password'] = [
                                'type' => 'info',
                                'raw' => true,
-                               'default' => $link,
+                               'default' => (string)$link,
                                'label-message' => 'yourpassword',
                                'section' => 'personal/info',
                        ];
@@ -471,16 +479,15 @@ class Preferences {
 
                                $emailAddress = $user->getEmail() ? htmlspecialchars( $user->getEmail() ) : '';
                                if ( $canEditPrivateInfo && $authManager->allowsPropertyChange( 'emailaddress' ) ) {
-                                       $link = $linkRenderer->makeLink(
-                                               SpecialPage::getTitleFor( 'ChangeEmail' ),
-                                               $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
-                                               [],
-                                               [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ] );
-
-                                       $emailAddress .= $emailAddress == '' ? $link : (
-                                               $context->msg( 'word-separator' )->escaped()
-                                               . $context->msg( 'parentheses' )->rawParams( $link )->escaped()
-                                       );
+                                       $link = new OOUI\ButtonWidget( [
+                                               'href' => SpecialPage::getTitleFor( 'ChangeEmail' )->getLinkURL( [
+                                                       'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText()
+                                               ] ),
+                                               'label' =>
+                                                       $context->msg( $user->getEmail() ? 'prefs-changeemail' : 'prefs-setemail' )->text(),
+                                       ] );
+
+                                       $emailAddress .= $emailAddress == '' ? $link : ( '<br />' . $link );
                                }
 
                                $defaultPreferences['emailaddress'] = [
@@ -515,10 +522,10 @@ class Preferences {
                                        } else {
                                                $disableEmailPrefs = true;
                                                $emailauthenticated = $context->msg( 'emailnotauthenticated' )->parse() . '<br />' .
-                                                       $linkRenderer->makeKnownLink(
-                                                               SpecialPage::getTitleFor( 'Confirmemail' ),
-                                                               $context->msg( 'emailconfirmlink' )->text()
-                                                       ) . '<br />';
+                                                       new OOUI\ButtonWidget( [
+                                                               'href' => SpecialPage::getTitleFor( 'Confirmemail' )->getLinkURL(),
+                                                               'label' => $context->msg( 'emailconfirmlink' )->text(),
+                                                       ] );
                                                $emailauthenticationclass = "mw-email-not-authenticated";
                                        }
                                } else {
@@ -755,6 +762,7 @@ class Preferences {
                        'default' => $tzSetting,
                        'size' => 20,
                        'section' => 'rendering/timeoffset',
+                       'id' => 'wpTimeCorrection',
                ];
        }
 
@@ -997,7 +1005,7 @@ class Preferences {
 
                # # Watchlist #####################################
                if ( $user->isAllowed( 'editmywatchlist' ) ) {
-                       $editWatchlistLinks = [];
+                       $editWatchlistLinks = '';
                        $editWatchlistModes = [
                                'edit' => [ 'EditWatchlist', false ],
                                'raw' => [ 'EditWatchlist', 'raw' ],
@@ -1006,16 +1014,19 @@ class Preferences {
                        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
                        foreach ( $editWatchlistModes as $editWatchlistMode => $mode ) {
                                // Messages: prefs-editwatchlist-edit, prefs-editwatchlist-raw, prefs-editwatchlist-clear
-                               $editWatchlistLinks[] = $linkRenderer->makeKnownLink(
-                                       SpecialPage::getTitleFor( $mode[0], $mode[1] ),
-                                       new HtmlArmor( $context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse() )
-                               );
+                               $editWatchlistLinks .=
+                                       new OOUI\ButtonWidget( [
+                                               'href' => SpecialPage::getTitleFor( $mode[0], $mode[1] )->getLinkURL(),
+                                               'label' => new OOUI\HtmlSnippet(
+                                                       $context->msg( "prefs-editwatchlist-{$editWatchlistMode}" )->parse()
+                                               ),
+                                       ] );
                        }
 
                        $defaultPreferences['editwatchlist'] = [
                                'type' => 'info',
                                'raw' => true,
-                               'default' => $context->getLanguage()->pipeList( $editWatchlistLinks ),
+                               'default' => $editWatchlistLinks,
                                'label-message' => 'prefs-editwatchlist-label',
                                'section' => 'watchlist/editwatchlist',
                        ];
@@ -1138,6 +1149,12 @@ class Preferences {
                                'default' => $user->getTokenFromOption( 'watchlisttoken' ),
                                'help-message' => 'prefs-help-watchlist-token2',
                        ];
+                       $defaultPreferences['watchlisttoken-info2'] = [
+                               'type' => 'info',
+                               'section' => 'watchlist/tokenwatchlist',
+                               'raw' => true,
+                               'default' => $context->msg( 'prefs-help-watchlist-token2' )->parse(),
+                       ];
                }
        }
 
@@ -1358,6 +1375,9 @@ class Preferences {
                $formClass = 'PreferencesForm',
                array $remove = []
        ) {
+               // We use ButtonWidgets in some of the getPreferences() functions
+               $context->getOutput()->enableOOUI();
+
                $formDescriptor = self::getPreferences( $user, $context );
                if ( count( $remove ) ) {
                        $removeKeys = array_flip( $remove );
index 7c55e5c..ed2daff 100644 (file)
@@ -50,8 +50,8 @@ class SpecialPreferences extends SpecialPage {
                        return;
                }
 
-               $out->addModules( 'mediawiki.special.preferences' );
-               $out->addModuleStyles( 'mediawiki.special.preferences.styles' );
+               $out->addModules( 'mediawiki.special.preferences.ooui' );
+               $out->addModuleStyles( 'mediawiki.special.preferences.styles.ooui' );
 
                $session = $this->getRequest()->getSession();
                if ( $session->get( 'specialPreferencesSaveSuccess' ) ) {
@@ -83,37 +83,19 @@ class SpecialPreferences extends SpecialPage {
 
                $htmlForm = $this->getFormObject( $user, $this->getContext() );
                $htmlForm->setSubmitCallback( [ 'Preferences', 'tryUISubmit' ] );
-               $sectionTitles = $htmlForm->getPreferenceSections();
-
-               $prefTabs = '';
-               foreach ( $sectionTitles as $key ) {
-                       $prefTabs .= Html::rawElement( 'li',
-                               [
-                                       'role' => 'presentation',
-                                       'class' => ( $key === 'personal' ) ? 'selected' : null
-                               ],
-                               Html::rawElement( 'a',
-                                       [
-                                               'id' => 'preftab-' . $key,
-                                               'role' => 'tab',
-                                               'href' => '#mw-prefsection-' . $key,
-                                               'aria-controls' => 'mw-prefsection-' . $key,
-                                               'aria-selected' => ( $key === 'personal' ) ? 'true' : 'false',
-                                               'tabIndex' => ( $key === 'personal' ) ? 0 : -1,
-                                       ],
-                                       $htmlForm->getLegend( $key )
-                               )
-                       );
+
+               $prefTabs = [];
+               foreach ( $htmlForm->getPreferenceSections() as $key ) {
+                       $prefTabs[] = [
+                               'name' => $key,
+                               'label' => $htmlForm->getLegend( $key ),
+                       ];
                }
+               $out->addJsConfigVars( 'wgPreferencesTabs', $prefTabs );
+
+               // TODO: Render fake tabs here to avoid FOUC.
+               // $out->addHTML( $fakeTabs );
 
-               $out->addHTML(
-                       Html::rawElement( 'ul',
-                               [
-                                       'id' => 'preftoc',
-                                       'role' => 'tablist'
-                               ],
-                               $prefTabs )
-               );
                $htmlForm->show();
        }
 
@@ -136,7 +118,7 @@ class SpecialPreferences extends SpecialPage {
 
                $context = new DerivativeContext( $this->getContext() );
                $context->setTitle( $this->getPageTitle( 'reset' ) ); // Reset subpage
-               $htmlForm = new HTMLForm( [], $context, 'prefs-restore' );
+               $htmlForm = HTMLForm::factory( 'ooui', [], $context, 'prefs-restore' );
 
                $htmlForm->setSubmitTextMsg( 'restoreprefs' );
                $htmlForm->setSubmitDestructive();
index d4e5ef4..28cfb8b 100644 (file)
  * @file
  */
 
-use MediaWiki\MediaWikiServices;
-
 /**
  * Form to edit user preferences.
  */
-class PreferencesForm extends HTMLForm {
+class PreferencesForm extends OOUIHTMLForm {
        // Override default value from HTMLForm
        protected $mSubSectionBeforeFields = false;
 
@@ -71,8 +69,6 @@ class PreferencesForm extends HTMLForm {
         * @return string
         */
        function getButtons() {
-               $attrs = [ 'id' => 'mw-prefs-restoreprefs' ];
-
                if ( !$this->getModifiedUser()->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
                        return '';
                }
@@ -82,9 +78,14 @@ class PreferencesForm extends HTMLForm {
                if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) {
                        $t = $this->getTitle()->getSubpage( 'reset' );
 
-                       $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
-                       $html .= "\n" . $linkRenderer->makeLink( $t, $this->msg( 'restoreprefs' )->text(),
-                               Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ) );
+                       $html .= new OOUI\ButtonWidget( [
+                               'infusable' => true,
+                               'id' => 'mw-prefs-restoreprefs',
+                               'label' => $this->msg( 'restoreprefs' )->text(),
+                               'href' => $t->getLinkURL(),
+                               'flags' => [ 'destructive' ],
+                               'framed' => false,
+                       ] );
 
                        $html = Xml::tags( 'div', [ 'class' => 'mw-prefs-buttons' ], $html );
                }
index b4a3f2f..db23c33 100644 (file)
@@ -2092,7 +2092,7 @@ return [
        'mediawiki.special.pagesWithProp' => [
                'styles' => 'resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css',
        ],
-       'mediawiki.special.preferences' => [
+       'mediawiki.special.preferences.ooui' => [
                'scripts' => [
                        'resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js',
                        'resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js',
@@ -2109,9 +2109,11 @@ return [
                        'mediawiki.language',
                        'mediawiki.confirmCloseWindow',
                        'mediawiki.notification.convertmessagebox',
+                       'oojs-ui-widgets',
+                       'mediawiki.widgets.SelectWithInputWidget',
                ],
        ],
-       'mediawiki.special.preferences.styles' => [
+       'mediawiki.special.preferences.styles.ooui' => [
                'styles' => 'resources/src/mediawiki.special/mediawiki.special.preferences.styles.css',
        ],
        'mediawiki.special.recentchanges' => [
index 7b2d711..596b0d6 100644 (file)
@@ -220,28 +220,6 @@ table.toc td {
        font-size: larger;
 }
 
-/* preference page with js-genrated toc */
-#preftoc {
-       float: left;
-       margin: 1em 1em 1em 1em;
-       width: 13em;
-}
-
-#preftoc li {
-       border: 1px solid #fff;
-}
-
-#preftoc li.selected {
-       background-color: #f9f9f9;
-       border: 1px dashed #aaa;
-}
-
-#preftoc a,
-#preftoc a:active {
-       display: block;
-       color: #005189;
-}
-
 .mw-prefs-buttons {
        clear: left;
        float: left;
index 45df37f..fe127eb 100644 (file)
@@ -4,9 +4,11 @@
  */
 ( function ( mw, $ ) {
        $( function () {
-               var allowCloseWindow;
+               var allowCloseWindow, saveButton, restoreButton;
 
-               // Check if all of the form values are unchanged
+               // Check if all of the form values are unchanged.
+               // (This function could be changed to infuse and check OOUI widgets, but that would only make it
+               // slower and more complicated. It works fine to treat them as HTML elements.)
                function isPrefsChanged() {
                        var inputs = $( '#mw-prefs-form :input[name]' ),
                                input, $input, inputType,
                        return false;
                }
 
+               saveButton = OO.ui.infuse( $( '#prefcontrol' ) );
+               restoreButton = OO.ui.infuse( $( '#mw-prefs-restoreprefs' ) );
+
                // Disable the button to save preferences unless preferences have changed
                // Check if preferences have been changed before JS has finished loading
                if ( !isPrefsChanged() ) {
-                       $( '#prefcontrol' ).prop( 'disabled', true );
-                       $( '#preferences > fieldset' ).one( 'change keydown mousedown', function () {
-                               $( '#prefcontrol' ).prop( 'disabled', false );
+                       saveButton.setDisabled( true );
+                       $( '#preferences .oo-ui-fieldsetLayout' ).one( 'change keydown mousedown', function () {
+                               saveButton.setDisabled( false );
                        } );
                }
 
                        namespace: 'prefswarning'
                } );
                $( '#mw-prefs-form' ).submit( $.proxy( allowCloseWindow, 'release' ) );
-               $( '#mw-prefs-restoreprefs' ).click( $.proxy( allowCloseWindow, 'release' ) );
+               restoreButton.on( 'click', function () {
+                       allowCloseWindow.release();
+                       // The default behavior of events in OOUI is always prevented. Follow the link manually.
+                       // Note that middle-click etc. still works, as it doesn't emit a OOUI 'click' event.
+                       location.href = restoreButton.getHref();
+               } );
        } );
 }( mediaWiki, jQuery ) );
index 33b630a..294dcd0 100644 (file)
@@ -1,27 +1,28 @@
 /* Reuses colors from mediawiki.legacy/shared.css */
-.mw-email-not-authenticated .mw-input,
-.mw-email-none .mw-input {
+.mw-email-not-authenticated .oo-ui-labelWidget,
+.mw-email-none .oo-ui-labelWidget {
        border: 1px solid #fde29b;
        background-color: #fdf1d1;
        color: #000;
 }
 /* Authenticated email field has its own class too. Unstyled by default */
 /*
-.mw-email-authenticated .mw-input { }
+.mw-email-authenticated .oo-ui-labelWidget { }
 */
-/* This breaks due to nolabel styling */
-#preferences > fieldset td.mw-label {
-       width: 20%;
-}
 
-#preferences > fieldset table {
-       width: 100%;
+/* This is needed because add extra buttons in a weird way */
+.mw-prefs-buttons .mw-htmlform-submit-buttons {
+       margin: 0;
+       display: inline;
 }
-#preferences > fieldset table.mw-htmlform-matrix {
-       width: auto;
+
+.mw-prefs-buttons {
+       margin-top: 1em;
 }
 
-/* The CSS below is also for JS enabled version, because we want to prevent FOUC */
+#prefcontrol {
+       margin-right: 0.5em;
+}
 
 /*
  * Hide, but keep accessible for screen-readers.
        zoom: 1;
 }
 
-.client-nojs #preftoc {
-       display: none;
+/* Override OOUI styles so that dropdowns near the bottom of the form don't get clipped,
+ * e.g.'Appearance' / 'Threshold for stub link formatting'. This is hacky and bad, it would be
+ * better solved by setting overlays for the widgets, but we can't do it from PHP... */
+#preferences .oo-ui-panelLayout {
+       position: static;
+       overflow: visible;
+       -webkit-transform: none;
+       transform: none;
+}
+
+/* Tweak the margins to reduce the shifting of form contents
+ * after JS code loads and rearranges the page */
+.client-js #preferences > .oo-ui-panelLayout {
+       margin: 1em 0;
+}
+
+.client-js #preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
+       margin-left: 0.25em;
+}
+
+.client-js #preferences .oo-ui-panelLayout-framed .oo-ui-tabPanelLayout .oo-ui-panelLayout-framed {
+       margin-left: 0;
 }
 
-.client-js #preferences > fieldset {
-       display: none;
+.client-js #preferences .oo-ui-panelLayout-framed .oo-ui-tabPanelLayout {
+       padding-top: 0.5em;
+       padding-bottom: 0.5em;
 }
 
-/* Only the 1st tab is shown by default in JS mode */
-.client-js #preferences #mw-prefsection-personal {
-       display: block;
+.client-js #preferences > .oo-ui-panelLayout > .oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-header {
+       margin-bottom: 1em;
 }
index dcfad27..9f1691c 100644 (file)
@@ -3,29 +3,10 @@
  */
 ( function ( mw, $ ) {
        $( function () {
-               var $preftoc, $preferences, $fieldsets, labelFunc, previousTab;
+               var $preferences, tabs, wrapper, previousTab;
 
-               labelFunc = function () {
-                       return this.id.replace( /^mw-prefsection/g, 'preftab' );
-               };
-
-               $preftoc = $( '#preftoc' );
                $preferences = $( '#preferences' );
 
-               $fieldsets = $preferences.children( 'fieldset' )
-                       .attr( {
-                               role: 'tabpanel',
-                               'aria-labelledby': labelFunc
-                       } );
-               $fieldsets.not( '#mw-prefsection-personal' )
-                       .hide()
-                       .attr( 'aria-hidden', 'true' );
-
-               // T115692: The following is kept for backwards compatibility with older skins
-               $preferences.addClass( 'jsprefs' );
-               $fieldsets.addClass( 'prefsection' );
-               $fieldsets.children( 'legend' ).addClass( 'mainLegend' );
-
                // Make sure the accessibility tip is selectable so that screen reader users take notice,
                // but hide it per default to reduce interface clutter. Also make sure it becomes visible
                // when selected. Similar to jquery.mw-jump
                                } else {
                                        $( this ).css( 'height', 'auto' );
                                }
-                       } ).insertBefore( $preftoc );
+                       } ).prependTo( '#mw-content-text' );
 
-               /**
-                * It uses document.getElementById for security reasons (HTML injections in $()).
-                *
-                * @ignore
-                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
-                * @param {string} [mode] A hash will be set according to the current
-                *  open section. Set mode 'noHash' to surpress this.
-                */
-               function switchPrefTab( name, mode ) {
-                       var $tab, scrollTop;
+               tabs = new OO.ui.IndexLayout( {
+                       expanded: false,
+                       // Do not remove focus from the tabs menu after choosing a tab
+                       autoFocus: false
+               } );
+
+               mw.config.get( 'wgPreferencesTabs' ).forEach( function ( tabConfig ) {
+                       var panel, $panelContents;
+
+                       panel = new OO.ui.TabPanelLayout( tabConfig.name, {
+                               expanded: false,
+                               label: tabConfig.label
+                       } );
+                       $panelContents = $( '#mw-prefsection-' + tabConfig.name );
+
+                       // Hide the unnecessary PHP PanelLayouts
+                       // (Do not use .remove(), as that would remove event handlers for everything inside them)
+                       $panelContents.parent().detach();
+
+                       panel.$element.append( $panelContents );
+                       tabs.addTabPanels( [ panel ] );
+
+                       // Remove duplicate labels
+                       // (This must be after .addTabPanels(), otherwise the tab item doesn't exist yet)
+                       $panelContents.children( 'legend' ).remove();
+                       $panelContents.attr( 'aria-labelledby', panel.getTabItem().getElementId() );
+               } );
+
+               wrapper = new OO.ui.PanelLayout( {
+                       expanded: false,
+                       padded: false,
+                       framed: true
+               } );
+               wrapper.$element.append( tabs.$element );
+               $preferences.prepend( wrapper.$element );
+
+               function updateHash( panel ) {
+                       var scrollTop, active;
                        // Handle hash manually to prevent jumping,
                        // therefore save and restore scrollTop to prevent jumping.
                        scrollTop = $( window ).scrollTop();
-                       if ( mode !== 'noHash' ) {
-                               location.hash = '#mw-prefsection-' + name;
+                       // Changing the hash apparently causes keyboard focus to be lost?
+                       // Save and restore it. This makes no sense though.
+                       active = document.activeElement;
+                       location.hash = '#mw-prefsection-' + panel.getName();
+                       if ( active ) {
+                               active.focus();
                        }
                        $( window ).scrollTop( scrollTop );
-
-                       $preftoc.find( 'li' ).removeClass( 'selected' )
-                               .find( 'a' ).attr( {
-                                       tabIndex: -1,
-                                       'aria-selected': 'false'
-                               } );
-
-                       $tab = $( document.getElementById( 'preftab-' + name ) );
-                       if ( $tab.length ) {
-                               $tab.attr( {
-                                       tabIndex: 0,
-                                       'aria-selected': 'true'
-                               } ).focus()
-                                       .parent().addClass( 'selected' );
-
-                               $preferences.children( 'fieldset' ).hide().attr( 'aria-hidden', 'true' );
-                               $( document.getElementById( 'mw-prefsection-' + name ) ).show().attr( 'aria-hidden', 'false' );
-                       }
                }
 
-               // Enable keyboard users to use left and right keys to switch tabs
-               $preftoc.on( 'keydown', function ( event ) {
-                       var keyLeft = 37,
-                               keyRight = 39,
-                               $el;
-
-                       if ( event.keyCode === keyLeft ) {
-                               $el = $( '#preftoc li.selected' ).prev().find( 'a' );
-                       } else if ( event.keyCode === keyRight ) {
-                               $el = $( '#preftoc li.selected' ).next().find( 'a' );
-                       } else {
-                               return;
+               tabs.on( 'set', updateHash );
+
+               /**
+                * @ignore
+                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
+                * @param {string} [mode] A hash will be set according to the current
+                *  open section. Set mode 'noHash' to supress this.
+                */
+               function switchPrefTab( name, mode ) {
+                       if ( mode === 'noHash' ) {
+                               tabs.off( 'set', updateHash );
                        }
-                       if ( $el.length > 0 ) {
-                               switchPrefTab( $el.attr( 'href' ).replace( '#mw-prefsection-', '' ) );
+                       tabs.setTabPanel( name );
+                       if ( mode === 'noHash' ) {
+                               tabs.on( 'set', updateHash );
                        }
-               } );
+               }
 
                // Jump to correct section as indicated by the hash.
                // This function is called onload and onhashchange.
                        }
                }
 
-               // In browsers that support the onhashchange event we will not bind click
-               // handlers and instead let the browser do the default behavior (clicking the
-               // <a href="#.."> will naturally set the hash, handled by onhashchange.
-               // But other things that change the hash will also be caught (e.g. using
+               // Handle other things that change the hash (e.g. using
                // the Back and Forward browser navigation).
-               // Note the special check for IE "compatibility" mode.
-               if ( 'onhashchange' in window &&
-                       ( document.documentMode === undefined || document.documentMode >= 8 )
-               ) {
+               if ( 'onhashchange' in window ) {
                        $( window ).on( 'hashchange', function () {
                                var hash = location.hash;
                                if ( hash.match( /^#mw-[\w-]+/ ) ) {
                                } else if ( hash === '' ) {
                                        switchPrefTab( 'personal', 'noHash' );
                                }
-                       } )
-                               // Run the function immediately to select the proper tab on startup.
-                               .trigger( 'hashchange' );
-               // In older browsers we'll bind a click handler as fallback.
-               // We must not have onhashchange *and* the click handlers, otherwise
-               // the click handler calls switchPrefTab() which sets the hash value,
-               // which triggers onhashchange and calls switchPrefTab() again.
-               } else {
-                       $preftoc.on( 'click', 'li a', function ( e ) {
-                               switchPrefTab( $( this ).attr( 'href' ).replace( '#mw-prefsection-', '' ) );
-                               e.preventDefault();
                        } );
-                       // If we've reloaded the page or followed an open-in-new-window,
-                       // make the selected tab visible.
-                       detectHash();
                }
 
+               // If we've reloaded the page or followed an open-in-new-window,
+               // make the selected tab visible.
+               detectHash();
+
                // Restore the active tab after saving the preferences
                previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
                if ( previousTab ) {
                }
 
                $( '#mw-prefs-form' ).on( 'submit', function () {
-                       var value = $( $preftoc ).find( 'li.selected a' ).attr( 'id' ).replace( 'preftab-', '' );
+                       var value = tabs.getCurrentTabPanelName();
                        mw.storage.session.set( 'mwpreferences-prevTab', value );
                } );
 
index 03656ee..7fbcc77 100644 (file)
@@ -4,13 +4,19 @@
 ( function ( mw, $ ) {
        $( function () {
                var
-                       $tzSelect, $tzTextbox, $localtimeHolder, servertime;
+                       timezoneWidget, $localtimeHolder, servertime;
 
                // Timezone functions.
                // Guesses Timezone from browser and updates fields onchange.
 
-               $tzSelect = $( '#mw-input-wptimecorrection' );
-               $tzTextbox = $( '#mw-input-wptimecorrection-other' );
+               // This is identical to OO.ui.infuse( ... ), but it makes the class name of the result known.
+               try {
+                       timezoneWidget = mw.widgets.SelectWithInputWidget.static.infuse( $( '#wpTimeCorrection' ) );
+               } catch ( err ) {
+                       // This preference could theoretically be disabled ($wgHiddenPrefs)
+                       timezoneWidget = null;
+               }
+
                $localtimeHolder = $( '#wpLocalTime' );
                servertime = parseInt( $( 'input[name="wpServerTime"]' ).val(), 10 );
 
 
                function updateTimezoneSelection() {
                        var minuteDiff, localTime,
-                               type = $tzSelect.val();
+                               type = timezoneWidget.dropdowninput.getValue();
 
                        if ( type === 'other' ) {
                                // User specified time zone manually in <input>
                                // Grab data from the textbox, parse it.
-                               minuteDiff = hoursToMinutes( $tzTextbox.val() );
+                               minuteDiff = hoursToMinutes( timezoneWidget.textinput.getValue() );
                        } else {
                                // Time zone not manually specified by user
                                if ( type === 'guess' ) {
                                        // Get browser timezone & fill it in
                                        minuteDiff = -( new Date().getTimezoneOffset() );
-                                       $tzTextbox.val( minutesToHours( minuteDiff ) );
-                                       $tzSelect.val( 'other' );
+                                       timezoneWidget.textinput.setValue( minutesToHours( minuteDiff ) );
+                                       timezoneWidget.dropdowninput.setValue( 'other' );
                                } else {
-                                       // Grab data from the $tzSelect value
+                                       // Grab data from the dropdown value
                                        minuteDiff = parseInt( type.split( '|' )[ 1 ], 10 ) || 0;
                                }
                        }
@@ -76,9 +82,9 @@
                        $localtimeHolder.text( mw.language.convertNumber( minutesToHours( localTime ) ) );
                }
 
-               if ( $tzSelect.length && $tzTextbox.length ) {
-                       $tzSelect.change( updateTimezoneSelection );
-                       $tzTextbox.blur( updateTimezoneSelection );
+               if ( timezoneWidget ) {
+                       timezoneWidget.dropdowninput.on( 'change', updateTimezoneSelection );
+                       timezoneWidget.textinput.on( 'change', updateTimezoneSelection );
                        updateTimezoneSelection();
                }
 
index d78c1e7..b25e046 100644 (file)
@@ -150,6 +150,13 @@ class PreferencesTest extends MediaWikiTestCase {
 
        /** Helper */
        protected function prefsFor( $user_key ) {
+               // TODO This should use Preferences::getPreferences() instead of calling internal methods.
+               // Unfortunately that currently ignores the $user parameter if it has cached data, even for
+               // a different user...
+               OutputPage::setupOOUI(
+                       strtolower( $this->context->getSkin()->getSkinName() ),
+                       $this->context->getLanguage()->getDir()
+               );
                $preferences = [];
                Preferences::profilePreferences(
                        $this->prefUsers[$user_key],
index 98b87fe..890fe5b 100644 (file)
@@ -3,8 +3,8 @@ const Page = require( './page' );
 
 class PreferencesPage extends Page {
 
-       get realName() { return browser.element( '#mw-input-wprealname' ); }
-       get save() { return browser.element( '#prefcontrol' ); }
+       get realName() { return browser.element( '#mw-input-wprealname .oo-ui-inputWidget-input' ); }
+       get save() { return browser.element( '#prefcontrol .oo-ui-buttonElement-button' ); }
 
        open() {
                super.open( 'Special:Preferences' );