A <howto-toggle-button>
represents a boolean option in a form. The most common type
of toggle button is a dual-type which allows the user to toggle between two
choices -- pressed and not pressed.
The element attempts to self apply the attributes role="button"
and
tabindex="0"
when it is first created. The role
attribute helps assistive
technology like a screen reader tell the user what kind of control this is.
The tabindex
attribute opts the element into the tab order, making it keyboard
focusable and operable. To learn more about these two topics, check out
What can ARIA do? and Using tabindex.
When the toggle button is pressed, it adds a pressed
boolean attribute, and sets
a corresponding pressed
property to true
. In addition, the element sets an
aria-pressed
attribute to either "true"
or "false"
, depending on its
state. Clicking on the toggle button with a mouse, space bar or enter key, toggles these
pressed states.
The toggle button also supports a disabled
state. If either the disabled
property
is set to true or the disabled
attribute is applied, the toggle button sets
aria-disabled="true"
and removes the tabindex
attribute.
<style>
howto-toggle-button {
background-color: #eee;
padding: 3px;
cursor: default;
user-select: none;
border: 1px solid #333;
border-radius: 3px;
transition: background-color .2s ease;
}
howto-toggle-button[pressed],
howto-toggle-button:not([disabled]):active {
background-color: #999;
}
howto-toggle-button[disabled] {
opacity: 0.35;
}
</style>
<howto-toggle-button>Press me</howto-toggle-button>
(function() {
const KEYCODE = {
SPACE: 32,
ENTER: 13,
};
const template = document.createElement('template');
template.innerHTML = `
<style>
:host {
display: inline-block;
}
:host([hidden]) {
display: none;
}
</style>
<slot></slot>
`;
class HowToToggleButton extends HTMLElement {
static get observedAttributes() {
return ['pressed', 'disabled'];
}
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
if (!this.hasAttribute('role'))
this.setAttribute('role', 'button');
if (!this.hasAttribute('tabindex'))
this.setAttribute('tabindex', 0);
if (!this.hasAttribute('aria-pressed'))
this.setAttribute('aria-pressed', 'false');
this._upgradeProperty('pressed');
this._upgradeProperty('disabled');
this.addEventListener('keydown', this._onKeyDown);
this.addEventListener('click', this._onClick);
}
_upgradeProperty(prop) {
if (this.hasOwnProperty(prop)) {
let value = this[prop];
delete this[prop];
this[prop] = value;
}
}
disconnectedCallback() {
this.removeEventListener('keydown', this._onKeyDown);
this.removeEventListener('click', this._onClick);
}
set pressed(value) {
const isPressed = Boolean(value);
if (isPressed)
this.setAttribute('pressed', '');
else
this.removeAttribute('pressed');
}
get pressed() {
return this.hasAttribute('pressed');
}
set disabled(value) {
const isDisabled = Boolean(value);
if (isDisabled)
this.setAttribute('disabled', '');
else
this.removeAttribute('disabled');
}
get disabled() {
return this.hasAttribute('disabled');
}
attributeChangedCallback(name, oldValue, newValue) {
const hasValue = newValue !== null;
switch (name) {
case 'pressed':
this.setAttribute('aria-pressed', hasValue);
break;
case 'disabled':
this.setAttribute('aria-disabled', hasValue);
if (hasValue) {
this.removeAttribute('tabindex');
this.blur();
} else {
this.setAttribute('tabindex', '0');
}
break;
}
}
_onKeyDown(event) {
if (event.altKey)
return;
switch (event.keyCode) {
case KEYCODE.SPACE:
case KEYCODE.ENTER:
event.preventDefault();
this._togglePressed();
break;
default:
return;
}
}
_onClick(event) {
this._togglePressed();
}
_togglePressed() {
if (this.disabled)
return;
this.pressed = !this.pressed;
this.dispatchEvent(new CustomEvent('change', {
detail: {
pressed: this.pressed,
},
bubbles: true,
}));
}
}
window.customElements.define('howto-toggle-button', HowToToggleButton);
})();