coral-spectrum/coral-base-formfield/src/scripts/BaseFormField.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 {transform, commons, validate} from '../../../coral-utils';
-
- // https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories
- let LABELLABLE_ELEMENTS_SELECTOR = 'button,input:not([type=hidden]),keygen,meter,output,progress,select,textarea';
- // @polyfill ie11
- // IE11 throws syntax error because of the "not()" in the selector for some reason in ColorInputColorProperties
- if (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0) {
- LABELLABLE_ELEMENTS_SELECTOR = 'button,keygen,meter,output,progress,select,textarea,';
-
- // Since we can't use :not() we have to indicate all input types
- [
- 'text',
- 'password',
- 'submit',
- 'reset',
- 'radio',
- 'checkbox',
- 'button',
- 'color',
- 'date',
- 'datetime-local',
- 'email',
- 'month',
- 'number',
- 'range',
- 'search',
- 'tel',
- 'time',
- 'url',
- 'week'
- ].forEach((type) => {
- LABELLABLE_ELEMENTS_SELECTOR += `input[type=${type}],`;
- });
-
- // Remove last ","
- LABELLABLE_ELEMENTS_SELECTOR = LABELLABLE_ELEMENTS_SELECTOR.slice(0, -1);
- }
- // _onInputChange is only triggered on non-hidden inputs
- const TARGET_INPUT_SELECTOR = 'input:not([type=hidden])';
-
- /**
- @base BaseFormField
- @classdesc The base element for Form Field components. If not extending a {@link HTMLInputElement}, following
- properties should be implemented at least :
- - <code>disabled</code>. Whether this field is disabled or not.
- - <code>invalid</code>. Whether the current value of this field is invalid or not.
- - <code>name</code>. Name used to submit the data in a form.
- - <code>readOnly</code>. Whether this field is readOnly or not. Indicating that the user cannot modify the value of the control.
- - <code>required</code>. Whether this field is required or not.
- - <code>value</code>. This field's current value.
- */
- class BaseFormField extends superClass {
- /** @ignore */
- constructor() {
- super();
-
- this._events = {
- 'capture:change input': '_onTargetInputChange',
- 'global:reset': '_onFormReset'
- };
- }
-
- /**
- Whether this field is disabled or not.
-
- @type {Boolean}
- @default false
- @htmlattribute disabled
- @htmlattributereflected
- @abstract
- */
-
- /**
- Whether the current value of this field is invalid or not.
-
- @type {Boolean}
- @default false
- @htmlattribute invalid
- @htmlattributereflected
- @abstract
- */
-
- /**
- Name used to submit the data in a form.
-
- @type {String}
- @default ""
- @htmlattribute name
- @htmlattributereflected
- @abstract
- */
-
- /**
- Whether this field is readOnly or not. Indicating that the user cannot modify the value of the control.
- This is ignored for checkbox, radio or fileupload.
-
- @type {Boolean}
- @default false
- @htmlattribute readonly
- @htmlattributereflected
- @abstract
- */
-
- /**
- Whether this field is required or not.
-
- @type {Boolean}
- @default false
- @htmlattribute required
- @htmlattributereflected
- @abstract
- */
-
- /**
- This field's current value.
-
- @type {String}
- @default ""
- @htmlattribute value
- @abstract
- */
-
- /**
- Whether the current value of this field is invalid or not.
-
- @type {Boolean}
- @default false
- @htmlattribute invalid
- @htmlattributereflected
- */
- get invalid() {
- return this._invalid || false;
- }
-
- set invalid(value) {
- value = transform.booleanAttr(value);
-
- this._reflectAttribute('invalid', value);
-
- if(validate.valueMustChange(this._invalid, value)) {
- this._invalid = value;
- this.setAttribute('aria-invalid', value);
- this.classList.toggle('is-invalid', value);
- }
- }
-
- /**
- Reflects the <code>aria-describedby</code> attribute to the labellable element e.g. inner input.
-
- @type {String}
- @default null
- @htmlattribute describedby
- */
- get describedBy() {
- return this._getLabellableElement().getAttribute('aria-describedby');
- }
-
- set describedBy(value) {
- value = transform.string(value);
-
- this._getLabellableElement()[value ? 'setAttribute' : 'removeAttribute']('aria-describedby', value);
- }
-
- /**
- Reflects the <code>aria-label</code> attribute to the labellable element e.g. inner input.
-
- @type {String}
- @default null
- @htmlattribute labelled
- */
- get labelled() {
- return this._getLabellableElement().getAttribute('aria-label');
- }
-
- set labelled(value) {
- value = transform.string(value);
-
- this._getLabellableElement()[value ? 'setAttribute' : 'removeAttribute']('aria-label', value);
- }
-
- /**
- Reference to a space delimited set of ids for the HTML elements that provide a label for the formField.
- Implementers should override this method to ensure that the appropriate descendant elements are labelled using the
- <code>aria-labelledby</code> attribute. This will ensure that the component is properly identified for
- accessibility purposes. It reflects the <code>aria-labelledby</code> attribute to the DOM.
- @type {?String}
- @default null
- @htmlattribute labelledby
- */
- get labelledBy() {
- return this._getLabellableElement().getAttribute('aria-labelledby');
- }
-
- set labelledBy(value) {
- value = transform.string(value);
-
- // gets the element that will get the label assigned. the _getLabellableElement method should be overriden to
- // allow other bevaviors.
- const element = this._getLabellableElement();
- // we get and assign the it that will be passed around
- const elementId = element.id = element.id || commons.getUID();
-
- const currentLabelledBy = element.getAttribute('aria-labelledby');
-
- // we clear the old label assignments
- if (currentLabelledBy && currentLabelledBy !== value) {
- this._updateForAttributes(currentLabelledBy, elementId, true);
- }
-
- if (value) {
- element.setAttribute('aria-labelledby', value);
- if (element.matches(LABELLABLE_ELEMENTS_SELECTOR)) {
- this._updateForAttributes(value, elementId);
- }
- } else {
- // since no labelledby value was set, we remove everything
- element.removeAttribute('aria-labelledby');
- }
- }
-
- /**
- Target property inside the component that will be updated when a change event is triggered.
- @type {String}
- @default "value"
- @protected
- */
- get _componentTargetProperty() {
- return 'value';
- }
-
- /**
- Target property that will be taken from <code>event.target</code> and set into
- {@link BaseFormField#_componentTargetProperty} when a change event is triggered.
- @type {String}
- @default "value"
- @protected
- */
- get _eventTargetProperty() {
- return 'value';
- }
-
- /**
- Whether the change event needs to be triggered when {@link BaseFormField#_onInputChange} is called.
- @type {Boolean}
- @default true
- @protected
- */
- get _triggerChangeEvent() {
- return true;
- }
-
- /**
- Gets the element that should get the label. In case none of the valid labelelable items are found, the component
- will be labelled instead.
- @protected
- @returns {HTMLElement} the labellable element.
- */
- _getLabellableElement() {
- // Use predefined element or query it
- const element = this._labellableElement || this.querySelector(LABELLABLE_ELEMENTS_SELECTOR);
-
- // Use the found element or the container
- return element || this;
- }
-
- /**
- Gets the internal input that the BaseFormField would watch for change. By default, it searches if the
- <code>_getLabellableElement()</code> is an input. Components can override this function to be able to provide a
- different implementation. In case the value is <code>null</code>, the change event will be handled no matter
- the input that produced it.
- @protected
- @return {HTMLElement} the input to watch for changes.
- */
- _getTargetChangeInput() {
- // we use this._targetChangeInput as an internal cache to avoid querying the DOM again every time
- return this._targetChangeInput ||
- // assignment returns the value
- (this._targetChangeInput = this._getLabellableElement().matches(TARGET_INPUT_SELECTOR) ?
- this._getLabellableElement() : null);
- }
-
- /**
- Function called whenever the target component triggers a change event. <code>_getTargetChangeInput</code> is used
- internally to determine if the input belongs to the component. If the component decides to override this function,
- the default from the base will not be called.
- @protected
- */
- _onInputChange(event) {
- // stops the current event
- event.stopPropagation();
-
- /** @ignore */
- this[this._componentTargetProperty] = event.target[this._eventTargetProperty];
-
- // Explicitly re-emit the change event after the property has been set
- if (this._triggerChangeEvent) {
- this.trigger('change');
- }
- }
-
- /**
- Resets the formField when a reset is triggered on the parent form.
- @protected
- */
- _onFormReset(event) {
- if (event.target.contains(this)) {
- this.reset();
- }
- }
-
- /**
- We capture every input change and validate that it belongs to our target input. If this is the case,
- <code>_onInputChange</code> will be called with the same event.
- @protected
- */
- _onTargetInputChange(event) {
- const targetInput = this._getTargetChangeInput();
- // if the targetInput is null we still call _onInputChange to be backwards compatible
- if (targetInput === event.target || targetInput === null) {
- // we call _onInputChange since the target matches
- this._onInputChange(event);
- }
- }
-
- /**
- A utility method for adding the appropriate <code>for</code> attribute to any <code>label</code> elements
- referenced by the <code>labelledBy</code> property value.
- @param {String} labelledBy
- The value of the <code>labelledBy<code> property providing a space-delimited list of the <code>id</code>
- attributes for elements that label the formField.
- @param {String} elementId
- The <code>id</code> of the formField or one of its descendants that should be labelled by
- <code>label</code> elements referenced by the <code>labelledBy</code> property value.
- @param {Boolean} remove
- Whether the existing <code>for</code> attributes should be removed.
- @protected
- */
- _updateForAttributes(labelledBy, elementId, remove) {
- // labelledby contains whitespace sparated items, so we need to separate each individual id
- const labelIds = labelledBy.split(/\s+/);
- // we update the 'for' attribute for every id.
- labelIds.forEach((currentValue) => {
- const labelElement = document.getElementById(currentValue);
- if (labelElement && labelElement.tagName === 'LABEL') {
- const forAttribute = labelElement.getAttribute('for');
-
- if (remove) {
- // we just remove it when it is our target
- if (forAttribute === elementId) {
- labelElement.removeAttribute('for');
- }
- } else {
- // if we do not have to remove, it does not matter the current value of the label, we can set it in every
- // case
- labelElement.setAttribute('for', elementId);
- }
- }
- });
- }
-
- /**
- Clears the <code>value</code> of formField to the default value.
- */
- clear() {
- /** @ignore */
- this.value = '';
- }
-
- /**
- Resets the <code>value</code> to the initial value.
- */
- reset() {
- // since the 'value' property is not reflected, form components use it to restore the initial value. When a
- // component has support for values, this method needs to be overwritten
- /** @ignore */
- this.value = transform.string(this.getAttribute('value'));
- }
-
- static get _attributePropertyMap() {
- return commons.extend(super._attributePropertyMap, {
- describedby: 'describedBy',
- labelledby: 'labelledBy',
- readonly: 'readOnly',
- });
- }
-
- // We don't want to watch existing attributes for components that extend native HTML elements
- static get _nativeObservedAttributes() {
- return super.observedAttributes.concat([
- 'describedby',
- 'labelled',
- 'labelledby',
- 'invalid'
- ]);
- }
-
- /** @ignore */
- static get observedAttributes() {
- return super.observedAttributes.concat([
- 'describedby',
- 'labelled',
- 'labelledby',
- 'invalid',
- 'readonly',
- 'name',
- 'value',
- 'disabled',
- 'required'
- ]);
- }
- };
-
- export default BaseFormField;