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);
})();