coral-spectrum/coral-base-button/src/scripts/BaseButton.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 {BaseLabellable} from '../../../coral-base-labellable';
- import {Icon} from '../../../coral-component-icon';
- import {transform, validate, commons} from '../../../coral-utils';
-
- /**
- Enumeration for {@link Button}, {@link AnchorButton} icon sizes.
-
- @typedef {Object} ButtonIconSizeEnum
-
- @property {String} EXTRA_EXTRA_SMALL
- Extra extra small size icon, typically 9px size.
- @property {String} EXTRA_SMALL
- Extra small size icon, typically 12px size.
- @property {String} SMALL
- Small size icon, typically 18px size. This is the default size.
- @property {String} MEDIUM
- Medium size icon, typically 24px size.
- */
- const iconSize = {};
- const excludedIconSizes = [Icon.size.LARGE, Icon.size.EXTRA_LARGE, Icon.size.EXTRA_EXTRA_LARGE];
- for (const key in Icon.size) {
- // Populate button icon sizes by excluding the largest icon sizes
- if (excludedIconSizes.indexOf(Icon.size[key]) === -1) {
- iconSize[key] = Icon.size[key];
- }
- }
-
- /**
- Enumeration for {@link Button}, {@link AnchorButton} variants.
-
- @typedef {Object} ButtonVariantEnum
-
- @property {String} CTA
- A button that is meant to grab the user's attention.
- @property {String} PRIMARY
- A button that is meant to grab the user's attention.
- @property {String} QUIET
- A quiet button that indicates that the button's action is the primary action.
- @property {String} SECONDARY
- A button that indicates that the button's action is the secondary action.
- @property {String} QUIET_SECONDARY
- A quiet secondary button.
- @property {String} ACTION
- An action button.
- @property {String} QUIET_ACTION
- A quiet action button.
- @property {String} MINIMAL
- A quiet minimalistic button.
- @property {String} WARNING
- A button that indicates that the button's action is dangerous.
- @property {String} QUIET_WARNING
- A quiet warning button,
- @property {String} OVER_BACKGROUND
- A button to be placed on top of colored background.
- @property {String} DEFAULT
- The default button look and feel.
- */
- const variant = {
- CTA: 'cta',
- PRIMARY: 'primary',
- SECONDARY: 'secondary',
- QUIET: 'quiet',
- MINIMAL: 'minimal',
- WARNING: 'warning',
- ACTION: 'action',
- QUIET_ACTION: 'quietaction',
- QUIET_SECONDARY: 'quietsecondary',
- QUIET_WARNING: 'quietwarning',
- OVER_BACKGROUND: 'overbackground',
- DEFAULT: 'default',
- // Private to be used for custom Button classes like field buttons
- _CUSTOM: '_custom'
- };
-
- // the button's base classname
- const CLASSNAME = '_coral-Button';
- const ACTION_CLASSNAME = '_coral-ActionButton';
-
- const ALL_VARIANT_CLASSES = [
- `${CLASSNAME}--cta`,
- `${CLASSNAME}--primary`,
- `${CLASSNAME}--secondary`,
- `${CLASSNAME}--warning`,
- `${CLASSNAME}--quiet`,
- `${ACTION_CLASSNAME}--quiet`,
- `${CLASSNAME}--overBackground`,
- ];
-
- const VARIANT_MAP = {
- cta: [CLASSNAME, ALL_VARIANT_CLASSES[0]],
- primary: [CLASSNAME, ALL_VARIANT_CLASSES[0]],
- secondary: [CLASSNAME, ALL_VARIANT_CLASSES[2]],
- warning: [CLASSNAME, ALL_VARIANT_CLASSES[3]],
- quiet: [CLASSNAME, ALL_VARIANT_CLASSES[1], ALL_VARIANT_CLASSES[4]],
- minimal: [CLASSNAME, ALL_VARIANT_CLASSES[2], ALL_VARIANT_CLASSES[4]],
- default: [CLASSNAME, ALL_VARIANT_CLASSES[1]],
- action: [ACTION_CLASSNAME],
- quietaction: [ACTION_CLASSNAME, ALL_VARIANT_CLASSES[5]],
- quietsecondary: [CLASSNAME, ALL_VARIANT_CLASSES[2], ALL_VARIANT_CLASSES[4]],
- quietwarning: [CLASSNAME, ALL_VARIANT_CLASSES[3], ALL_VARIANT_CLASSES[4]],
- overbackground: [CLASSNAME, ALL_VARIANT_CLASSES[6]]
- };
-
- /**
- Enumeration for {@link BaseButton} sizes.
-
- @typedef {Object} ButtonSizeEnum
-
- @property {String} MEDIUM
- A medium button is the default, normal sized button.
- @property {String} LARGE
- Not supported. Falls back to MEDIUM.
- */
- const size = {
- MEDIUM: 'M',
- LARGE: 'L'
- };
-
- /**
- Enumeration for {@link BaseButton} icon position options.
-
- @typedef {Object} ButtonIconPositionEnum
-
- @property {String} RIGHT
- Position should be right of the button label.
- @property {String} LEFT
- Position should be left of the button label.
- */
- const iconPosition = {
- RIGHT: 'right',
- LEFT: 'left'
- };
-
- /**
- @base BaseButton
- @classdesc The base element for Button components
- */
- const BaseButton = (superClass) => class extends BaseLabellable(superClass) {
- /** @ignore */
- constructor() {
- super();
-
- // Templates
- this._elements = {
- // Create or fetch the label element
- label: this.querySelector(this._contentZoneTagName) || document.createElement(this._contentZoneTagName),
- icon: this.querySelector('coral-icon')
- };
-
- // Events
- this._events = {
- mousedown: '_onMouseDown',
- click: '_onClick'
- };
-
- super._observeLabel();
- }
-
- /**
- The label of the button.
- @type {HTMLElement}
- @contentzone
- */
- get label() {
- return this._getContentZone(this._elements.label);
- }
-
- set label(value) {
- this._setContentZone('label', value, {
- handle: 'label',
- tagName: this._contentZoneTagName,
- insert: function (label) {
- // Update label styles
- this._updateLabel(label);
-
- // Ensure there's no extra space left for icon only buttons
- if (label.innerHTML.trim() === '') {
- label.textContent = '';
- }
-
- if (this.iconPosition === iconPosition.LEFT) {
- this.appendChild(label);
- } else {
- this.insertBefore(label, this.firstChild);
- }
- }
- });
- }
-
- /**
- Position of the icon relative to the label. If no <code>iconPosition</code> is provided, it will be set on the
- left side by default.
- See {@link ButtonIconPositionEnum}.
-
- @type {String}
- @default ButtonIconPositionEnum.LEFT
- @htmlattribute iconposition
- @htmlattributereflected
- */
- get iconPosition() {
- return this._iconPosition || iconPosition.LEFT;
- }
-
- set iconPosition(value) {
- value = transform.string(value).toLowerCase();
- value = validate.enumeration(iconPosition)(value) && value || iconPosition.LEFT;
-
- this._reflectAttribute('iconposition', value);
-
- if(validate.valueMustChange(this._iconPosition, value)) {
- this._iconPosition = value;
- this._updateIcon(this.icon);
- }
- }
-
- /**
- Specifies the icon name used inside the button. See {@link Icon} for valid icon names.
-
- @type {String}
- @default ""
- @htmlattribute icon
- */
- get icon() {
- if (this._elements.icon) {
- return this._elements.icon.getAttribute('icon') || '';
- }
-
- return this._icon || '';
- }
-
- set icon(value) {
- value = transform.string(value);
- if(validate.valueMustChange(this._icon, value)) {
- this._icon = value;
- this._updateIcon(value);
- }
- }
-
- /**
- Size of the icon. It accepts both lower and upper case sizes. See {@link ButtonIconSizeEnum}.
-
- @type {String}
- @default ButtonIconSizeEnum.SMALL
- @htmlattribute iconsize
- */
- get iconSize() {
- if (this._elements.icon) {
- return this._elements.icon.getAttribute('size') || Icon.size.SMALL;
- }
-
- return this._iconSize || Icon.size.SMALL;
- }
-
- set iconSize(value) {
- value = transform.string(value).toUpperCase();
- value = validate.enumeration(Icon.size)(value) && value || Icon.size.SMALL;
-
- if(validate.valueMustChange(this._iconSize, value)) {
- this._iconSize = value;
- this._updatedIcon && this._getIconElement().setAttribute('size', value);
- }
- }
-
- /**
- Whether aria-label is set automatically. See {@link IconAutoAriaLabelEnum}.
-
- @type {String}
- @default IconAutoAriaLabelEnum.OFF
- @htmlattribute autoarialabel
- */
- get iconAutoAriaLabel() {
- if (this._elements.icon) {
- return this._elements.icon.getAttribute('autoarialabel') || Icon.autoAriaLabel.OFF;
- }
-
- return this._iconAutoAriaLabel || Icon.autoAriaLabel.OFF;
- }
-
- set iconAutoAriaLabel(value) {
- value = transform.string(value).toLowerCase();
- value = validate.enumeration(Icon.autoAriaLabel)(value) && value || Icon.autoAriaLabel.OFF;
-
- if(validate.valueMustChange(this._iconAutoAriaLabel, value)) {
- this._iconAutoAriaLabel = value;
- this._updatedIcon && this._getIconElement().setAttribute('autoarialabel', value);
- }
- }
-
- /**
- The size of the button. It accepts both lower and upper case sizes. See {@link ButtonSizeEnum}.
- Currently only "MEDIUM" is supported.
-
- @type {String}
- @default ButtonSizeEnum.MEDIUM
- @htmlattribute size
- @htmlattributereflected
- */
- get size() {
- return this._size || size.MEDIUM;
- }
-
- set size(value) {
- value = transform.string(value).toUpperCase();
- this._size = validate.enumeration(size)(value) && value || size.MEDIUM;
-
- this._reflectAttribute('size', this._size);
- }
-
- /**
- Whether the button is selected.
-
- @type {Boolean}
- @default false
- @htmlattribute selected
- @htmlattributereflected
- */
- get selected() {
- return this._selected || false;
- }
-
- set selected(value) {
- value = transform.booleanAttr(value);
-
- this._reflectAttribute('selected', value);
-
- if(validate.valueMustChange(this._selected, value)) {
- this._selected = value;
-
- this.classList.toggle('is-selected', value);
-
- this.trigger('coral-button:_selectedchanged');
- }
- }
-
- // We just reflect it but we also trigger an event to be used by button group
- /** @ignore */
- get value() {
- return this.getAttribute('value');
- }
-
- set value(value) {
- this._reflectAttribute('value', value);
-
- this.trigger('coral-button:_valuechanged');
- }
-
- /**
- Expands the button to the full width of the parent.
-
- @type {Boolean}
- @default false
- @htmlattribute block
- @htmlattributereflected
- */
- get block() {
- return this._block || false;
- }
-
- set block(value) {
- value = transform.booleanAttr(value);
-
- this._reflectAttribute('block', value);
-
- if(validate.valueMustChange(this._block, value)) {
- this._block = value;
-
- this.classList.toggle(`${CLASSNAME}--block`, value);
- }
- }
-
- /**
- The button's variant. See {@link ButtonVariantEnum}.
-
- @type {String}
- @default ButtonVariantEnum.DEFAULT
- @htmlattribute variant
- @htmlattributereflected
- */
- get variant() {
- return this._variant || variant.DEFAULT;
- }
-
- set variant(value) {
- value = transform.string(value).toLowerCase();
- value = validate.enumeration(variant)(value) && value || variant.DEFAULT;
-
- this._reflectAttribute('variant', value);
-
- if(validate.valueMustChange(this._variant , value)) {
- this._variant = value;
-
- // removes every existing variant
- this.classList.remove(CLASSNAME, ACTION_CLASSNAME);
- this.classList.remove(...ALL_VARIANT_CLASSES);
-
- if (value === variant._CUSTOM) {
- this.classList.remove(CLASSNAME);
- } else {
- this.classList.add(...VARIANT_MAP[value]);
-
- if (value === variant.ACTION || value === variant.QUIET_ACTION) {
- this.classList.remove(CLASSNAME);
- }
- }
-
- // Update label styles
- this._updateLabel();
- }
- }
-
- /**
- Inherited from {@link BaseComponent#trackingElement}.
- */
- get trackingElement() {
- return typeof this._trackingElement === 'undefined' ?
- // keep spaces to only 1 max and trim. this mimics native html behaviors
- (this.label || this).textContent.replace(/\s{2,}/g, ' ').trim() || this.icon :
- this._trackingElement;
- }
-
- set trackingElement(value) {
- super.trackingElement = value;
- }
-
- _onClick(event) {
- if (!this.disabled) {
- this._trackEvent('click', this.getAttribute('is'), event);
- }
- }
-
- /** @ignore */
- _updateIcon(value) {
- if (!this._updatedIcon && this._elements.icon) {
- return;
- }
-
- this._updatedIcon = true;
-
- const iconSizeValue = this.iconSize;
- const iconAutoAriaLabelValue = this.iconAutoAriaLabel;
- const iconElement = this._getIconElement();
- iconElement.icon = value;
- // Update size as well
- iconElement.size = iconSizeValue;
- // Update autoAriaLabel as well
- iconElement.autoAriaLabel = iconAutoAriaLabelValue;
-
- // removes the icon element from the DOM.
- if (this.icon === '') {
- iconElement.remove();
- }
- // add or adjust the icon. Add it back since it was blown away by textContent
- else if (!iconElement.parentNode || this._iconPosition) {
- if (this.contains(this.label)) {
- // insertBefore with <code>null</code> appends
- this.insertBefore(iconElement, this.iconPosition === iconPosition.LEFT ? this.label : this.label.nextElementSibling);
- }
- }
-
- super._toggleIconAriaHidden();
- }
-
- /** @ignore */
- _getIconElement() {
- if (!this._elements.icon) {
- this._elements.icon = new Icon();
- this._elements.icon.size = this.iconSize;
- }
- return this._elements.icon;
- }
-
- /**
- Forces button to receive focus on mousedown
- @param {MouseEvent} event mousedown event
- @ignore
- */
- _onMouseDown(event) {
- const target = event.matchedTarget;
-
- // Wait a frame or button won't receive focus in Safari.
- window.requestAnimationFrame(() => {
- if (target !== document.activeElement) {
- target.focus();
- }
- });
- }
-
- _updateLabel(label) {
- label = label || this._elements.label;
-
- label.classList.remove(`${CLASSNAME}-label`, `${ACTION_CLASSNAME}-label`);
-
- if (this._variant !== variant._CUSTOM) {
- if (this._variant === variant.ACTION || this._variant === variant.QUIET_ACTION) {
- label.classList.add(`${ACTION_CLASSNAME}-label`);
- } else {
- label.classList.add(`${CLASSNAME}-label`);
- }
- }
- }
-
- /** @private */
- get _contentZoneTagName() {
- return Object.keys(this._contentZones)[0];
- }
-
- get _contentZones() {
- return {'coral-button-label': 'label'};
- }
-
- /**
- Returns {@link BaseButton} sizes.
-
- @return {ButtonSizeEnum}
- */
- static get size() {
- return size;
- }
-
- /**
- Returns {@link BaseButton} variants.
-
- @return {ButtonVariantEnum}
- */
- static get variant() {
- return variant;
- }
-
- /**
- Returns {@link BaseButton} icon positions.
-
- @return {ButtonIconPositionEnum}
- */
- static get iconPosition() {
- return iconPosition;
- }
-
- /**
- Returns {@link BaseButton} icon sizes.
-
- @return {ButtonIconSizeEnum}
- */
- static get iconSize() {
- return iconSize;
- }
-
- static get _attributePropertyMap() {
- return commons.extend(super._attributePropertyMap, {
- iconposition: 'iconPosition',
- iconsize: 'iconSize',
- iconautoarialabel: 'iconAutoAriaLabel'
- });
- }
-
- /** @ignore */
- static get observedAttributes() {
- return super.observedAttributes.concat([
- 'iconposition',
- 'iconsize',
- 'icon',
- 'iconautoarialabel',
- 'size',
- 'selected',
- 'block',
- 'variant',
- 'value'
- ]);
- }
-
- /** @ignore */
- render() {
- super.render();
-
- // Default reflected attributes
- if (!this._variant) {
- this.variant = variant.DEFAULT;
- }
- if (!this._size) {
- this.size = size.MEDIUM;
- }
-
- // Create a fragment
- const fragment = document.createDocumentFragment();
-
- const label = this._elements.label;
-
- const contentZoneProvided = label.parentNode;
-
- // Remove it so we can process children
- if (contentZoneProvided) {
- this.removeChild(label);
- }
-
- let iconAdded = false;
- // Process remaining elements as necessary
- while (this.firstChild) {
- const child = this.firstChild;
-
- if (child.nodeName === 'CORAL-ICON') {
- // Don't add duplicated icons
- if (iconAdded) {
- this.removeChild(child);
- } else {
- // Conserve existing icon element to content
- this._elements.icon = child;
- fragment.appendChild(child);
- iconAdded = true;
- }
- }
- // Avoid content zone to be voracious
- else if (contentZoneProvided) {
- fragment.appendChild(child);
- } else {
- // Move anything else into the label
- label.appendChild(child);
- }
- }
-
- // Add the frag to the component
- this.appendChild(fragment);
-
- // Assign the content zones, moving them into place in the process
- this.label = label;
-
- // Make sure the icon is well positioned
- this._updatedIcon = true;
- this._updateIcon(this.icon);
- }
-
- /**
- Triggered when {@link BaseButton#selected} changed.
-
- @typedef {CustomEvent} coral-button:_selectedchanged
-
- @private
- */
-
- /**
- Triggered when {@link BaseButton#value} changed.
-
- @typedef {CustomEvent} coral-button:_valuechanged
-
- @private
- */
- };
-
- export default BaseButton;