ExamplesPlaygroundReference Source

coral-spectrum/coral-base-formfield/src/scripts/BaseFormField.js

  1. /**
  2. * Copyright 2019 Adobe. All rights reserved.
  3. * This file is licensed to you under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License. You may obtain a copy
  5. * of the License at http://www.apache.org/licenses/LICENSE-2.0
  6. *
  7. * Unless required by applicable law or agreed to in writing, software distributed under
  8. * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
  9. * OF ANY KIND, either express or implied. See the License for the specific language
  10. * governing permissions and limitations under the License.
  11. */
  12.  
  13. import {transform, commons, validate} from '../../../coral-utils';
  14.  
  15. // https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories
  16. let LABELLABLE_ELEMENTS_SELECTOR = 'button,input:not([type=hidden]),keygen,meter,output,progress,select,textarea';
  17. // @polyfill ie11
  18. // IE11 throws syntax error because of the "not()" in the selector for some reason in ColorInputColorProperties
  19. if (navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0) {
  20. LABELLABLE_ELEMENTS_SELECTOR = 'button,keygen,meter,output,progress,select,textarea,';
  21.  
  22. // Since we can't use :not() we have to indicate all input types
  23. [
  24. 'text',
  25. 'password',
  26. 'submit',
  27. 'reset',
  28. 'radio',
  29. 'checkbox',
  30. 'button',
  31. 'color',
  32. 'date',
  33. 'datetime-local',
  34. 'email',
  35. 'month',
  36. 'number',
  37. 'range',
  38. 'search',
  39. 'tel',
  40. 'time',
  41. 'url',
  42. 'week'
  43. ].forEach((type) => {
  44. LABELLABLE_ELEMENTS_SELECTOR += `input[type=${type}],`;
  45. });
  46.  
  47. // Remove last ","
  48. LABELLABLE_ELEMENTS_SELECTOR = LABELLABLE_ELEMENTS_SELECTOR.slice(0, -1);
  49. }
  50. // _onInputChange is only triggered on non-hidden inputs
  51. const TARGET_INPUT_SELECTOR = 'input:not([type=hidden])';
  52.  
  53. /**
  54. @base BaseFormField
  55. @classdesc The base element for Form Field components. If not extending a {@link HTMLInputElement}, following
  56. properties should be implemented at least :
  57. - <code>disabled</code>. Whether this field is disabled or not.
  58. - <code>invalid</code>. Whether the current value of this field is invalid or not.
  59. - <code>name</code>. Name used to submit the data in a form.
  60. - <code>readOnly</code>. Whether this field is readOnly or not. Indicating that the user cannot modify the value of the control.
  61. - <code>required</code>. Whether this field is required or not.
  62. - <code>value</code>. This field's current value.
  63. */
  64. class BaseFormField extends superClass {
  65. /** @ignore */
  66. constructor() {
  67. super();
  68.  
  69. this._events = {
  70. 'capture:change input': '_onTargetInputChange',
  71. 'global:reset': '_onFormReset'
  72. };
  73. }
  74.  
  75. /**
  76. Whether this field is disabled or not.
  77.  
  78. @type {Boolean}
  79. @default false
  80. @htmlattribute disabled
  81. @htmlattributereflected
  82. @abstract
  83. */
  84.  
  85. /**
  86. Whether the current value of this field is invalid or not.
  87.  
  88. @type {Boolean}
  89. @default false
  90. @htmlattribute invalid
  91. @htmlattributereflected
  92. @abstract
  93. */
  94.  
  95. /**
  96. Name used to submit the data in a form.
  97.  
  98. @type {String}
  99. @default ""
  100. @htmlattribute name
  101. @htmlattributereflected
  102. @abstract
  103. */
  104.  
  105. /**
  106. Whether this field is readOnly or not. Indicating that the user cannot modify the value of the control.
  107. This is ignored for checkbox, radio or fileupload.
  108.  
  109. @type {Boolean}
  110. @default false
  111. @htmlattribute readonly
  112. @htmlattributereflected
  113. @abstract
  114. */
  115.  
  116. /**
  117. Whether this field is required or not.
  118.  
  119. @type {Boolean}
  120. @default false
  121. @htmlattribute required
  122. @htmlattributereflected
  123. @abstract
  124. */
  125.  
  126. /**
  127. This field's current value.
  128.  
  129. @type {String}
  130. @default ""
  131. @htmlattribute value
  132. @abstract
  133. */
  134.  
  135. /**
  136. Whether the current value of this field is invalid or not.
  137.  
  138. @type {Boolean}
  139. @default false
  140. @htmlattribute invalid
  141. @htmlattributereflected
  142. */
  143. get invalid() {
  144. return this._invalid || false;
  145. }
  146.  
  147. set invalid(value) {
  148. value = transform.booleanAttr(value);
  149.  
  150. this._reflectAttribute('invalid', value);
  151. if(validate.valueMustChange(this._invalid, value)) {
  152. this._invalid = value;
  153. this.setAttribute('aria-invalid', value);
  154. this.classList.toggle('is-invalid', value);
  155. }
  156. }
  157.  
  158. /**
  159. Reflects the <code>aria-describedby</code> attribute to the labellable element e.g. inner input.
  160.  
  161. @type {String}
  162. @default null
  163. @htmlattribute describedby
  164. */
  165. get describedBy() {
  166. return this._getLabellableElement().getAttribute('aria-describedby');
  167. }
  168.  
  169. set describedBy(value) {
  170. value = transform.string(value);
  171.  
  172. this._getLabellableElement()[value ? 'setAttribute' : 'removeAttribute']('aria-describedby', value);
  173. }
  174.  
  175. /**
  176. Reflects the <code>aria-label</code> attribute to the labellable element e.g. inner input.
  177.  
  178. @type {String}
  179. @default null
  180. @htmlattribute labelled
  181. */
  182. get labelled() {
  183. return this._getLabellableElement().getAttribute('aria-label');
  184. }
  185.  
  186. set labelled(value) {
  187. value = transform.string(value);
  188.  
  189. this._getLabellableElement()[value ? 'setAttribute' : 'removeAttribute']('aria-label', value);
  190. }
  191.  
  192. /**
  193. Reference to a space delimited set of ids for the HTML elements that provide a label for the formField.
  194. Implementers should override this method to ensure that the appropriate descendant elements are labelled using the
  195. <code>aria-labelledby</code> attribute. This will ensure that the component is properly identified for
  196. accessibility purposes. It reflects the <code>aria-labelledby</code> attribute to the DOM.
  197. @type {?String}
  198. @default null
  199. @htmlattribute labelledby
  200. */
  201. get labelledBy() {
  202. return this._getLabellableElement().getAttribute('aria-labelledby');
  203. }
  204.  
  205. set labelledBy(value) {
  206. value = transform.string(value);
  207.  
  208. // gets the element that will get the label assigned. the _getLabellableElement method should be overriden to
  209. // allow other bevaviors.
  210. const element = this._getLabellableElement();
  211. // we get and assign the it that will be passed around
  212. const elementId = element.id = element.id || commons.getUID();
  213.  
  214. const currentLabelledBy = element.getAttribute('aria-labelledby');
  215.  
  216. // we clear the old label assignments
  217. if (currentLabelledBy && currentLabelledBy !== value) {
  218. this._updateForAttributes(currentLabelledBy, elementId, true);
  219. }
  220.  
  221. if (value) {
  222. element.setAttribute('aria-labelledby', value);
  223. if (element.matches(LABELLABLE_ELEMENTS_SELECTOR)) {
  224. this._updateForAttributes(value, elementId);
  225. }
  226. } else {
  227. // since no labelledby value was set, we remove everything
  228. element.removeAttribute('aria-labelledby');
  229. }
  230. }
  231.  
  232. /**
  233. Target property inside the component that will be updated when a change event is triggered.
  234. @type {String}
  235. @default "value"
  236. @protected
  237. */
  238. get _componentTargetProperty() {
  239. return 'value';
  240. }
  241.  
  242. /**
  243. Target property that will be taken from <code>event.target</code> and set into
  244. {@link BaseFormField#_componentTargetProperty} when a change event is triggered.
  245. @type {String}
  246. @default "value"
  247. @protected
  248. */
  249. get _eventTargetProperty() {
  250. return 'value';
  251. }
  252.  
  253. /**
  254. Whether the change event needs to be triggered when {@link BaseFormField#_onInputChange} is called.
  255. @type {Boolean}
  256. @default true
  257. @protected
  258. */
  259. get _triggerChangeEvent() {
  260. return true;
  261. }
  262.  
  263. /**
  264. Gets the element that should get the label. In case none of the valid labelelable items are found, the component
  265. will be labelled instead.
  266. @protected
  267. @returns {HTMLElement} the labellable element.
  268. */
  269. _getLabellableElement() {
  270. // Use predefined element or query it
  271. const element = this._labellableElement || this.querySelector(LABELLABLE_ELEMENTS_SELECTOR);
  272.  
  273. // Use the found element or the container
  274. return element || this;
  275. }
  276.  
  277. /**
  278. Gets the internal input that the BaseFormField would watch for change. By default, it searches if the
  279. <code>_getLabellableElement()</code> is an input. Components can override this function to be able to provide a
  280. different implementation. In case the value is <code>null</code>, the change event will be handled no matter
  281. the input that produced it.
  282. @protected
  283. @return {HTMLElement} the input to watch for changes.
  284. */
  285. _getTargetChangeInput() {
  286. // we use this._targetChangeInput as an internal cache to avoid querying the DOM again every time
  287. return this._targetChangeInput ||
  288. // assignment returns the value
  289. (this._targetChangeInput = this._getLabellableElement().matches(TARGET_INPUT_SELECTOR) ?
  290. this._getLabellableElement() : null);
  291. }
  292.  
  293. /**
  294. Function called whenever the target component triggers a change event. <code>_getTargetChangeInput</code> is used
  295. internally to determine if the input belongs to the component. If the component decides to override this function,
  296. the default from the base will not be called.
  297. @protected
  298. */
  299. _onInputChange(event) {
  300. // stops the current event
  301. event.stopPropagation();
  302.  
  303. /** @ignore */
  304. this[this._componentTargetProperty] = event.target[this._eventTargetProperty];
  305.  
  306. // Explicitly re-emit the change event after the property has been set
  307. if (this._triggerChangeEvent) {
  308. this.trigger('change');
  309. }
  310. }
  311.  
  312. /**
  313. Resets the formField when a reset is triggered on the parent form.
  314. @protected
  315. */
  316. _onFormReset(event) {
  317. if (event.target.contains(this)) {
  318. this.reset();
  319. }
  320. }
  321.  
  322. /**
  323. We capture every input change and validate that it belongs to our target input. If this is the case,
  324. <code>_onInputChange</code> will be called with the same event.
  325. @protected
  326. */
  327. _onTargetInputChange(event) {
  328. const targetInput = this._getTargetChangeInput();
  329. // if the targetInput is null we still call _onInputChange to be backwards compatible
  330. if (targetInput === event.target || targetInput === null) {
  331. // we call _onInputChange since the target matches
  332. this._onInputChange(event);
  333. }
  334. }
  335.  
  336. /**
  337. A utility method for adding the appropriate <code>for</code> attribute to any <code>label</code> elements
  338. referenced by the <code>labelledBy</code> property value.
  339. @param {String} labelledBy
  340. The value of the <code>labelledBy<code> property providing a space-delimited list of the <code>id</code>
  341. attributes for elements that label the formField.
  342. @param {String} elementId
  343. The <code>id</code> of the formField or one of its descendants that should be labelled by
  344. <code>label</code> elements referenced by the <code>labelledBy</code> property value.
  345. @param {Boolean} remove
  346. Whether the existing <code>for</code> attributes should be removed.
  347. @protected
  348. */
  349. _updateForAttributes(labelledBy, elementId, remove) {
  350. // labelledby contains whitespace sparated items, so we need to separate each individual id
  351. const labelIds = labelledBy.split(/\s+/);
  352. // we update the 'for' attribute for every id.
  353. labelIds.forEach((currentValue) => {
  354. const labelElement = document.getElementById(currentValue);
  355. if (labelElement && labelElement.tagName === 'LABEL') {
  356. const forAttribute = labelElement.getAttribute('for');
  357.  
  358. if (remove) {
  359. // we just remove it when it is our target
  360. if (forAttribute === elementId) {
  361. labelElement.removeAttribute('for');
  362. }
  363. } else {
  364. // if we do not have to remove, it does not matter the current value of the label, we can set it in every
  365. // case
  366. labelElement.setAttribute('for', elementId);
  367. }
  368. }
  369. });
  370. }
  371.  
  372. /**
  373. Clears the <code>value</code> of formField to the default value.
  374. */
  375. clear() {
  376. /** @ignore */
  377. this.value = '';
  378. }
  379.  
  380. /**
  381. Resets the <code>value</code> to the initial value.
  382. */
  383. reset() {
  384. // since the 'value' property is not reflected, form components use it to restore the initial value. When a
  385. // component has support for values, this method needs to be overwritten
  386. /** @ignore */
  387. this.value = transform.string(this.getAttribute('value'));
  388. }
  389.  
  390. static get _attributePropertyMap() {
  391. return commons.extend(super._attributePropertyMap, {
  392. describedby: 'describedBy',
  393. labelledby: 'labelledBy',
  394. readonly: 'readOnly',
  395. });
  396. }
  397.  
  398. // We don't want to watch existing attributes for components that extend native HTML elements
  399. static get _nativeObservedAttributes() {
  400. return super.observedAttributes.concat([
  401. 'describedby',
  402. 'labelled',
  403. 'labelledby',
  404. 'invalid'
  405. ]);
  406. }
  407.  
  408. /** @ignore */
  409. static get observedAttributes() {
  410. return super.observedAttributes.concat([
  411. 'describedby',
  412. 'labelled',
  413. 'labelledby',
  414. 'invalid',
  415. 'readonly',
  416. 'name',
  417. 'value',
  418. 'disabled',
  419. 'required'
  420. ]);
  421. }
  422. };
  423.  
  424. export default BaseFormField;