Expand all

mediawiki.page.ready.CheckboxHack

Utility library for managing components using the CSS checkbox hack. To access call require('mediawiki.page.ready').checkboxHack.

The checkbox hack works without JavaScript for graphical user-interface users, but relies on enhancements to work well for screen reader users. This module provides required a11y interactivity for updating the aria-expanded accessibility state, and optional enhancements for avoiding the distracting focus ring when using a pointing device, and target dismissal on focus loss or external click.

The checkbox hack is a prevalent pattern in MediaWiki similar to disclosure widgets0. Although dated and out-of-fashion, it's surprisingly flexible allowing for both details / summary-like patterns, menu components, and more complex structures (to be used sparingly) where the toggle button and target are in different parts of the Document without an enclosing element, so long as they can be described as a sibling to the input. It's complicated and frequent enough to warrant single implementation.

In time, proper disclosure widgets should replace checkbox hacks. However, the second pattern has no equivalent so the checkbox hack may have a continued use case for some time to come.

When the abstraction is leaky, the underlying implementation is simpler than anything built to hide it. Attempts to abstract the functionality for the second pattern failed so all related code celebrates the implementation as directly as possible.

All the code assumes that when the input is checked, the target is in an expanded state.

Consider the disclosure widget pattern first mentioned:

<details>                                              <!-- Container -->
    <summary>Click to expand navigation menu</summary> <!-- Button -->
    <ul>                                               <!-- Target -->
        <li>Main page</li>
        <li>Random article</li>
        <li>Donate to Wikipedia</li>
    </ul>
</details>

Which is represented verbosely by a checkbox hack as such:

<div>                                                 <!-- Container -->
    <input                                            <!-- Visually hidden checkbox -->
        type="checkbox"
        id="sidebar-checkbox"
        class="mw-checkbox-hack-checkbox"
        {{#visible}}checked{{/visible}}
        role="button"
        aria-labelledby="sidebar-button"
        aria-expanded="true||false"
        aria-haspopup="true">                         <!-- Optional attribute -->
    <label                                            <!-- Button -->
        id="sidebar-button"
        class="mw-checkbox-hack-button"
        for="sidebar-checkbox"
        aria-hidden="true">
        Click to expand navigation menu
    </label>
    <ul id="sidebar" class="mw-checkbox-hack-target"> <!-- Target -->
        <li>Main page</li>
        <li>Random article</li>
        <li>Donate to Wikipedia</li>
    </ul>
</div>

Where the checkbox is the input, the label is the button, and the target is the unordered list. aria-haspopup is an optional attribute that can be applied when dealing with popup elements (i.e. menus).

Note that while the label acts as a button for visual users (i.e. it's usually styled as a button and is clicked), the checkbox is what's actually interacted with for keyboard and screenreader users. Many of the HTML attributes and JS enhancements serve to give the checkbox the behavior and semantics of a button. For this reason any hover/focus/active state styles for the button should be applied based on the checkbox state (i.e. https://github.com/wikimedia/mediawiki/blob/master/resources/src/mediawiki.ui.button/button.less#L90)

Consider the disparate pattern:

<!-- ... -->
<!-- The only requirement is that the button and target can be described as a sibling to the
     checkbox. -->
<input
    type="checkbox"
    id="sidebar-checkbox"
    class="mw-checkbox-hack-checkbox"
    {{#visible}}checked{{/visible}}
    role="button"
    aria-labelledby="sidebar-button"
    aria-expanded="true||false"
    aria-haspopup="true">
<!-- ... -->
<label
    id="sidebar-button"
    class="mw-checkbox-hack-button"
    for="sidebar-checkbox"
    aria-hidden="true">
    Toggle navigation menu
</label>
<!-- ... -->
<ul id="sidebar" class="mw-checkbox-hack-target">
    <li>Main page</li>
    <li>Random article</li>
    <li>Donate to Wikipedia</li>
</ul>
<!-- ... -->

Which is the same as the disclosure widget but without the enclosing container and the input only needs to be a preceding sibling of the button and target. It's possible to bend the checkbox hack further to allow the button and target to be at an arbitrary depth so long as a parent can be described as a succeeding sibling of the input, but this requires a mixin implementation that duplicates the rules for each relation selector.

Exposed APIs should be considered stable.

Accompanying checkbox hack styles are tracked in T252774.

Methods

bind(window, checkbox, button, target) → {function}static #

Dismiss the target when clicking or focusing elsewhere and update the aria-expanded attribute based on checkbox state (target visibility) changes made by the user. When tapping the button itself, clear the focus outline.

This function calls the other bind* functions and is the only expected interaction for most use cases. It's constituents are provided distinctly for the other use cases.

Parameters:

Name Type Description
window window
checkbox HTMLInputElement

The underlying hidden checkbox that controls target visibility.

button HTMLElement

The visible label icon associated with the checkbox. This button toggles the state of the underlying checkbox.

target Node

The Node to toggle visibility of based on checkbox state.

Source:

Returns:

Cleanup function that removes the added event listeners.

Type
function

Dismiss the target when clicking or focusing elsewhere and update the aria-expanded attribute based on checkbox state (target visibility) changes made by the user.

Dismiss the target when clicking on a link to prevent the target from being open when navigating to a new page.

bindDismissOnClickOutside(window, checkbox, button, target) → {function}static #

Dismiss the target when clicking elsewhere and update the aria-expanded attribute based on checkbox state (target visibility).

Parameters:

Name Type Description
window window
checkbox HTMLInputElement
button HTMLElement
target Node
Source:

Returns:

Cleanup function that removes the added event listeners.

Type
function

Dismiss the target when clicking elsewhere and update the aria-expanded attribute based on checkbox state (target visibility).

bindDismissOnFocusLoss(window, checkbox, button, target) → {function}static #

Dismiss the target when focusing elsewhere and update the aria-expanded attribute based on checkbox state (target visibility).

Parameters:

Name Type Description
window window
checkbox HTMLInputElement
button HTMLElement
target Node
Source:

Returns:

Cleanup function that removes the added event listeners.

Type
function

Dismiss the target when focusing elsewhere and update the aria-expanded attribute based on checkbox state (target visibility).

bindToggleOnClick(checkbox, button) → {function}static #

Manually change the checkbox state to avoid a focus change when using a pointing device.

Parameters:

Name Type Description
checkbox HTMLInputElement
button HTMLElement
Source:

Returns:

Cleanup function that removes the added event listeners.

Type
function
Manually change the checkbox state to avoid a focus change when using a pointing device.

bindToggleOnEnter(checkbox) → {function}static #

Manually change the checkbox state when the button is focused and Enter is pressed.

Parameters:

Name Type Description
checkbox HTMLInputElement
Source:

Returns:

Cleanup function that removes the added event listeners.

Type
function
Manually change the checkbox state when the button is focused and Enter is pressed.

bindToggleOnSpaceEnter(checkbox, button) → {function}static #

Manually change the checkbox state when the button is focused and SPACE is pressed.

Parameters:

Name Type Description
checkbox HTMLInputElement
button HTMLElement
Deprecated:
  • Use `bindToggleOnEnter` instead.
Source:

Returns:

Cleanup function that removes the added event listeners.

Type
function
Manually change the checkbox state when the button is focused and SPACE is pressed.

bindUpdateAriaExpandedOnInput(checkbox, button) → {function}static #

Update the aria-expanded attribute based on checkbox state (target visibility) changes.

Parameters:

Name Type Description
checkbox HTMLInputElement
button HTMLElement
Source:

Returns:

Cleanup function that removes the added event listeners.

Type
function
Update the aria-expanded attribute based on checkbox state (target visibility) changes.

updateAriaExpanded(checkbox, button)static #

Revise the button's aria-expanded state to match the checked state.

Parameters:

Name Type Description
checkbox HTMLInputElement
button HTMLElement
Source:
Revise the button's aria-expanded state to match the checked state.