diff --git a/resources/skins.vector.js/dropdownMenus.js b/resources/skins.vector.js/dropdownMenus.js index dc2df6a..9363f15 100644 --- a/resources/skins.vector.js/dropdownMenus.js +++ b/resources/skins.vector.js/dropdownMenus.js @@ -4,18 +4,14 @@ var checkboxHack = /** @type {CheckboxHack} */ require( /** @type {string} */( 'mediawiki.page.ready' ) ).checkboxHack, CHECKBOX_HACK_CONTAINER_SELECTOR = '.vector-menu-dropdown', CHECKBOX_HACK_CHECKBOX_SELECTOR = '.vector-menu-checkbox', - // In core's checkboxHack.js, it is recommended to use a label element as a - // button that toggles the checkbox. In Vector's dropdown menus that use the - // Menu.mustache template, a checkbox is used as both the "button" and the - // "checkbox". - CHECKBOX_HACK_BUTTON_SELECTOR = '.vector-menu-checkbox', + CHECKBOX_HACK_BUTTON_SELECTOR = '.vector-menu-heading', CHECKBOX_HACK_TARGET_SELECTOR = '.vector-menu-content'; /** * Add the ability for users to toggle dropdown menus using the enter key (as * well as space) using core's checkboxHack. */ -function bindToggleOnSpaceEnter() { +function bind() { // Search for all dropdown containers using the CHECKBOX_HACK_CONTAINER_SELECTOR. var containers = document.querySelectorAll( CHECKBOX_HACK_CONTAINER_SELECTOR ); @@ -29,7 +25,7 @@ function bindToggleOnSpaceEnter() { return; } - checkboxHack.bindToggleOnSpaceEnter( checkbox, button ); + checkboxHack.bind( window, checkbox, button, target ); } ); } @@ -46,20 +42,6 @@ function bindCloseOnUnload() { } ); } -/** - * Make sure that clicking outside a menu closes it. - */ -function closeDropdownsOnClickOutside() { - $( document.body ).on( 'click', function ( ev ) { - var $closestPortlet = $( ev.target ).closest( '.mw-portlet' ); - // Uncheck (close) any menus that are open. - // eslint-disable-next-line no-jquery/no-global-selector - $( '.vector-menu-checkbox:checked' ).not( - $closestPortlet.find( '.vector-menu-checkbox' ) - ).prop( 'checked', false ); - } ); -} - /** * Adds icon placeholder for gadgets to use. * @@ -110,7 +92,6 @@ Array.prototype.forEach.call( mw.hook( 'util.addPortletLink' ).add( addPortletLinkHandler ); module.exports = function dropdownMenus() { - closeDropdownsOnClickOutside(); - bindToggleOnSpaceEnter(); + bind(); bindCloseOnUnload(); }; diff --git a/resources/skins.vector.js/sidebar.js b/resources/skins.vector.js/sidebar.js index e9a4a1b..b014a9c 100644 --- a/resources/skins.vector.js/sidebar.js +++ b/resources/skins.vector.js/sidebar.js @@ -15,15 +15,91 @@ /** @interface CheckboxHack */ /** @interface MwApi */ -/** @type {CheckboxHack} */ var checkboxHack = -require( /** @type {string} */( 'mediawiki.page.ready' ) ).checkboxHack; -var SIDEBAR_BUTTON_ID = 'mw-sidebar-button', +var checkboxHack = /** @type {CheckboxHack} */ require( /** @type {string} */( 'mediawiki.page.ready' ) ).checkboxHack, + SIDEBAR_BUTTON_ID = 'mw-sidebar-button', SIDEBAR_CHECKBOX_ID = 'mw-sidebar-checkbox', SIDEBAR_PREFERENCE_NAME = 'VectorSidebarVisible'; var debounce = require( /** @type {string} */ ( 'mediawiki.util' ) ).debounce; /** @type {MwApi} */ var api; +/** + * Revise the button's `aria-expanded` state to match the checked state. + * + * @param {HTMLInputElement} checkbox + * @param {HTMLElement} button + * @return {void} + * @ignore + */ +function updateAriaExpanded( checkbox, button ) { + button.setAttribute( 'aria-expanded', checkbox.checked.toString() ); +} + +/** + * Update the `aria-expanded` attribute based on checkbox state (target visibility) changes. + * + * @param {HTMLInputElement} checkbox + * @param {HTMLElement} button + * @return {function(): void} Cleanup function that removes the added event listeners. + * @ignore + */ +function bindUpdateAriaExpandedOnInput( checkbox, button ) { + var listener = updateAriaExpanded.bind( undefined, checkbox, button ); + // Whenever the checkbox state changes, update the `aria-expanded` state. + checkbox.addEventListener( 'input', listener ); + + return function () { + checkbox.removeEventListener( 'input', listener ); + }; +} + +/** + * Manually change the checkbox state when the button is focused and SPACE is pressed. + * + * @param {HTMLElement} button + * @return {function(): void} Cleanup function that removes the added event listeners. + * @ignore + */ +function bindToggleOnSpaceEnter( button ) { + function isEnterOrSpace( /** @type {KeyboardEvent} */ event ) { + return event.key === ' ' || event.key === 'Enter'; + } + + function onKeydown( /** @type {KeyboardEvent} */ event ) { + // Only handle SPACE and ENTER. + if ( !isEnterOrSpace( event ) ) { + return; + } + // Prevent the browser from scrolling when pressing space. The browser will + // try to do this unless the "button" element is a button or a checkbox. + // Depending on the actual "button" element, this also possibly prevents a + // native click event from being triggered so we programatically trigger a + // click event in the keyup handler. + event.preventDefault(); + } + + function onKeyup( /** @type {KeyboardEvent} */ event ) { + // Only handle SPACE and ENTER. + if ( !isEnterOrSpace( event ) ) { + return; + } + + // A native button element triggers a click event when the space or enter + // keys are pressed. Since the passed in "button" may or may not be a + // button, programmatically trigger a click event to make it act like a + // button. + button.click(); + } + + button.addEventListener( 'keydown', onKeydown ); + button.addEventListener( 'keyup', onKeyup ); + + return function () { + button.removeEventListener( 'keydown', onKeydown ); + button.removeEventListener( 'keyup', onKeyup ); + }; +} + /** * 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. @@ -35,9 +111,9 @@ var debounce = require( /** @type {string} */ ( 'mediawiki.util' ) ).debounce; function initCheckboxHack( checkbox, button ) { if ( checkbox instanceof HTMLInputElement && button ) { checkboxHack.bindToggleOnClick( checkbox, button ); - checkboxHack.bindUpdateAriaExpandedOnInput( checkbox, button ); - checkboxHack.updateAriaExpanded( checkbox, button ); - checkboxHack.bindToggleOnSpaceEnter( checkbox, button ); + bindUpdateAriaExpandedOnInput( checkbox, button ); + updateAriaExpanded( checkbox, button ); + bindToggleOnSpaceEnter( button ); } }