A <howto-checkbox>
represents a boolean option in a form. The most common type
of checkbox is a dual-type which allows the user to toggle between two
choices -- checked and unchecked.
The element attempts to self apply the attributes role="checkbox"
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 checkbox is checked, it adds a checked
boolean attribute, and sets
a corresponding checked
property to true
. In addition, the element sets an
aria-checked
attribute to either "true"
or "false"
, depending on its
state. Clicking on the checkbox with a mouse, or space bar, toggles these
checked states.
The checkbox also supports a disabled
state. If either the disabled
property
is set to true or the disabled
attribute is applied, the checkbox sets
aria-disabled="true"
, removes the tabindex
attribute, and returns focus
to the document if the checkbox is the current activeElement
.
Warning: Just because you can build a custom element checkbox, doesn't
necessarily mean that you should. As this example shows, you will need to add
your own keyboard, labeling, and ARIA support. It's also important to note that
the native <form>
element will NOT submit values from a custom element. You
will need to wire that up yourself using AJAX or a hidden <input>
field. For
these reasons it can often be preferrable to use the built-in <input
type="checkbox">
instead.
<style>
howto-checkbox {
vertical-align: middle;
}
howto-label {
vertical-align: middle;
display: inline-block;
font-weight: bold;
font-family: sans-serif;
font-size: 20px;
margin-left: 8px;
}
</style>
<howto-checkbox id="join-checkbox"></howto-checkbox>
<howto-label for="join-checkbox">Join Newsletter</howto-label>
(function() {
const KEYCODE = {
SPACE: 32,
};
const template = document.createElement('template');
template.innerHTML = `
<style>
:host {
display: inline-block;
background: url('../images/unchecked-checkbox.svg') no-repeat;
background-size: contain;
width: 24px;
height: 24px;
}
:host([hidden]) {
display: none;
}
:host([checked]) {
background: url('../images/checked-checkbox.svg') no-repeat;
background-size: contain;
}
:host([disabled]) {
background:
url('../images/unchecked-checkbox-disabled.svg') no-repeat;
background-size: contain;
}
:host([checked][disabled]) {
background:
url('../images/checked-checkbox-disabled.svg') no-repeat;
background-size: contain;
}
</style>
`;
class HowToCheckbox extends HTMLElement {
static get observedAttributes() {
return ['checked', 'disabled'];
}
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
if (!this.hasAttribute('role'))
this.setAttribute('role', 'checkbox');
if (!this.hasAttribute('tabindex'))
this.setAttribute('tabindex', 0);
this._upgradeProperty('checked');
this._upgradeProperty('disabled');
this.addEventListener('keyup', this._onKeyUp);
this.addEventListener('click', this._onClick);
}
_upgradeProperty(prop) {
if (this.hasOwnProperty(prop)) {
let value = this[prop];
delete this[prop];
this[prop] = value;
}
}
disconnectedCallback() {
this.removeEventListener('keyup', this._onKeyUp);
this.removeEventListener('click', this._onClick);
}
set checked(value) {
const isChecked = Boolean(value);
if (isChecked)
this.setAttribute('checked', '');
else
this.removeAttribute('checked');
}
get checked() {
return this.hasAttribute('checked');
}
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 'checked':
this.setAttribute('aria-checked', hasValue);
break;
case 'disabled':
this.setAttribute('aria-disabled', hasValue);
if (hasValue) {
this.removeAttribute('tabindex');
this.blur();
} else {
this.setAttribute('tabindex', '0');
}
break;
}
}
_onKeyUp(event) {
if (event.altKey)
return;
switch (event.keyCode) {
case KEYCODE.SPACE:
event.preventDefault();
this._toggleChecked();
break;
default:
return;
}
}
_onClick(event) {
this._toggleChecked();
}
_toggleChecked() {
if (this.disabled)
return;
this.checked = !this.checked;
this.dispatchEvent(new CustomEvent('change', {
detail: {
checked: this.checked,
},
bubbles: true,
}));
}
}
window.customElements.define('howto-checkbox', HowToCheckbox);
})();