coral-spectrum/coral-component-tooltip/src/scripts/Tooltip.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 {ExtensibleOverlay, Overlay} from '../../../coral-component-overlay';
- import Vent from '@adobe/vent';
- import base from '../templates/base';
- import {commons, transform, validate} from '../../../coral-utils';
- import {Decorator} from '../../../coral-decorator';
-
- const arrowMap = {
- left: 'left',
- right: 'right',
- top: 'top',
- bottom: 'bottom'
- };
-
- const CLASSNAME = '_coral-Tooltip';
-
- const OFFSET = 5;
-
- /**
- Enumeration for {@link Tooltip} variants.
-
- @typedef {Object} TooltipVariantEnum
-
- @property {String} DEFAULT
- A default tooltip that provides additional information.
- @property {String} INFO
- A tooltip that informs the user of non-critical information.
- @property {String} SUCCESS
- A tooltip that indicates an operation was successful.
- @property {String} ERROR
- A tooltip that indicates an error has occurred.
- @property {String} WARNING
- Not supported. Falls back to DEFAULT.
- @property {String} INSPECT
- Not supported. Falls back to DEFAULT.
- */
- const variant = {
- DEFAULT: 'default',
- INFO: 'info',
- SUCCESS: 'success',
- ERROR: 'error',
- WARNING: 'warning',
- INSPECT: 'inspect'
- };
-
- // A string of all possible variant classnames
- const ALL_VARIANT_CLASSES = [];
- for (const variantName in variant) {
- ALL_VARIANT_CLASSES.push(`${CLASSNAME}--${variant[variantName]}`);
- }
-
- // A string of all position placement classnames
- const ALL_PLACEMENT_CLASSES = [];
-
- // A map of lowercase directions to their corresponding classname
- const placementClassMap = {};
- for (const key in Overlay.placement) {
- const direction = Overlay.placement[key];
- const placementClass = `${CLASSNAME}--${arrowMap[direction]}`;
-
- // Store in map
- placementClassMap[direction] = placementClass;
-
- // Store in list
- ALL_PLACEMENT_CLASSES.push(placementClass);
- }
-
- /**
- @class Coral.Tooltip
- @classdesc A Tooltip component that can be attached to any element and may be displayed immediately or on hovering the
- target element.
- @htmltag coral-tooltip
- @extends {Overlay}
- */
- const Tooltip = Decorator(class extends ExtensibleOverlay {
- /** @ignore */
- constructor() {
- super();
-
- // Override defaults
- this._lengthOffset = OFFSET;
- this._overlayAnimationTime = Overlay.FADETIME;
- this._focusOnShow = Overlay.focusOnShow.OFF;
-
- // Fetch or create the content zone element
- this._elements = commons.extend(this._elements, {
- content: this.querySelector('coral-tooltip-content') || document.createElement('coral-tooltip-content')
- });
-
- // Generate template
- base.call(this._elements);
-
- // Used for events
- this._id = commons.getUID();
- this._delegateEvents({
- 'coral-overlay:positioned': '_onPositioned',
- 'coral-overlay:_animate': '_onAnimate',
- 'mouseenter': '_onMouseEnter',
- 'mouseleave': '_onMouseLeave'
- });
- }
-
- /**
- The variant of tooltip. See {@link TooltipVariantEnum}.
-
- @type {String}
- @default TooltipVariantEnum.DEFAULT
- @htmlattribute variant
- @htmlattributereflected
- */
- get variant() {
- return this._variant || variant.DEFAULT;
- }
-
- set variant(value) {
- value = transform.string(value).toLowerCase();
- this._variant = validate.enumeration(variant)(value) && value || variant.DEFAULT;
- this._reflectAttribute('variant', this._variant);
-
- this.classList.remove(...ALL_VARIANT_CLASSES);
- this.classList.add(`${CLASSNAME}--${this._variant}`);
- }
-
- /**
- The amount of time in miliseconds to wait before showing the tooltip when the target is interacted with.
-
- @type {Number}
- @default 500
- @htmlattribute delay
- */
- get delay() {
- return typeof this._delay === 'number' ? this._delay : 500;
- }
-
- set delay(value) {
- this._delay = transform.number(value);
- }
-
- /**
- The Tooltip content element.
-
- @type {TooltipContent}
- @contentzone
- */
- get content() {
- return this._getContentZone(this._elements.content);
- }
-
- set content(value) {
- this._setContentZone('content', value, {
- handle: 'content',
- tagName: 'coral-tooltip-content',
- insert: function (content) {
- content.classList.add(`${CLASSNAME}-label`);
- this.appendChild(content);
- }
- });
- }
-
- /**
- Inherited from {@link Overlay#open}.
- */
- get open() {
- return super.open;
- }
-
- set open(value) {
- super.open = value;
-
- if (!this.open) {
- // Stop previous show operations from happening
- this._cancelShow();
- }
- }
-
- /**
- Inherited from {@link Overlay#target}.
- */
- get target() {
- return super.target;
- }
-
- set target(value) {
- super.target = value;
-
- const target = this._getTarget(value);
-
- if (target) {
- this._elements.tip.hidden = false;
-
- if (this.interaction === this.constructor.interaction.ON) {
- // Add listeners to the target
- this._addTargetListeners(target);
- }
- } else {
- this._elements.tip.hidden = true;
- }
- }
-
- /**
- Inherited from {@link Overlay#interaction}.
- */
- get interaction() {
- return super.interaction;
- }
-
- set interaction(value) {
- super.interaction = value;
-
- const target = this._getTarget();
-
- if (target) {
- if (value === this.constructor.interaction.ON) {
- this._addTargetListeners(target);
- } else {
- this._removeTargetListeners(target);
- }
- }
- }
-
- /** @ignore */
- _onPositioned(event) {
- // Set arrow placement
- this.classList.remove(...ALL_PLACEMENT_CLASSES);
- this.classList.add(placementClassMap[event.detail.placement]);
- }
-
- _onAnimate() {
- // popper attribute
- const popperPlacement = this.getAttribute('x-placement');
-
- // popper takes care of setting left, top to 0 on positioning
- if (popperPlacement === 'left') {
- this.style.left = '8px';
- } else if (popperPlacement === 'top') {
- this.style.top = '8px';
- } else if (popperPlacement === 'right') {
- this.style.left = '-8px';
- } else if (popperPlacement === 'bottom') {
- this.style.top = '-8px';
- }
- }
-
- _onMouseEnter() {
- if (this.interaction === this.constructor.interaction.ON && this.open) {
- // on automatic interaction and tooltip still open and mouse enters the tooltip, cancel hide.
- this._cancelHide();
- }
- }
-
- _onMouseLeave() {
- if (this.interaction === this.constructor.interaction.ON) {
- // on automatic interaction and mouse leave tooltip and execute same flow when mouse leaves target.
- this._startHide();
- }
- }
-
- /** @ignore */
- _handleFocusOut() {
- // The item that should have focus will get it on the next frame
- window.requestAnimationFrame(() => {
- const targetIsFocused = document.activeElement === this._getTarget();
-
- if (!targetIsFocused) {
- this._cancelShow();
- this.open = false;
- }
- });
- }
-
- /** @ignore */
- _cancelShow() {
- window.clearTimeout(this._showTimeout);
- }
-
- /** @ignore */
- _cancelHide() {
- window.clearTimeout(this._hideTimeout);
- }
-
- /** @ignore */
- _startHide() {
- if (this.delay === 0) {
- // Hide immediately
- this._handleFocusOut();
- } else {
- this._hideTimeout = window.setTimeout(() => {
- this._handleFocusOut();
- }, this.delay);
- }
- }
-
- /** @ignore */
- _addTargetListeners(target) {
- // Make sure we don't add listeners twice to the same element for this particular tooltip
- if (target[`_hasTooltipListeners${this._id}`]) {
- return;
- }
- target[`_hasTooltipListeners${this._id}`] = true;
-
- // Remove listeners from the old target
- if (this._oldTarget) {
- const oldTarget = this._getTarget(this._oldTarget);
- if (oldTarget) {
- this._removeTargetListeners(oldTarget);
- }
- }
-
- // Store the current target value
- this._oldTarget = target;
-
- // Use Vent to bind events on the target
- this._targetEvents = new Vent(target);
-
- this._targetEvents.on(`mouseenter.Tooltip${this._id}`, this._handleOpenTooltip.bind(this));
- this._targetEvents.on(`focusin.Tooltip${this._id}`, this._handleOpenTooltip.bind(this));
-
- this._targetEvents.on(`mouseleave.Tooltip${this._id}`, () => {
- if (this.interaction === this.constructor.interaction.ON) {
- this._startHide();
- }
- });
-
- this._targetEvents.on(`focusout.Tooltip${this._id}`, () => {
- if (this.interaction === this.constructor.interaction.ON) {
- this._handleFocusOut();
- }
- });
- }
-
- _handleOpenTooltip() {
- // Don't let the tooltip hide
- this._cancelHide();
-
- if (!this.open) {
- this._cancelShow();
-
- if (this.delay === 0) {
- // Show immediately
- this.show();
- } else {
- this._showTimeout = window.setTimeout(() => {
- this.show();
- }, this.delay);
- }
- }
- }
-
- /** @ignore */
- _removeTargetListeners(target) {
- // Remove listeners for this tooltip and mark that the element doesn't have them
- // Use the ID so we can support multiple tooltips on the same element
- if (this._targetEvents) {
- this._targetEvents.off(`.Tooltip${this._id}`);
- }
- target[`_hasTooltipListeners${this._id}`] = false;
- }
-
- get _contentZones() {
- return {'coral-tooltip-content': 'content'};
- }
-
- /**
- Returns {@link Tooltip} variants.
-
- @return {TooltipVariantEnum}
- */
- static get variant() {
- return variant;
- }
-
- /** @ignore */
- static get observedAttributes() {
- return super.observedAttributes.concat(['variant', 'delay']);
- }
-
- /** @ignore */
- render() {
- super.render();
-
- this.classList.add(CLASSNAME);
-
- // ARIA
- this.setAttribute('role', 'tooltip');
- // Let the tooltip be focusable
- // We'll marshall focus around when its focused
- this.setAttribute('tabindex', '-1');
-
- // Default reflected attributes
- if (!this._variant) {
- this.variant = variant.DEFAULT;
- }
-
- // Support cloneNode
- const tip = this.querySelector('._coral-Tooltip-tip');
- if (tip) {
- tip.remove();
- }
-
- const content = this._elements.content;
-
- // Move the content into the content zone if none specified
- if (!content.parentNode) {
- while (this.firstChild) {
- content.appendChild(this.firstChild);
- }
- }
-
- // Append template
- this.appendChild(this._elements.tip);
-
- // Assign the content zone so the insert function will be called
- this.content = content;
- }
- });
-
- export default Tooltip;