ExamplesPlaygroundReference Source

coral-spectrum/coral-component-checkbox/src/scripts/Checkbox.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 {BaseComponent} from '../../../coral-base-component';
  14. import {BaseFormField} from '../../../coral-base-formfield';
  15. import {Icon} from '../../../coral-component-icon';
  16. import base from '../templates/base';
  17. import {transform, commons, i18n} from '../../../coral-utils';
  18. import {Decorator} from '../../../coral-decorator';
  19.  
  20. const IS_IE_OR_EDGE = navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > 0 ||
  21. window.navigator.userAgent.indexOf('Edge') !== -1;
  22.  
  23. const CLASSNAME = '_coral-Checkbox';
  24.  
  25. /**
  26. @class Coral.Checkbox
  27. @classdesc A Checkbox component to be used as a form field.
  28. @htmltag coral-checkbox
  29. @extends {HTMLElement}
  30. @extends {BaseComponent}
  31. @extends {BaseFormField}
  32. */
  33. const Checkbox = Decorator(class extends BaseFormField(BaseComponent(HTMLElement)) {
  34. /** @ignore */
  35. constructor() {
  36. super();
  37.  
  38. // @polyfill ie
  39. this._delegateEvents(commons.extend(this._events, {
  40. click: '_onClick',
  41. mousedown: '_onMouseDown'
  42. }));
  43.  
  44. // Prepare templates
  45. this._elements = {
  46. // Try to find the label content zone or create one
  47. label: this.querySelector('coral-checkbox-label') || document.createElement('coral-checkbox-label')
  48. };
  49. base.call(this._elements, {commons, i18n, Icon});
  50.  
  51. // Pre-define labellable element
  52. this._labellableElement = this._elements.input;
  53.  
  54. // Check if the label is empty whenever we get a mutation
  55. this._observer = new MutationObserver(this._hideLabelIfEmpty.bind(this));
  56.  
  57. // Watch for changes to the label element's children
  58. this._observer.observe(this._elements.labelWrapper, {
  59. // Catch changes to childList
  60. childList: true,
  61. // Catch changes to textContent
  62. characterData: true,
  63. // Monitor any child node
  64. subtree: true
  65. });
  66. }
  67.  
  68. /**
  69. Checked state for the checkbox.
  70.  
  71. @type {Boolean}
  72. @default false
  73. @htmlattribute checked
  74. @htmlattributereflected
  75. @emits {change}
  76. */
  77. get checked() {
  78. return this._checked || false;
  79. }
  80.  
  81. set checked(value) {
  82. this._checked = transform.booleanAttr(value);
  83. this._reflectAttribute('checked', this._checked);
  84.  
  85. this._elements.input.checked = this._checked;
  86. }
  87.  
  88. /**
  89. Indicates that the checkbox is neither on nor off.
  90.  
  91. @type {Boolean}
  92. @default false
  93. @htmlattribute indeterminate
  94. @htmlattributereflected
  95. */
  96. get indeterminate() {
  97. return this._indeterminate || false;
  98. }
  99.  
  100. set indeterminate(value) {
  101. this._indeterminate = transform.booleanAttr(value);
  102. this._reflectAttribute('indeterminate', this._indeterminate);
  103.  
  104. this.classList.toggle('is-indeterminate', this._indeterminate);
  105. this._elements.input.indeterminate = this._indeterminate;
  106. this._elements.input[this._indeterminate ? 'setAttribute' : 'removeAttribute']('aria-checked', 'mixed');
  107. }
  108.  
  109. /**
  110. The checkbox's label element.
  111.  
  112. @type {CheckboxLabel}
  113. @contentzone
  114. */
  115. get label() {
  116. return this._getContentZone(this._elements.label);
  117. }
  118.  
  119. set label(value) {
  120. this._setContentZone('label', value, {
  121. handle: 'label',
  122. tagName: 'coral-checkbox-label',
  123. insert: function (label) {
  124. this._elements.labelWrapper.appendChild(label);
  125. }
  126. });
  127. }
  128.  
  129. /**
  130. Name used to submit the data in a form.
  131. @type {String}
  132. @default ""
  133. @htmlattribute name
  134. @htmlattributereflected
  135. */
  136. get name() {
  137. return this._elements.input.name;
  138. }
  139.  
  140. set name(value) {
  141. this._reflectAttribute('name', value);
  142.  
  143. this._elements.input.name = value;
  144. }
  145.  
  146. /**
  147. The value that will be submitted when the checkbox is checked. Changing this value will not trigger an event.
  148.  
  149. @type {String}
  150. @default "on"
  151. @htmlattribute value
  152. */
  153. get value() {
  154. return this._elements.input.value || 'on';
  155. }
  156.  
  157. set value(value) {
  158. this._elements.input.value = value;
  159. }
  160.  
  161. /**
  162. Whether this field is disabled or not.
  163. @type {Boolean}
  164. @default false
  165. @htmlattribute disabled
  166. @htmlattributereflected
  167. */
  168. get disabled() {
  169. return this._disabled || false;
  170. }
  171.  
  172. set disabled(value) {
  173. this._disabled = transform.booleanAttr(value);
  174. this._reflectAttribute('disabled', this._disabled);
  175.  
  176. this[this._disabled ? 'setAttribute' : 'removeAttribute']('aria-disabled', this._disabled);
  177. this.classList.toggle('is-disabled', this._disabled);
  178. this._elements.input.disabled = this._disabled;
  179. }
  180.  
  181. /**
  182. Whether this field is required or not.
  183. @type {Boolean}
  184. @default false
  185. @htmlattribute required
  186. @htmlattributereflected
  187. */
  188. get required() {
  189. return this._required || false;
  190. }
  191.  
  192. set required(value) {
  193. this._required = transform.booleanAttr(value);
  194. this._reflectAttribute('required', this._required);
  195.  
  196. this._elements.input.required = this._required;
  197. }
  198.  
  199. /**
  200. Whether this field is readOnly or not. Indicating that the user cannot modify the value of the control.
  201. @type {Boolean}
  202. @default false
  203. @htmlattribute readonly
  204. @htmlattributereflected
  205. */
  206. get readOnly() {
  207. return this._readOnly || false;
  208. }
  209.  
  210. set readOnly(value) {
  211. this._readOnly = transform.booleanAttr(value);
  212. this._reflectAttribute('readonly', this._readOnly);
  213.  
  214. this.classList.toggle('is-readOnly', this._readOnly);
  215. this._elements.input.tabIndex = this._readOnly ? -1 : 0;
  216. }
  217.  
  218. /**
  219. Inherited from {@link BaseFormField#labelled}.
  220. */
  221. get labelled() {
  222. return super.labelled;
  223. }
  224.  
  225. set labelled(value) {
  226. super.labelled = value;
  227.  
  228. this._hideLabelIfEmpty();
  229. }
  230.  
  231. /**
  232. Inherited from {@link BaseFormField#labelledBy}.
  233. */
  234. get labelledBy() {
  235. return super.labelledBy;
  236. }
  237.  
  238. set labelledBy(value) {
  239. super.labelledBy = value;
  240.  
  241. this._hideLabelIfEmpty();
  242. }
  243.  
  244. /**
  245. Inherited from {@link BaseComponent#trackingElement}.
  246. */
  247. get trackingElement() {
  248. // it uses the name as the first fallback since it is not localized, otherwise it uses the label
  249. return typeof this._trackingElement === 'undefined' ?
  250. // keep spaces to only 1 max and trim. this mimics native html behaviors
  251. (this.name ? `${this.name}=${this.value}` : '') || (this.label || this).textContent.replace(/\s{2,}/g, ' ').trim() :
  252. this._trackingElement;
  253. }
  254.  
  255. set trackingElement(value) {
  256. super.trackingElement = value;
  257. }
  258.  
  259. /*
  260. Indicates to the formField that the 'checked' property needs to be set in this component.
  261.  
  262. @protected
  263. */
  264. get _componentTargetProperty() {
  265. return 'checked';
  266. }
  267.  
  268. /*
  269. Indicates to the formField that the 'checked' property has to be extracted from the event.
  270.  
  271. @protected
  272. */
  273. get _eventTargetProperty() {
  274. return 'checked';
  275. }
  276.  
  277. /** @private */
  278. _onInputChange(event) {
  279. // stops the current event
  280. event.stopPropagation();
  281.  
  282. /** @ignore */
  283. this[this._componentTargetProperty] = event.target[this._eventTargetProperty];
  284.  
  285. // resets the indeterminate state after user interaction
  286. this.indeterminate = false;
  287.  
  288. // Explicitly re-emit the change event after the property has been set
  289. if (this._triggerChangeEvent) {
  290. // @polyfill ie/edge
  291. if (IS_IE_OR_EDGE) {
  292. // We need 1 additional frame in case the indeterminate state is set manually on change event
  293. window.requestAnimationFrame(() => {
  294. this.trigger('change');
  295. });
  296. } else {
  297. this.trigger('change');
  298. }
  299. }
  300. }
  301.  
  302. /**
  303. @private
  304. @polyfill ie/edge
  305. */
  306. _onClick(event) {
  307. // Force the check/uncheck and trigger the change event since IE won't.
  308. if (IS_IE_OR_EDGE && this.indeterminate) {
  309. // Other browsers like Chrome and Firefox will trigger the change event and set indeterminate = false. So we
  310. // verify if indeterminate was changed and if not, we manually check/uncheck and trigger the change event.
  311. this.checked = !this.checked;
  312. this._onInputChange(event);
  313. }
  314. // Handle the click() just like the native checkbox
  315. else if (event.target === this) {
  316. this.indeterminate = false;
  317. this.checked = !this.checked;
  318. this.trigger('change');
  319. }
  320.  
  321. this._trackEvent(this.checked ? 'checked' : 'unchecked', 'coral-checkbox', event);
  322. }
  323.  
  324. /**
  325. Forces checkbox to receive focus on mousedown
  326. @ignore
  327. */
  328. _onMouseDown() {
  329. const target = this._elements.input;
  330. window.requestAnimationFrame(() => {
  331. if (target !== document.activeElement) {
  332. target.focus();
  333. }
  334. });
  335. }
  336.  
  337. /**
  338. Hide the label if it's empty
  339. @ignore
  340. */
  341. _hideLabelIfEmpty() {
  342. const label = this._elements.label;
  343.  
  344. // If it's empty and has no non-textnode children, hide the label
  345. const hiddenValue = !(label.children.length === 0 && label.textContent.replace(/\s*/g, '') === '');
  346.  
  347. // Toggle the screen reader text
  348. this._elements.labelWrapper.style.margin = !hiddenValue ? '0' : '';
  349. this._elements.screenReaderOnly.hidden = !!hiddenValue || !!this.labelledBy || !!this.labelled;
  350. }
  351.  
  352. /**
  353. Inherited from {@link BaseFormField#clear}.
  354. */
  355. clear() {
  356. this.checked = false;
  357. }
  358.  
  359. /**
  360. Inherited from {@link BaseFormField#reset}.
  361. */
  362. reset() {
  363. this.checked = this._initialCheckedState;
  364. }
  365.  
  366. get _contentZones() {
  367. return {'coral-checkbox-label': 'label'};
  368. }
  369.  
  370. /** @ignore */
  371. static get observedAttributes() {
  372. return super.observedAttributes.concat(['indeterminate', 'checked']);
  373. }
  374.  
  375. /** @ignore */
  376. render() {
  377. super.render();
  378.  
  379. this.classList.add(CLASSNAME);
  380.  
  381. // Create a fragment
  382. const frag = document.createDocumentFragment();
  383.  
  384. const templateHandleNames = ['input', 'checkbox', 'labelWrapper'];
  385.  
  386. // Render the main template
  387. frag.appendChild(this._elements.input);
  388. frag.appendChild(this._elements.checkbox);
  389. frag.appendChild(this._elements.labelWrapper);
  390.  
  391. const label = this._elements.label;
  392.  
  393. // Remove it so we can process children
  394. if (label.parentNode) {
  395. label.parentNode.removeChild(label);
  396. }
  397.  
  398. while (this.firstChild) {
  399. const child = this.firstChild;
  400. if (child.nodeType === Node.TEXT_NODE ||
  401. child.nodeType === Node.ELEMENT_NODE && templateHandleNames.indexOf(child.getAttribute('handle')) === -1) {
  402. // Add non-template elements to the label
  403. label.appendChild(child);
  404. } else {
  405. // Remove anything else (e.g labelWrapper)
  406. this.removeChild(child);
  407. }
  408. }
  409.  
  410. // Add the frag to the component
  411. this.appendChild(frag);
  412.  
  413. // Assign the content zones, moving them into place in the process
  414. this.label = label;
  415.  
  416. // Cache the initial checked state of the checkbox (in order to implement reset)
  417. this._initialCheckedState = this.checked;
  418.  
  419. // Check if we need to hide the label
  420. // We must do this because IE does not catch mutations when nodes are not in the DOM
  421. this._hideLabelIfEmpty();
  422. }
  423. });
  424.  
  425. export default Checkbox;