coral-spectrum/coral-component-checkbox/src/scripts/Checkbox.js
/**
* Copyright 2019 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import {BaseComponent} from '../../../coral-base-component';
import {BaseFormField} from '../../../coral-base-formfield';
import {Icon} from '../../../coral-component-icon';
import base from '../templates/base';
import {transform, commons, i18n} from '../../../coral-utils';
import {Decorator} from '../../../coral-decorator';
const IS_IE_OR_EDGE = navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0 ||
window.navigator.userAgent.indexOf('Edge') !== -1;
const CLASSNAME = '_coral-Checkbox';
/**
@class Coral.Checkbox
@classdesc A Checkbox component to be used as a form field.
@htmltag coral-checkbox
@extends {HTMLElement}
@extends {BaseComponent}
@extends {BaseFormField}
*/
const Checkbox = Decorator(class extends BaseFormField(BaseComponent(HTMLElement)) {
/** @ignore */
constructor() {
super();
// @polyfill ie
this._delegateEvents(commons.extend(this._events, {
click: '_onClick',
mousedown: '_onMouseDown'
}));
// Prepare templates
this._elements = {
// Try to find the label content zone or create one
label: this.querySelector('coral-checkbox-label') || document.createElement('coral-checkbox-label')
};
base.call(this._elements, {commons, i18n, Icon});
// Pre-define labellable element
this._labellableElement = this._elements.input;
// Check if the label is empty whenever we get a mutation
this._observer = new MutationObserver(this._hideLabelIfEmpty.bind(this));
// Watch for changes to the label element's children
this._observer.observe(this._elements.labelWrapper, {
// Catch changes to childList
childList: true,
// Catch changes to textContent
characterData: true,
// Monitor any child node
subtree: true
});
}
/**
Checked state for the checkbox.
@type {Boolean}
@default false
@htmlattribute checked
@htmlattributereflected
@emits {change}
*/
get checked() {
return this._checked || false;
}
set checked(value) {
this._checked = transform.booleanAttr(value);
this._reflectAttribute('checked', this._checked);
this._elements.input.checked = this._checked;
}
/**
Indicates that the checkbox is neither on nor off.
@type {Boolean}
@default false
@htmlattribute indeterminate
@htmlattributereflected
*/
get indeterminate() {
return this._indeterminate || false;
}
set indeterminate(value) {
this._indeterminate = transform.booleanAttr(value);
this._reflectAttribute('indeterminate', this._indeterminate);
this.classList.toggle('is-indeterminate', this._indeterminate);
this._elements.input.indeterminate = this._indeterminate;
this._elements.input[this._indeterminate ? 'setAttribute' : 'removeAttribute']('aria-checked', 'mixed');
}
/**
The checkbox's label element.
@type {CheckboxLabel}
@contentzone
*/
get label() {
return this._getContentZone(this._elements.label);
}
set label(value) {
this._setContentZone('label', value, {
handle: 'label',
tagName: 'coral-checkbox-label',
insert: function (label) {
this._elements.labelWrapper.appendChild(label);
}
});
}
/**
Name used to submit the data in a form.
@type {String}
@default ""
@htmlattribute name
@htmlattributereflected
*/
get name() {
return this._elements.input.name;
}
set name(value) {
this._reflectAttribute('name', value);
this._elements.input.name = value;
}
/**
The value that will be submitted when the checkbox is checked. Changing this value will not trigger an event.
@type {String}
@default "on"
@htmlattribute value
*/
get value() {
return this._elements.input.value || 'on';
}
set value(value) {
this._elements.input.value = value;
}
/**
Whether this field is disabled or not.
@type {Boolean}
@default false
@htmlattribute disabled
@htmlattributereflected
*/
get disabled() {
return this._disabled || false;
}
set disabled(value) {
this._disabled = transform.booleanAttr(value);
this._reflectAttribute('disabled', this._disabled);
this[this._disabled ? 'setAttribute' : 'removeAttribute']('aria-disabled', this._disabled);
this.classList.toggle('is-disabled', this._disabled);
this._elements.input.disabled = this._disabled;
}
/**
Whether this field is required or not.
@type {Boolean}
@default false
@htmlattribute required
@htmlattributereflected
*/
get required() {
return this._required || false;
}
set required(value) {
this._required = transform.booleanAttr(value);
this._reflectAttribute('required', this._required);
this._elements.input.required = this._required;
}
/**
Whether this field is readOnly or not. Indicating that the user cannot modify the value of the control.
@type {Boolean}
@default false
@htmlattribute readonly
@htmlattributereflected
*/
get readOnly() {
return this._readOnly || false;
}
set readOnly(value) {
this._readOnly = transform.booleanAttr(value);
this._reflectAttribute('readonly', this._readOnly);
this.classList.toggle('is-readOnly', this._readOnly);
this._elements.input.tabIndex = this._readOnly ? -1 : 0;
}
/**
Inherited from {@link BaseFormField#labelled}.
*/
get labelled() {
return super.labelled;
}
set labelled(value) {
super.labelled = value;
this._hideLabelIfEmpty();
}
/**
Inherited from {@link BaseFormField#labelledBy}.
*/
get labelledBy() {
return super.labelledBy;
}
set labelledBy(value) {
super.labelledBy = value;
this._hideLabelIfEmpty();
}
/**
Inherited from {@link BaseComponent#trackingElement}.
*/
get trackingElement() {
// it uses the name as the first fallback since it is not localized, otherwise it uses the label
return typeof this._trackingElement === 'undefined' ?
// keep spaces to only 1 max and trim. this mimics native html behaviors
(this.name ? `${this.name}=${this.value}` : '') || (this.label || this).textContent.replace(/\s{2,}/g, ' ').trim() :
this._trackingElement;
}
set trackingElement(value) {
super.trackingElement = value;
}
/*
Indicates to the formField that the 'checked' property needs to be set in this component.
@protected
*/
get _componentTargetProperty() {
return 'checked';
}
/*
Indicates to the formField that the 'checked' property has to be extracted from the event.
@protected
*/
get _eventTargetProperty() {
return 'checked';
}
/** @private */
_onInputChange(event) {
// stops the current event
event.stopPropagation();
/** @ignore */
this[this._componentTargetProperty] = event.target[this._eventTargetProperty];
// resets the indeterminate state after user interaction
this.indeterminate = false;
// Explicitly re-emit the change event after the property has been set
if (this._triggerChangeEvent) {
// @polyfill ie/edge
if (IS_IE_OR_EDGE) {
// We need 1 additional frame in case the indeterminate state is set manually on change event
window.requestAnimationFrame(() => {
this.trigger('change');
});
} else {
this.trigger('change');
}
}
}
/**
@private
@polyfill ie/edge
*/
_onClick(event) {
// Force the check/uncheck and trigger the change event since IE won't.
if (IS_IE_OR_EDGE && this.indeterminate) {
// Other browsers like Chrome and Firefox will trigger the change event and set indeterminate = false. So we
// verify if indeterminate was changed and if not, we manually check/uncheck and trigger the change event.
this.checked = !this.checked;
this._onInputChange(event);
}
// Handle the click() just like the native checkbox
else if (event.target === this) {
this.indeterminate = false;
this.checked = !this.checked;
this.trigger('change');
}
this._trackEvent(this.checked ? 'checked' : 'unchecked', 'coral-checkbox', event);
}
/**
Forces checkbox to receive focus on mousedown
@ignore
*/
_onMouseDown() {
const target = this._elements.input;
window.requestAnimationFrame(() => {
if (target !== document.activeElement) {
target.focus();
}
});
}
/**
Hide the label if it's empty
@ignore
*/
_hideLabelIfEmpty() {
const label = this._elements.label;
// If it's empty and has no non-textnode children, hide the label
const hiddenValue = !(label.children.length === 0 && label.textContent.replace(/\s*/g, '') === '');
// Toggle the screen reader text
this._elements.labelWrapper.style.margin = !hiddenValue ? '0' : '';
this._elements.screenReaderOnly.hidden = !!hiddenValue || !!this.labelledBy || !!this.labelled;
}
/**
Inherited from {@link BaseFormField#clear}.
*/
clear() {
this.checked = false;
}
/**
Inherited from {@link BaseFormField#reset}.
*/
reset() {
this.checked = this._initialCheckedState;
}
get _contentZones() {
return {'coral-checkbox-label': 'label'};
}
/** @ignore */
static get observedAttributes() {
return super.observedAttributes.concat(['indeterminate', 'checked']);
}
/** @ignore */
render() {
super.render();
this.classList.add(CLASSNAME);
// Create a fragment
const frag = document.createDocumentFragment();
const templateHandleNames = ['input', 'checkbox', 'labelWrapper'];
// Render the main template
frag.appendChild(this._elements.input);
frag.appendChild(this._elements.checkbox);
frag.appendChild(this._elements.labelWrapper);
const label = this._elements.label;
// Remove it so we can process children
if (label.parentNode) {
label.parentNode.removeChild(label);
}
while (this.firstChild) {
const child = this.firstChild;
if (child.nodeType === Node.TEXT_NODE ||
child.nodeType === Node.ELEMENT_NODE && templateHandleNames.indexOf(child.getAttribute('handle')) === -1) {
// Add non-template elements to the label
label.appendChild(child);
} else {
// Remove anything else (e.g labelWrapper)
this.removeChild(child);
}
}
// Add the frag to the component
this.appendChild(frag);
// Assign the content zones, moving them into place in the process
this.label = label;
// Cache the initial checked state of the checkbox (in order to implement reset)
this._initialCheckedState = this.checked;
// Check if we need to hide the label
// We must do this because IE does not catch mutations when nodes are not in the DOM
this._hideLabelIfEmpty();
}
});
export default Checkbox;