From fc65ff17d9f132aa332c99f7da44b92f6a372826 Mon Sep 17 00:00:00 2001 From: Ed Sanders Date: Mon, 23 Apr 2018 12:56:13 +0100 Subject: [PATCH] Special:Preferences: Construct fake tabs to avoid FOUC Bug: T192769 Bug: T189366 Change-Id: I4aabda97d14d97dce3e35abda2ce82925d721c9b --- includes/htmlform/HTMLForm.php | 7 +- includes/htmlform/OOUIHTMLForm.php | 2 +- includes/specials/SpecialPreferences.php | 4 +- .../specials/forms/PreferencesFormOOUI.php | 98 ++++++++++++++++++- resources/Resources.php | 10 +- .../tabs.js | 1 + ...iawiki.special.preferences.styles.ooui.css | 54 +++++++--- 7 files changed, 155 insertions(+), 21 deletions(-) diff --git a/includes/htmlform/HTMLForm.php b/includes/htmlform/HTMLForm.php index ff6cfffb56..6b2afe12f5 100644 --- a/includes/htmlform/HTMLForm.php +++ b/includes/htmlform/HTMLForm.php @@ -1604,9 +1604,10 @@ class HTMLForm extends ContextSource { * @param string $legend Legend text for the fieldset * @param string $section The section content in plain Html * @param array $attributes Additional attributes for the fieldset + * @param bool $isRoot Section is at the root of the tree * @return string The fieldset's Html */ - protected function wrapFieldSetSection( $legend, $section, $attributes ) { + protected function wrapFieldSetSection( $legend, $section, $attributes, $isRoot ) { return Xml::fieldset( $legend, $section, $attributes ) . "\n"; } @@ -1689,7 +1690,9 @@ class HTMLForm extends ContextSource { if ( $fieldsetIDPrefix ) { $attributes['id'] = Sanitizer::escapeIdForAttribute( "$fieldsetIDPrefix$key" ); } - $subsectionHtml .= $this->wrapFieldSetSection( $legend, $section, $attributes ); + $subsectionHtml .= $this->wrapFieldSetSection( + $legend, $section, $attributes, $fields === $this->mFieldTree + ); } else { // Just return the inputs, nothing fancy. $subsectionHtml .= $section; diff --git a/includes/htmlform/OOUIHTMLForm.php b/includes/htmlform/OOUIHTMLForm.php index ba36888ca8..49cbdee6e0 100644 --- a/includes/htmlform/OOUIHTMLForm.php +++ b/includes/htmlform/OOUIHTMLForm.php @@ -145,7 +145,7 @@ class OOUIHTMLForm extends HTMLForm { [ 'class' => 'mw-htmlform-submit-buttons' ], "\n$buttons" ) . "\n"; } - protected function wrapFieldSetSection( $legend, $section, $attributes ) { + protected function wrapFieldSetSection( $legend, $section, $attributes, $isRoot ) { // to get a user visible effect, wrap the fieldset into a framed panel layout $layout = new OOUI\PanelLayout( [ 'expanded' => false, diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php index 1cfcffa85d..7a4cde99fb 100644 --- a/includes/specials/SpecialPreferences.php +++ b/includes/specials/SpecialPreferences.php @@ -73,6 +73,7 @@ class SpecialPreferences extends SpecialPage { if ( $this->oouiEnabled ) { $out->addModules( 'mediawiki.special.preferences.ooui' ); $out->addModuleStyles( 'mediawiki.special.preferences.styles.ooui' ); + $out->addModuleStyles( 'oojs-ui-widgets.styles' ); } else { $out->addModules( 'mediawiki.special.preferences' ); $out->addModuleStyles( 'mediawiki.special.preferences.styles' ); @@ -118,9 +119,6 @@ class SpecialPreferences extends SpecialPage { ]; } $out->addJsConfigVars( 'wgPreferencesTabs', $prefTabs ); - - // TODO: Render fake tabs here to avoid FOUC. - // $out->addHTML( $fakeTabs ); } else { $prefTabs = ''; diff --git a/includes/specials/forms/PreferencesFormOOUI.php b/includes/specials/forms/PreferencesFormOOUI.php index a781254352..3a5adbb07f 100644 --- a/includes/specials/forms/PreferencesFormOOUI.php +++ b/includes/specials/forms/PreferencesFormOOUI.php @@ -114,12 +114,108 @@ class PreferencesFormOOUI extends OOUIHTMLForm implements PreferencesForm { return $data; } + protected function wrapFieldSetSection( $legend, $section, $attributes, $isRoot ) { + // to get a user visible effect, wrap the fieldset into a framed panel layout + if ( $isRoot ) { + // Mimic TabPanelLayout + $wrapper = new OOUI\PanelLayout( [ + 'expanded' => false, + 'scrollable' => true, + // Framed and padded for no-JS, frame hidden with CSS + 'framed' => true, + 'infusable' => false, + 'classes' => [ 'oo-ui-stackLayout oo-ui-indexLayout-stackLayout' ] + ] ); + $layout = new OOUI\PanelLayout( [ + 'expanded' => false, + 'scrollable' => true, + 'infusable' => false, + 'classes' => [ 'oo-ui-tabPanelLayout' ] + ] ); + $wrapper->appendContent( $layout ); + } else { + $wrapper = $layout = new OOUI\PanelLayout( [ + 'expanded' => false, + 'padded' => true, + 'framed' => true, + 'infusable' => false, + ] ); + } + + $layout->appendContent( + new OOUI\FieldsetLayout( [ + 'label' => $legend, + 'infusable' => false, + 'items' => [ + new OOUI\Widget( [ + 'content' => new OOUI\HtmlSnippet( $section ) + ] ), + ], + ] + $attributes ) + ); + return $wrapper; + } + /** * Get the whole body of the form. * @return string */ function getBody() { - return $this->displaySection( $this->mFieldTree, '', 'mw-prefsection-' ); + // Construct fake tabs to avoid FOUC. The structure mimics OOUI's tabPanelLayout. + // TODO: Consider creating an infusable TabPanelLayout in OOUI-PHP. + $fakeTabs = []; + foreach ( $this->getPreferenceSections() as $i => $key ) { + $fakeTabs[] = + Html::rawElement( + 'div', + [ + 'class' => + 'oo-ui-widget oo-ui-widget-enabled oo-ui-optionWidget '. + 'oo-ui-tabOptionWidget oo-ui-labelElement' . + ( $i === 0 ? ' oo-ui-optionWidget-selected' : '' ) + ], + Html::element( + 'a', + [ + 'class' => 'oo-ui-labelElement-label', + // Make this a usable link instead of a span so the tabs + // can be used before JS runs + 'href' => '#mw-prefsection-' . $key + ], + $this->getLegend( $key ) + ) + ); + } + $fakeTabsHtml = Html::rawElement( + 'div', + [ 'class' => 'oo-ui-layout oo-ui-panelLayout oo-ui-indexLayout-tabPanel' ], + Html::rawElement( + 'div', + [ 'class' => 'oo-ui-widget oo-ui-widget-enabled oo-ui-selectWidget '. + 'oo-ui-selectWidget-depressed oo-ui-tabSelectWidget' ], + implode( $fakeTabs ) + ) + ); + + return Html::rawElement( + 'div', + [ 'class' => 'oo-ui-layout oo-ui-panelLayout oo-ui-panelLayout-framed mw-prefs-faketabs' ], + Html::rawElement( + 'div', + [ 'class' => 'oo-ui-layout oo-ui-menuLayout oo-ui-menuLayout-static ' . + 'oo-ui-menuLayout-top oo-ui-menuLayout-showMenu oo-ui-indexLayout' ], + Html::rawElement( + 'div', + [ 'class' => 'oo-ui-menuLayout-menu' ], + $fakeTabsHtml + ) . + Html::rawElement( + 'div', + [ 'class' => 'oo-ui-menuLayout-content' ], + $this->displaySection( $this->mFieldTree, '', 'mw-prefsection-' ) + ) + ) + ); } /** diff --git a/resources/Resources.php b/resources/Resources.php index 77391a8ce7..132a15abd2 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -2888,9 +2888,9 @@ return [ 'oojs-ui-widgets' => [ 'class' => ResourceLoaderOOUIFileModule::class, 'scripts' => 'resources/lib/oojs-ui/oojs-ui-widgets.js', - 'themeStyles' => 'widgets', 'dependencies' => [ 'oojs-ui-core', + 'oojs-ui-widgets.styles', 'oojs-ui.styles.icons-interactions', 'oojs-ui.styles.icons-content', 'oojs-ui.styles.icons-editing-advanced', @@ -2909,6 +2909,14 @@ return [ ], 'targets' => [ 'desktop', 'mobile' ], ], + // You should never directly load this module. The CSS classes it defines are not a public API, + // they depend on the internal structure of OOUI widgets, which can change at any time. If you + // find that you need to load this module, you're probably doing something wrong or very hacky. + 'oojs-ui-widgets.styles' => [ + 'class' => ResourceLoaderOOUIFileModule::class, + 'themeStyles' => 'widgets', + 'targets' => [ 'desktop', 'mobile' ], + ], // Toolbar and tools module. 'oojs-ui-toolbars' => [ 'class' => ResourceLoaderOOUIFileModule::class, diff --git a/resources/src/mediawiki.special.preferences.ooui/tabs.js b/resources/src/mediawiki.special.preferences.ooui/tabs.js index c948ff09f2..40c9df932d 100644 --- a/resources/src/mediawiki.special.preferences.ooui/tabs.js +++ b/resources/src/mediawiki.special.preferences.ooui/tabs.js @@ -56,6 +56,7 @@ } ); wrapper.$element.append( tabs.$element ); $preferences.prepend( wrapper.$element ); + $( '.mw-prefs-faketabs' ).remove(); function updateHash( panel ) { var scrollTop, active; diff --git a/resources/src/mediawiki.special.preferences.styles.ooui.css b/resources/src/mediawiki.special.preferences.styles.ooui.css index 8810318e98..a72186b4b8 100644 --- a/resources/src/mediawiki.special.preferences.styles.ooui.css +++ b/resources/src/mediawiki.special.preferences.styles.ooui.css @@ -45,35 +45,63 @@ transform: none; } -#preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed { - border-color: #c8ccd1; - border-width: 1px 0 0; +#preferences .oo-ui-menuLayout .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed { + border-width: 0; border-radius: 0; + box-shadow: none; padding-left: 0; padding-right: 0; +} + +.mw-prefs-faketabs > .oo-ui-menuLayout > .oo-ui-menuLayout-menu a { + color: inherit; + text-decoration: none; +} + +/* Adjust the borders when JS is disabled: frame each prefsection instead of the + * whole tabLayout wrapper */ +.client-nojs #preferences .oo-ui-menuLayout .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed { + border-color: #c8ccd1; + border-width: 1px 0 0; +} + +.client-nojs .mw-prefs-faketabs { + border-width: 0; + border-radius: 0; box-shadow: 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-nojs .mw-prefs-faketabs > .oo-ui-menuLayout > .oo-ui-menuLayout-content > .oo-ui-stackLayout { + margin-bottom: 1em; } -.client-js #preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed { - margin-left: 0.25em; +/* Hide the tab menu when JS is disabled as we can't use this feature */ +.client-nojs .mw-prefs-faketabs > .oo-ui-menuLayout > .oo-ui-menuLayout-menu { + display: none; +} + +.client-nojs #preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed:last-child { + padding-bottom: 0; + margin-bottom: 0; +} + +/* Hide top level legends when JS is enabled, as they will not be visible + * when the real tabLayout is built */ +.client-js #preferences .oo-ui-tabPanelLayout > fieldset > legend { + display: none; } .client-js #preferences .oo-ui-tabPanelLayout { padding-top: 0.5em; - padding-bottom: 0.5em; } -.client-js #preferences .oo-ui-tabPanelLayout .oo-ui-panelLayout-framed { +.client-js #preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed { margin-left: 0; margin-bottom: 0; - border: 0; - padding-top: 0; + padding: 0; + border-width: 0; + border-radius: 0; + box-shadow: none; } .client-js #preferences > .oo-ui-panelLayout > .oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-header { -- 2.20.1