[feature] add menu button to toggle panel visibility

Add a menu button that toggles the panel's (also referred to as a
sidebar) collapse state. When the screen is wide enough, animate the
transition.

The menu icon from OOUI is copied into Vector to avoid two
ResourceLoaders modules (collapseHorizontal icon isn't ready for
inclusion in the OOUI icon pack and ResourceLoaderOOUIIconPackModule
doesn't support images).

Additional polish and collaboration is needed but this patch fulfills
the scope of its referenced task.

Bug: T246419
Depends-On: I8e153c0ab927f9d880a68fb9efb0bf37b91d26b2
Change-Id: Ic9d54de7e19ef8d5dfd703d95a45b78c0aaf791a
This commit is contained in:
Stephen Niedzielski 2020-03-30 14:07:35 -06:00 committed by VolkerE
parent d49eb1e0ff
commit 5195f5fd67
15 changed files with 272 additions and 14 deletions

View file

@ -10,6 +10,7 @@
"vector-opt-out-tooltip": "Change your settings to go back to the old look of the skin (legacy Vector)", "vector-opt-out-tooltip": "Change your settings to go back to the old look of the skin (legacy Vector)",
"vector.css": "/* All CSS here will be loaded for users of the Vector skin */", "vector.css": "/* All CSS here will be loaded for users of the Vector skin */",
"vector.js": "/* All JavaScript here will be loaded for users of the Vector skin */", "vector.js": "/* All JavaScript here will be loaded for users of the Vector skin */",
"vector-action-toggle-sidebar": "Toggle sidebar",
"vector-action-addsection": "Add topic", "vector-action-addsection": "Add topic",
"vector-action-delete": "Delete", "vector-action-delete": "Delete",
"vector-action-move": "Move", "vector-action-move": "Move",

View file

@ -21,6 +21,7 @@
"vector-opt-out-tooltip": "Used as the tooltip for the Vector opt-out link", "vector-opt-out-tooltip": "Used as the tooltip for the Vector opt-out link",
"vector.css": "{{optional}}", "vector.css": "{{optional}}",
"vector.js": "{{optional}}", "vector.js": "{{optional}}",
"vector-action-toggle-sidebar": "Accessibility label for the button that toggles the sidebar's visibility, as well as audible presentation for screen readers.",
"vector-action-addsection": "Used in the Vector skin. See for example {{canonicalurl:Talk:Main_Page|useskin=vector}}\n{{Identical|Add topic}}", "vector-action-addsection": "Used in the Vector skin. See for example {{canonicalurl:Talk:Main_Page|useskin=vector}}\n{{Identical|Add topic}}",
"vector-action-delete": "Used in the Vector skin, as the name of a tab at the top of the page. See for example {{canonicalurl:Translating:MediaWiki|useskin=vector}}\n\n{{Identical|Delete}}", "vector-action-delete": "Used in the Vector skin, as the name of a tab at the top of the page. See for example {{canonicalurl:Translating:MediaWiki|useskin=vector}}\n\n{{Identical|Delete}}",
"vector-action-move": "Used in the Vector skin, on the tabs at the top of the page. See for example {{canonicalurl:Talk:Main_Page|useskin=vector}}\n\n{{Identical|Move}}", "vector-action-move": "Used in the Vector skin, on the tabs at the top of the page. See for example {{canonicalurl:Talk:Main_Page|useskin=vector}}\n\n{{Identical|Move}}",

View file

@ -80,11 +80,17 @@ class SkinVector extends SkinTemplate {
*/ */
public function getDefaultModules() { public function getDefaultModules() {
$modules = parent::getDefaultModules(); $modules = parent::getDefaultModules();
// add vector skin styles and vector module
$module = $this->isLegacy() if ( $this->isLegacy() ) {
? 'skins.vector.styles.legacy' : 'skins.vector.styles'; $modules['styles']['skin'][] = 'skins.vector.styles.legacy';
$modules['styles']['skin'][] = $module; $modules[Constants::SKIN_NAME] = 'skins.vector.legacy.js';
$modules['core'][] = $this->isLegacy() ? 'skins.vector.legacy.js' : 'skins.vector.js'; } else {
$modules['styles'] = array_merge(
$modules['styles'],
[ 'skins.vector.styles', 'mediawiki.ui.icon', 'skins.vector.icons' ]
);
$modules[Constants::SKIN_NAME][] = 'skins.vector.js';
}
return $modules; return $modules;
} }

View file

@ -371,6 +371,9 @@ class VectorTemplate extends BaseTemplate {
), ),
'array-portals-rest' => array_slice( $props, 1 ), 'array-portals-rest' => array_slice( $props, 1 ),
'data-portals-first' => $firstPortal, 'data-portals-first' => $firstPortal,
'msg-toggle-sidebar-button-label' => $this->msg( 'vector-action-toggle-sidebar' )->text(),
// [todo] fetch user preference when logged in (T246427).
'sidebar-visible' => false
]; ];
} }

View file

@ -7,9 +7,33 @@
MenuDefinition data-portals-first MenuDefinition data-portals-first
MenuDefinition[] array-portals-rest MenuDefinition[] array-portals-rest
emphasized-sidebar-action data-emphasized-sidebar-action For displaying an emphasized action in the sidebar. emphasized-sidebar-action data-emphasized-sidebar-action For displaying an emphasized action in the sidebar.
string msg-toggle-sidebar-button-label The label used by the sidebar button.
boolean sidebar-visible For users that want to see the sidebar on initial render, this should be
true.
}} }}
<div id="mw-panel" class="mw-sidebar"> <input
type="checkbox"
id="mw-sidebar-checkbox"
class="mw-checkbox-hack-checkbox"
role="button"
{{#sidebar-visible}}checked{{/sidebar-visible}}
aria-labelledby="mw-sidebar-button"
aria-controls="mw-panel">
<label
id="mw-sidebar-button"
class="
mw-checkbox-hack-button
mw-ui-icon
mw-ui-icon-element
{{#sidebar-visible}}mw-ui-icon-wikimedia-collapseHorizontal-base20{{/sidebar-visible}}
{{^sidebar-visible}}mw-ui-icon-wikimedia-menu-base20{{/sidebar-visible}}
"
for="mw-sidebar-checkbox"
data-event-name="ui.sidebar">
{{msg-toggle-sidebar-button-label}}
</label>
<div id="mw-panel" class="mw-sidebar mw-checkbox-hack-target">
{{#data-portals-first}}{{>Menu}}{{/data-portals-first}} {{#data-portals-first}}{{>Menu}}{{/data-portals-first}}
{{#data-emphasized-sidebar-action}} {{#data-emphasized-sidebar-action}}
<div class="mw-sidebar-action"> <div class="mw-sidebar-action">

View file

@ -14,6 +14,21 @@
"cleverLinks": true, "cleverLinks": true,
"default": { "default": {
"useLongnameInNav": true "useLongnameInNav": true
},
"wmf": {
"linkMap": {
"Document": "https://developer.mozilla.org/docs/Web/API/Document",
"HTMLElement": "https://developer.mozilla.org/docs/Web/API/HTMLElement",
"HTMLInputElement": "https://developer.mozilla.org/docs/Web/API/HTMLInputElement",
"Window": "https://developer.mozilla.org/docs/Web/API/Window",
"CheckboxHack": "https://doc.wikimedia.org/mediawiki-core/master/js",
"MW": "https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw",
"JQueryStatic": "https://api.jquery.com",
"void": "#"
}
} }
} }
} }

16
resources/CheckboxHack.d.ts vendored Normal file
View file

@ -0,0 +1,16 @@
interface CheckboxHack {
updateAriaExpanded(checkbox: HTMLInputElement): void;
bindUpdateAriaExpandedOnInput(checkbox: HTMLInputElement): CheckboxHackListeners;
bindToggleOnClick(checkbox: HTMLInputElement, button: HTMLElement): CheckboxHackListeners;
bindDismissOnClickOutside(window: Window, checkbox: HTMLInputElement, button: HTMLElement, target: Node): CheckboxHackListeners;
bindDismissOnFocusLoss(window: Window, checkbox: HTMLInputElement, button: HTMLElement, target: Node): CheckboxHackListeners;
bind(window: Window, checkbox: HTMLInputElement, button: HTMLElement, target: Node): CheckboxHackListeners;
unbind(window: Window, checkbox: HTMLInputElement, button: HTMLElement, listeners: CheckboxHackListeners): void;
}
interface CheckboxHackListeners {
onUpdateAriaExpandedOnInput?: EventListenerOrEventListenerObject;
onToggleOnClick?: EventListenerOrEventListenerObject;
onDismissOnClickOutside?: EventListenerOrEventListenerObject;
onDismissOnFocusLoss?: EventListenerOrEventListenerObject;
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<title>
collapse
</title>
<path d="M9 2l1.3 1.3L3.7 10l6.6 6.7L9 18l-8-8 8-8zm8.5 0L19 3.3 12.2 10l6.7 6.7-1.4 1.3-8-8 8-8z"/>
</svg>

After

Width:  |  Height:  |  Size: 261 B

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>menu</title><path d="M1 3v2h18V3zm0 8h18V9H1zm0 6h18v-2H1z"/></svg>

After

Width:  |  Height:  |  Size: 195 B

View file

@ -1,10 +1,56 @@
var /** @type {CheckboxHack} */ var checkboxHack =
collapsibleTabs = require( '../skins.vector.legacy.js/collapsibleTabs.js' ), require( /** @type {string} */ ( 'mediawiki.page.ready' ) ).checkboxHack;
vector = require( '../skins.vector.legacy.js/vector.js' ); var collapsibleTabs = require( '../skins.vector.legacy.js/collapsibleTabs.js' );
var vector = require( '../skins.vector.legacy.js/vector.js' );
function main() { /**
collapsibleTabs.init(); * Update the state of the menu icon to be an expanded or collapsed icon.
$( vector.init ); * @param {HTMLInputElement} checkbox
* @param {HTMLElement} button
* @return {void}
*/
function updateMenuIcon( checkbox, button ) {
button.classList.remove(
checkbox.checked ?
'mw-ui-icon-wikimedia-menu-base20' :
'mw-ui-icon-wikimedia-collapseHorizontal-base20'
);
button.classList.add(
checkbox.checked ?
'mw-ui-icon-wikimedia-collapseHorizontal-base20' :
'mw-ui-icon-wikimedia-menu-base20'
);
} }
main(); /**
* Improve the interactivity of the sidebar panel by binding optional checkbox hack enhancements
* for focus and `aria-expanded`. Also, flip the icon image on click.
* @param {Document} document
* @return {void}
*/
function initSidebar( document ) {
var checkbox = document.getElementById( 'mw-sidebar-checkbox' );
var button = document.getElementById( 'mw-sidebar-button' );
if ( checkbox instanceof HTMLInputElement && button ) {
checkboxHack.bindToggleOnClick( checkbox, button );
checkboxHack.bindUpdateAriaExpandedOnInput( checkbox );
button.addEventListener( 'click', updateMenuIcon.bind( undefined, checkbox, button ) );
checkbox.addEventListener( 'input', updateMenuIcon.bind( undefined, checkbox, button ) );
checkboxHack.updateAriaExpanded( checkbox );
updateMenuIcon( checkbox, button );
}
}
/**
* @param {Window} window
* @return {void}
*/
function main( window ) {
collapsibleTabs.init();
$( vector.init );
initSidebar( window.document );
}
main( window );

View file

@ -1,5 +1,6 @@
@import '../../variables.less'; @import '../../variables.less';
@import 'mediawiki.mixins.less'; @import 'mediawiki.mixins.less';
@import './layout.less';
.mw-logo { .mw-logo {
.flex-display(); .flex-display();
@ -8,6 +9,8 @@
height: 100%; height: 100%;
// Center vertically. // Center vertically.
align-items: center; align-items: center;
// Make room for the sidebar menu button.
margin-left: @size-sidebar-button;
} }
.mw-logo-icon { .mw-logo-icon {

View file

@ -1,5 +1,8 @@
@import '../../variables.less'; @import '../../variables.less';
@import 'mediawiki.mixins.less';
@import './layout.less';
@import 'legacy/Sidebar.less'; @import 'legacy/Sidebar.less';
@import 'checkboxHack.less';
.mw-sidebar-action { .mw-sidebar-action {
// Align with the portal heading/links // Align with the portal heading/links
@ -11,3 +14,67 @@
font-size: @font-size-portal-list-item; font-size: @font-size-portal-list-item;
font-weight: bold; font-weight: bold;
} }
// FIXME please add a class, .mw-navigation, and use that instead of this identifier.
#mw-navigation {
.mw-checkbox-hack-checkbox,
.mw-checkbox-hack-button {
// The icon is only 44px tall but the header is 50px. Offset by the difference from the logo
// icon and center with respect to the header.
top: @height-logo-icon - @size-sidebar-button + ( @height-header - @height-logo-icon ) / 2;
// Some made up value to be revised by Alex.
left: 10px;
}
.mw-checkbox-hack-button {
position: absolute;
z-index: @z-index-sidebar-button;
// Override minimum dimensions set by mw-ui-icon.mw-ui-icon-element.
min-width: @size-sidebar-button;
min-height: @size-sidebar-button;
width: @size-sidebar-button;
height: @size-sidebar-button;
border: 1px solid transparent;
border-radius: @border-radius-base;
&:before {
// Center it.
margin: 12px;
// FIXME: the icon itself is supposed to be 20px. mediawiki.ui uses 24px.
// As soon as mediawiki.ui is standardized, remove this override. See T191021.
min-height: 20px;
opacity: 0.87;
}
&:hover {
background-color: @background-color-frameless--hover;
}
.transition( background-color @transition-duration-base, border-color @transition-duration-base, box-shadow @transition-duration-base; );
}
.mw-checkbox-hack-checkbox:focus ~ .mw-checkbox-hack-button {
// Next two rules from OOUI, frameless, icon-only button widget.
border-color: @color-primary;
.box-shadow( inset 0 0 0 1px @color-primary );
}
// Use the MediaWiki checkbox hack class from checkboxHack.less. This class exists on the
// checkbox input for the menu panel.
.mw-checkbox-hack-checkbox:not( :checked ) ~ .mw-sidebar {
// Turn off presentation so that screen readers get the same effect as visually hiding.
// Visibility and opacity can be animated. If animation is unnecessary, use `display: none`
// instead to avoid hidden rendering.
visibility: hidden;
opacity: 0;
.transform( translateX( -100% ) );
}
}
.mw-sidebar {
// Enable animations on desktop width only.
@media ( min-width: @width-breakpoint-desktop ) {
@timing: @transition-duration-base ease-out;
.transition( transform @timing, opacity @timing, visibility @timing; );
}
}

View file

@ -0,0 +1,48 @@
// This file is being considered for Core as part of T252774.
// Notes:
//
// - Usage requires three elements: a hidden checkbox input, a button, and a show / hide target.
// - By convention, the checked state is considered expanded or visible. Unchecked is considered
// hidden.
// - Please see additional documentation in checkboxHack.js for example HTML and JavaScript
// integration.
//
// Example supplemental styles (to be added on a per use case basis):
//
// - Animate target in and out from start (left in LTR) to end (right in LTR):
//
// .mw-checkbox-hack-checkbox:not( :checked ) ~ .mw-checkbox-hack-target {
// // Turn off presentation so that screen readers get the same effect as visually
// // hiding. Visibility and opacity can be animated. If animation is unnecessary, all
// // of this can be replaced with `display: none` instead to avoid hidden rendering.
// visibility: hidden;
// opacity: 0;
// @timing: @transition-duration-base ease-in-out;
// .transition( transform @timing, opacity @timing, visibility @timing; );
// .transform( translateX( -100% ) );
// }
//
// - Show / hide the target instantly without animation:
//
// .mw-checkbox-hack-checkbox:not( :checked ) ~ .mw-checkbox-hack-target {
// display: none;
// }
@import 'mediawiki.ui/variables.less';
@import 'mediawiki.mixins.less';
.mw-checkbox-hack-checkbox {
position: absolute;
// Always lower the checkbox behind the foreground content.
z-index: -1;
// The checkbox `display` cannot be `none` since its focus state is used for other selectors.
opacity: 0;
}
.mw-checkbox-hack-button {
// Labels are inlined by default but are also an icon having width and height specified.
display: inline-block;
// Use the hand icon for the toggle button which is actually a checkbox label.
cursor: pointer;
}

View file

@ -58,6 +58,15 @@
], ],
"styles": [ "resources/skins.vector.styles/index.less" ] "styles": [ "resources/skins.vector.styles/index.less" ]
}, },
"skins.vector.icons": {
"class": "ResourceLoaderImageModule",
"selector": ".mw-ui-icon-wikimedia-{name}-base20:before",
"defaultColor": "#54595d",
"images": {
"menu": "resources/skins.vector.icons/menu.svg",
"collapseHorizontal": "resources/skins.vector.icons/collapseHorizontal.svg"
}
},
"skins.vector.styles.responsive": { "skins.vector.styles.responsive": {
"targets": [ "targets": [
"desktop", "desktop",
@ -72,7 +81,8 @@
"resources/skins.vector.legacy.js/vector.js" "resources/skins.vector.legacy.js/vector.js"
], ],
"dependencies": [ "dependencies": [
"mediawiki.util" "mediawiki.util",
"mediawiki.page.ready"
] ]
}, },
"skins.vector.legacy.js": { "skins.vector.legacy.js": {

View file

@ -43,6 +43,10 @@
@color-link-new: #a55858; @color-link-new: #a55858;
@color-link-selected: @color-base; @color-link-selected: @color-base;
// See oojs/ui/src/themes/wikimediaui/common.less.
@background-color-frameless--hover: rgba( 0, 24, 73, 7/255 ); // equivalent to @wmui-color-base90 on white
@color-primary: #36c; // wikimedia-ui-base.less
@font-size-base: unit( 14 / @font-size-browser, em ); // Equals `0.875em`. @font-size-base: unit( 14 / @font-size-browser, em ); // Equals `0.875em`.
@font-size-reset: @font-size-root; @font-size-reset: @font-size-root;
@font-size-heading-1: 1.8em; @font-size-heading-1: 1.8em;
@ -125,8 +129,14 @@
// @z-index-ui-slider-handle: 2; // @z-index-ui-slider-handle: 2;
// Display on top of page tabs (T39158, T50078). // Display on top of page tabs (T39158, T50078).
@z-index-personal: 100; @z-index-personal: 100;
@z-index-sidebar-button: 101;
// See skinStyles/jquery.ui/jquery.ui.selectable.css. // See skinStyles/jquery.ui/jquery.ui.selectable.css.
// @z-index-ui-selectable-helper: 100; // @z-index-ui-selectable-helper: 100;
@z-index-overlay: 101; @z-index-overlay: 101;
// See skinStyles/jquery.ui/jquery.ui.tooltip.css. // See skinStyles/jquery.ui/jquery.ui.tooltip.css.
// @z-index-ui-tooltip: 9999; // @z-index-ui-tooltip: 9999;
// Transitions
@transition-duration-base: 100ms;
@size-sidebar-button: 2.75em; // 44px