From 5195f5fd67181cc48d9af5a6a2354e0401e76d89 Mon Sep 17 00:00:00 2001 From: Stephen Niedzielski Date: Mon, 30 Mar 2020 14:07:35 -0600 Subject: [PATCH] [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 --- i18n/en.json | 1 + i18n/qqq.json | 1 + includes/SkinVector.php | 16 +++-- includes/VectorTemplate.php | 3 + includes/templates/Sidebar.mustache | 26 ++++++- jsdoc.json | 15 +++++ resources/CheckboxHack.d.ts | 16 +++++ .../skins.vector.icons/collapseHorizontal.svg | 7 ++ resources/skins.vector.icons/menu.svg | 1 + resources/skins.vector.js/index.js | 60 +++++++++++++++-- resources/skins.vector.styles/Logo.less | 3 + resources/skins.vector.styles/Sidebar.less | 67 +++++++++++++++++++ .../skins.vector.styles/checkboxHack.less | 48 +++++++++++++ skin.json | 12 +++- variables.less | 10 +++ 15 files changed, 272 insertions(+), 14 deletions(-) create mode 100644 resources/CheckboxHack.d.ts create mode 100644 resources/skins.vector.icons/collapseHorizontal.svg create mode 100644 resources/skins.vector.icons/menu.svg create mode 100644 resources/skins.vector.styles/checkboxHack.less diff --git a/i18n/en.json b/i18n/en.json index 234414c..4e39e67 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -10,6 +10,7 @@ "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.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-delete": "Delete", "vector-action-move": "Move", diff --git a/i18n/qqq.json b/i18n/qqq.json index cebe737..b350642 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -21,6 +21,7 @@ "vector-opt-out-tooltip": "Used as the tooltip for the Vector opt-out link", "vector.css": "{{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-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}}", diff --git a/includes/SkinVector.php b/includes/SkinVector.php index 84880ac..d9eb3cf 100644 --- a/includes/SkinVector.php +++ b/includes/SkinVector.php @@ -80,11 +80,17 @@ class SkinVector extends SkinTemplate { */ public function getDefaultModules() { $modules = parent::getDefaultModules(); - // add vector skin styles and vector module - $module = $this->isLegacy() - ? 'skins.vector.styles.legacy' : 'skins.vector.styles'; - $modules['styles']['skin'][] = $module; - $modules['core'][] = $this->isLegacy() ? 'skins.vector.legacy.js' : 'skins.vector.js'; + + if ( $this->isLegacy() ) { + $modules['styles']['skin'][] = 'skins.vector.styles.legacy'; + $modules[Constants::SKIN_NAME] = 'skins.vector.legacy.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; } diff --git a/includes/VectorTemplate.php b/includes/VectorTemplate.php index a7dd37e..dc16a65 100644 --- a/includes/VectorTemplate.php +++ b/includes/VectorTemplate.php @@ -371,6 +371,9 @@ class VectorTemplate extends BaseTemplate { ), 'array-portals-rest' => array_slice( $props, 1 ), '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 ]; } diff --git a/includes/templates/Sidebar.mustache b/includes/templates/Sidebar.mustache index dab6422..5dc965d 100644 --- a/includes/templates/Sidebar.mustache +++ b/includes/templates/Sidebar.mustache @@ -7,9 +7,33 @@ MenuDefinition data-portals-first MenuDefinition[] array-portals-rest 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. }} -
+ + +
{{#data-portals-first}}{{>Menu}}{{/data-portals-first}} {{#data-emphasized-sidebar-action}}
diff --git a/jsdoc.json b/jsdoc.json index 831551b..e7f4ab4 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -14,6 +14,21 @@ "cleverLinks": true, "default": { "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": "#" + } } } } diff --git a/resources/CheckboxHack.d.ts b/resources/CheckboxHack.d.ts new file mode 100644 index 0000000..d2fa76e --- /dev/null +++ b/resources/CheckboxHack.d.ts @@ -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; +} diff --git a/resources/skins.vector.icons/collapseHorizontal.svg b/resources/skins.vector.icons/collapseHorizontal.svg new file mode 100644 index 0000000..85f7ec7 --- /dev/null +++ b/resources/skins.vector.icons/collapseHorizontal.svg @@ -0,0 +1,7 @@ + + + + collapse + + + \ No newline at end of file diff --git a/resources/skins.vector.icons/menu.svg b/resources/skins.vector.icons/menu.svg new file mode 100644 index 0000000..f1c381c --- /dev/null +++ b/resources/skins.vector.icons/menu.svg @@ -0,0 +1 @@ +menu \ No newline at end of file diff --git a/resources/skins.vector.js/index.js b/resources/skins.vector.js/index.js index 1bdf069..459b13b 100644 --- a/resources/skins.vector.js/index.js +++ b/resources/skins.vector.js/index.js @@ -1,10 +1,56 @@ -var - collapsibleTabs = require( '../skins.vector.legacy.js/collapsibleTabs.js' ), - vector = require( '../skins.vector.legacy.js/vector.js' ); +/** @type {CheckboxHack} */ var checkboxHack = + require( /** @type {string} */ ( 'mediawiki.page.ready' ) ).checkboxHack; +var collapsibleTabs = require( '../skins.vector.legacy.js/collapsibleTabs.js' ); +var vector = require( '../skins.vector.legacy.js/vector.js' ); -function main() { - collapsibleTabs.init(); - $( vector.init ); +/** + * Update the state of the menu icon to be an expanded or collapsed icon. + * @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 ); diff --git a/resources/skins.vector.styles/Logo.less b/resources/skins.vector.styles/Logo.less index 297efaa..6d35b38 100644 --- a/resources/skins.vector.styles/Logo.less +++ b/resources/skins.vector.styles/Logo.less @@ -1,5 +1,6 @@ @import '../../variables.less'; @import 'mediawiki.mixins.less'; +@import './layout.less'; .mw-logo { .flex-display(); @@ -8,6 +9,8 @@ height: 100%; // Center vertically. align-items: center; + // Make room for the sidebar menu button. + margin-left: @size-sidebar-button; } .mw-logo-icon { diff --git a/resources/skins.vector.styles/Sidebar.less b/resources/skins.vector.styles/Sidebar.less index d01d8e1..f751657 100644 --- a/resources/skins.vector.styles/Sidebar.less +++ b/resources/skins.vector.styles/Sidebar.less @@ -1,5 +1,8 @@ @import '../../variables.less'; +@import 'mediawiki.mixins.less'; +@import './layout.less'; @import 'legacy/Sidebar.less'; +@import 'checkboxHack.less'; .mw-sidebar-action { // Align with the portal heading/links @@ -11,3 +14,67 @@ font-size: @font-size-portal-list-item; 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; ); + } +} diff --git a/resources/skins.vector.styles/checkboxHack.less b/resources/skins.vector.styles/checkboxHack.less new file mode 100644 index 0000000..8ed8a0f --- /dev/null +++ b/resources/skins.vector.styles/checkboxHack.less @@ -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; +} diff --git a/skin.json b/skin.json index 6823047..8c68db7 100644 --- a/skin.json +++ b/skin.json @@ -58,6 +58,15 @@ ], "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": { "targets": [ "desktop", @@ -72,7 +81,8 @@ "resources/skins.vector.legacy.js/vector.js" ], "dependencies": [ - "mediawiki.util" + "mediawiki.util", + "mediawiki.page.ready" ] }, "skins.vector.legacy.js": { diff --git a/variables.less b/variables.less index 210b6af..5b82fa5 100644 --- a/variables.less +++ b/variables.less @@ -43,6 +43,10 @@ @color-link-new: #a55858; @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-reset: @font-size-root; @font-size-heading-1: 1.8em; @@ -125,8 +129,14 @@ // @z-index-ui-slider-handle: 2; // Display on top of page tabs (T39158, T50078). @z-index-personal: 100; +@z-index-sidebar-button: 101; // See skinStyles/jquery.ui/jquery.ui.selectable.css. // @z-index-ui-selectable-helper: 100; @z-index-overlay: 101; // See skinStyles/jquery.ui/jquery.ui.tooltip.css. // @z-index-ui-tooltip: 9999; + +// Transitions +@transition-duration-base: 100ms; + +@size-sidebar-button: 2.75em; // 44px