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. |
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.
bindDismissOnClickLink(checkbox, target) → {function}static
#
Dismiss the target when clicking on a link to prevent the target from being open when navigating to a new page.
Parameters:
Name | Type | Description |
---|---|---|
checkbox |
HTMLInputElement | |
target |
Node |
Returns:
Cleanup function that removes the added event listeners.
- Type
- function
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 |
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 |
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 |
Returns:
Cleanup function that removes the added event listeners.
- Type
- function
bindToggleOnEnter(checkbox) → {function}static
#
Manually change the checkbox state when the button is focused and Enter is pressed.
Parameters:
Name | Type | Description |
---|---|---|
checkbox |
HTMLInputElement |
Returns:
Cleanup function that removes the added event listeners.
- Type
- function
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
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 |
Returns:
Cleanup function that removes the added event listeners.
- Type
- function
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 |
aria-expanded
state to match the checked state.