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