coral-spectrum/coral-component-switch/src/scripts/Switch.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 base from '../templates/base';
import {transform, commons, i18n} from '../../../coral-utils';
import {Decorator} from '../../../coral-decorator';
const CLASSNAME = '_coral-ToggleSwitch';
/**
@class Coral.Switch
@classdesc A Switch component is a toggle form field similar to a Checkbox component.
@htmltag coral-switch
@extends {HTMLElement}
@extends {BaseComponent}
@extends {BaseFormField}
*/
const Switch = Decorator(class extends BaseFormField(BaseComponent(HTMLElement)) {
/** @ignore */
constructor() {
super();
// Make sure the events from the FormField are attached
this._delegateEvents(commons.extend(this._events, {
'capture:focus ._coral-ToggleSwitch-input': '_onFocus',
'capture:blur ._coral-ToggleSwitch-input': '_onBlur'
}));
// Prepare templates
this._elements = {
// Try to find the label content zone
label: this.querySelector('coral-switch-label') || document.createElement('coral-switch-label')
};
base.call(this._elements, {commons, i18n});
// 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
});
}
/**
Whether the switch is on or off.
@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;
}
/**
The switch's label element.
@type {SwitchLabel}
@contentzone
*/
get label() {
return this._getContentZone(this._elements.label);
}
set label(value) {
this._setContentZone('label', value, {
handle: 'label',
tagName: 'coral-switch-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();
}
/*
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';
}
/**
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.labelled;
}
_onFocus() {
this._elements.input.classList.add('focus-ring');
}
_onBlur() {
this._elements.input.classList.remove('focus-ring');
}
get _contentZones() {
return {'coral-switch-label': 'label'};
}
/**
Inherited from {@link BaseFormField#clear}.
*/
clear() {
this.checked = false;
}
/**
Inherited from {@link BaseFormField#reset}.
*/
reset() {
this.checked = this._initialCheckedState;
}
/** @ignore */
static get observedAttributes() {
return super.observedAttributes.concat(['checked']);
}
/** @ignore */
render() {
super.render();
this.classList.add(CLASSNAME);
// Create a fragment
const frag = document.createDocumentFragment();
const templateHandleNames = ['input', 'switch', 'labelWrapper'];
// Render the template
frag.appendChild(this._elements.input);
frag.appendChild(this._elements.switch);
frag.appendChild(this._elements.labelWrapper);
const label = this._elements.label;
// Remove it so we can process children
if (label && label.parentNode) {
label.parentNode.removeChild(label);
}
// Clean up
while (this.firstChild) {
const child = this.firstChild;
// Only works if all root template elements have a handle attribute
if (child.nodeType === Node.TEXT_NODE ||
child.nodeType === Node.ELEMENT_NODE && templateHandleNames.indexOf(child.getAttribute('handle')) === -1) {
// Add non-template elements to the content
label.appendChild(child);
} else {
// Remove anything else
this.removeChild(child);
}
}
// Append the fragment 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 switch (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 Switch;