From 3b804ceeb2f005c328c08fd610e42882c6b78658 Mon Sep 17 00:00:00 2001 From: Christian Otto Date: Fri, 24 Jan 2020 13:52:06 +0100 Subject: [PATCH 1/5] feat(og-tooltip): add tooltip component with examples --- package.json | 2 + src/components.d.ts | 131 ++ src/components/og-tooltip/og-tooltip.e2e.ts | 11 + src/components/og-tooltip/og-tooltip.scss | 227 ++++ src/components/og-tooltip/og-tooltip.tsx | 1201 +++++++++++++++++ src/examples/Tooltip.html | 143 ++ src/index.html | 4 +- src/styles/themes/dark.theme.scss | 5 + src/styles/themes/light.theme.scss | 5 + src/styles/variables/color.variable.scss | 6 + src/styles/variables/sizing.variable.scss | 9 +- src/utils/add-class.ts | 7 + src/utils/custom-event.ts | 46 + .../get-transition-duration-from-element.ts | 26 + src/utils/has-class.ts | 7 + src/utils/remove-class.ts | 7 + src/utils/uuidv4.ts | 7 + 17 files changed, 1841 insertions(+), 3 deletions(-) create mode 100644 src/components/og-tooltip/og-tooltip.e2e.ts create mode 100644 src/components/og-tooltip/og-tooltip.scss create mode 100644 src/components/og-tooltip/og-tooltip.tsx create mode 100644 src/examples/Tooltip.html create mode 100644 src/utils/add-class.ts create mode 100644 src/utils/custom-event.ts create mode 100644 src/utils/get-transition-duration-from-element.ts create mode 100644 src/utils/has-class.ts create mode 100644 src/utils/remove-class.ts create mode 100644 src/utils/uuidv4.ts diff --git a/package.json b/package.json index cb92289e..3831f4e7 100644 --- a/package.json +++ b/package.json @@ -59,9 +59,11 @@ "glob": "^7.1.4", "jest": "24.8.0", "jest-cli": "24.8.0", + "lodash": "^4.17.15", "moment": "^2.24.0", "node-sass": "^4.12.0", "pre-commit": "^1.2.2", + "popper.js": "^1.16.0", "puppeteer": "1.17.0", "systemjs": "^3.1.6", "tabulator-tables": "^4.2.7", diff --git a/src/components.d.ts b/src/components.d.ts index c60019fb..f8d14294 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -14,6 +14,9 @@ import { import { OgDatatableConfig, } from './components/og-datatable/interfaces/og-datatable-column-def'; +import { + OgTooltipConfig, +} from './components/og-tooltip/og-tooltip'; export namespace Components { interface OgButton { @@ -462,6 +465,68 @@ export namespace Components { */ 'value': boolean; } + interface OgTooltip { + /** + * The config property allows for overriding the tooltip´s default configuration. + */ + 'config': Partial; + /** + * Name of the event that fires immediately when a tooltip is first disabled. If .defaultPrevented() is used on the event then the tooltip will not be disabled. + */ + 'disableEventName': string; + /** + * When set to `true` the tooltip will be disabled. + */ + 'disabled': boolean; + /** + * Name of the event that is fired when a tooltip has finished being disabled. + */ + 'disabledEventName': string; + /** + * Name of the event that fires immediately when a tooltip is first enabled. If .defaultPrevented() is used on the event then the tooltip will not be enabled. + */ + 'enableEventName': string; + /** + * Name of the event that is fired when a tooltip has finished being enabled. + */ + 'enabledEventName': string; + /** + * Name of the event that is fired when the tooltip has finished being hidden from the user (will wait for CSS transitions to complete). + */ + 'hiddenEventName': string; + /** + * Name of the event that event is fired immediately when the hide instance method has been called. + */ + 'hideEventName': string; + /** + * Name of the event that is fired after the `og.tooltip.show` event when the tooltip template has been added to the DOM. + */ + 'insertedEventName': string; + /** + * It will override any other `title` settings and use this for the tooltip title. The value is watched so that it will update dynamically. + */ + 'ogTitle': string; + /** + * Name of the event that fires immediately when the show instance method is called. + */ + 'showEventName': string; + /** + * When set to `true`, the `og-tooltip` will toggle open. + */ + 'showTooltip': boolean; + /** + * Name of the event that is fired when the tooltip has been made visible to the user (will wait for CSS transitions to complete). + */ + 'shownEventName': string; + /** + * When tabindex is set to a negative value (smaller than `0`), the tooltip will work on hover, but it will not appear via keyboard raised focus. + */ + 'tabindex': string | number; + /** + * `.tooltip(Partial)` attaches a tooltip handler to a element. `.tooltip('show')` reveals an element´s tooltip. `.tooltip('hide')` hides an element´s tooltip. `.tooltip('toggle')` toggles an element´s tooltip. `.tooltip('enable')` gives an element’s tooltip the ability to be shown. `.tooltip('disable')` removes the ability for an element´s tooltip to be shown. The tooltip will only be able to be shown if it is re-enabled.. `.tooltip('toggleEnabled')` toggles the ability for an element´s tooltip to be shown or hidden. `.tooltip('update')` updates the position of an element´s tooltip. + */ + 'tooltip': (tooltipOptions: string | Partial) => Promise; + } } declare global { @@ -622,6 +687,12 @@ declare global { prototype: HTMLOgToggleSwitchElement; new (): HTMLOgToggleSwitchElement; }; + + interface HTMLOgTooltipElement extends Components.OgTooltip, HTMLStencilElement {} + var HTMLOgTooltipElement: { + prototype: HTMLOgTooltipElement; + new (): HTMLOgTooltipElement; + }; interface HTMLElementTagNameMap { 'og-button': HTMLOgButtonElement; 'og-calendar': HTMLOgCalendarElement; @@ -649,6 +720,7 @@ declare global { 'og-text-input': HTMLOgTextInputElement; 'og-textarea': HTMLOgTextareaElement; 'og-toggle-switch': HTMLOgToggleSwitchElement; + 'og-tooltip': HTMLOgTooltipElement; } } @@ -1198,6 +1270,64 @@ declare namespace LocalJSX { */ 'value'?: boolean; } + interface OgTooltip extends JSXBase.HTMLAttributes { + /** + * The config property allows for overriding the tooltip´s default configuration. + */ + 'config'?: Partial; + /** + * Name of the event that fires immediately when a tooltip is first disabled. If .defaultPrevented() is used on the event then the tooltip will not be disabled. + */ + 'disableEventName'?: string; + /** + * When set to `true` the tooltip will be disabled. + */ + 'disabled'?: boolean; + /** + * Name of the event that is fired when a tooltip has finished being disabled. + */ + 'disabledEventName'?: string; + /** + * Name of the event that fires immediately when a tooltip is first enabled. If .defaultPrevented() is used on the event then the tooltip will not be enabled. + */ + 'enableEventName'?: string; + /** + * Name of the event that is fired when a tooltip has finished being enabled. + */ + 'enabledEventName'?: string; + /** + * Name of the event that is fired when the tooltip has finished being hidden from the user (will wait for CSS transitions to complete). + */ + 'hiddenEventName'?: string; + /** + * Name of the event that event is fired immediately when the hide instance method has been called. + */ + 'hideEventName'?: string; + /** + * Name of the event that is fired after the `og.tooltip.show` event when the tooltip template has been added to the DOM. + */ + 'insertedEventName'?: string; + /** + * It will override any other `title` settings and use this for the tooltip title. The value is watched so that it will update dynamically. + */ + 'ogTitle'?: string; + /** + * Name of the event that fires immediately when the show instance method is called. + */ + 'showEventName'?: string; + /** + * When set to `true`, the `og-tooltip` will toggle open. + */ + 'showTooltip'?: boolean; + /** + * Name of the event that is fired when the tooltip has been made visible to the user (will wait for CSS transitions to complete). + */ + 'shownEventName'?: string; + /** + * When tabindex is set to a negative value (smaller than `0`), the tooltip will work on hover, but it will not appear via keyboard raised focus. + */ + 'tabindex'?: string | number; + } interface IntrinsicElements { 'og-button': OgButton; @@ -1226,6 +1356,7 @@ declare namespace LocalJSX { 'og-text-input': OgTextInput; 'og-textarea': OgTextarea; 'og-toggle-switch': OgToggleSwitch; + 'og-tooltip': OgTooltip; } } diff --git a/src/components/og-tooltip/og-tooltip.e2e.ts b/src/components/og-tooltip/og-tooltip.e2e.ts new file mode 100644 index 00000000..c0f53382 --- /dev/null +++ b/src/components/og-tooltip/og-tooltip.e2e.ts @@ -0,0 +1,11 @@ +import { newE2EPage } from '@stencil/core/testing'; + +describe('og-tooltip', () => { + it('renders', async () => { + const page = await newE2EPage(); + + await page.setContent(''); + const element = await page.find('og-tooltip'); + expect(element).toHaveClass('hydrated'); + }); +}); diff --git a/src/components/og-tooltip/og-tooltip.scss b/src/components/og-tooltip/og-tooltip.scss new file mode 100644 index 00000000..3736baa6 --- /dev/null +++ b/src/components/og-tooltip/og-tooltip.scss @@ -0,0 +1,227 @@ +/** + * @prop --og-tooltip-BackgroundColor: Overall background color of the tooltip + * @prop --og-tooltip-BorderRadius: Border radius of the tooltip + * @prop --og-tooltip-Color: Text color within the tooltip + * @prop --og-tooltip-DropShadow: Drop-shadow filter of the tooltip + * @prop --og-tooltip-FontSize: Font size of the tooltip´s text + * @prop --og-tooltip-LineHeight: Line height of the tooltip´s text + * @prop --og-tooltip-Margin: Margin around the tooltip + * @prop --og-tooltip-Opacity: Opacity of the visible tooltip + * @prop --og-tooltip-Padding: Padding within the tooltip + * @prop --og-tooltip-Transition: Transition to apply when tooltip uses animation + * @prop --og-tooltip-ZIndex: Z-index value of the tooltip + * @prop --og-tooltip-arrow-Height: Height of the arrow element when applied on top or bottom + * @prop --og-tooltip-arrow-Width: Width of the arrow element when applied on top or bottom + */ + +// (this is the tooltip´s target element) +// ----------------------------------------------------------------------------- +:host, +og-tooltip { + display: inline-block; +} + +// Tooltip element (the bubble visually attached to it´s target) +// ----------------------------------------------------------------------------- +.og-tooltip { + // Interal vars trying to map external (:root) vars and provide a fallback + --_og-tooltip-BackgroundColor: var(--og-tooltip-BackgroundColor, var(--OG-TOOLTIP-BACKGROUND-COLOR, #{$og-tooltip-background-color})); + --_og-tooltip-BorderRadius: var(--og-tooltip-BorderRadius, #{$og-border-radius}); + --_og-tooltip-Color: var(--og-tooltip-Color, var(--OG-FONT-COLOR, inherit)); + --_og-tooltip-DropShadow: var(--og-tooltip-DropShadow, #{$og-tooltip-drop-shadow}); + --_og-tooltip-FontSize: var(--og-tooltip-FontSize, var(--OG-FONT-SIZE--S, 12px)); + --_og-tooltip-LineHeight: var(--og-tooltip-LineHeight, 1.5); + --_og-tooltip-Margin: var(--og-tooltip-Margin, 0); + --_og-tooltip-Opacity: var(--og-tooltip-Opacity, 1); + --_og-tooltip-Padding: var(--og-tooltip-Padding, 6px 8px); + --_og-tooltip-Transition: var(--og-tooltip-Transition, opacity 0.15s linear); + --_og-tooltip-ZIndex: var(--og-tooltip-ZIndex, #{$og-z-index-tooltip}); + --_og-tooltip-arrow-Height: var(--og-tooltip-arrow-Height, 4px); + --_og-tooltip-arrow-Width: var(--og-tooltip-arrow-Width, 10px); + + // Internal vars NOT meant to be overridden externally + --_og-tooltip-FlexDirection: column; + --_og-tooltip-MaxWidth: 75vw; + --_og-tooltip-MaxHeight: 50vw; + --_og-tooltip-arrow-Order: 0; + --_og-tooltip-arrow-BorderTopColor: transparent; + --_og-tooltip-arrow-BorderRightColor: transparent; + --_og-tooltip-arrow-BorderBottomColor: transparent; + --_og-tooltip-arrow-BorderLeftColor: transparent; + --_og-tooltip-arrow-BorderWidth: 0; + --_og-tooltip-arrow-MarginTop: auto; + --_og-tooltip-arrow-MarginRight: auto; + --_og-tooltip-arrow-MarginBottom: auto; + --_og-tooltip-arrow-MarginLeft: auto; + + position: absolute; + z-index: var(--_og-tooltip-ZIndex); + display: flex; + flex-direction: var(--_og-tooltip-FlexDirection); + font-size: var(--_og-tooltip-FontSize); + line-height: var(--_og-tooltip-LineHeight); + margin: var(--_og-tooltip-Margin); + filter: var(--_og-tooltip-DropShadow); + opacity: 0; + + // tame the dimensions of the tooltip (e.g. viewport-width/height values) + max-width: var(--_og-tooltip-MaxWidth); + max-height: var(--_og-tooltip-MaxHeight); + + // show + &.show { + opacity: var(--_og-tooltip-Opacity); + } + + // fade + &.fade { + transition: var(--_og-tooltip-Transition); + + &:not(.show) { + opacity: 0; + } + } + + // top & bottom + &[class*="og-tooltip-top"], + &[class*="og-tooltip-bottom"], + &[class*="og-tooltip-auto"][x-placement^="top"], + &[class*="og-tooltip-auto"][x-placement^="bottom"] { + // start + &[class*="-start"], + &[x-placement$="-start"] { + --_og-tooltip-arrow-MarginLeft: var(--_og-tooltip-arrow-Width); + } + + // end + &[class*="-end"], + &[x-placement$="-end"] { + --_og-tooltip-arrow-MarginRight: var(--_og-tooltip-arrow-Width); + } + } + + // left & right + &[class*="og-tooltip-right"], + &[class*="og-tooltip-left"], + &[class*="og-tooltip-auto"][x-placement^="right"], + &[class*="og-tooltip-auto"][x-placement^="left"] { + --_og-tooltip-FlexDirection: row; + --_og-tooltip-arrow-Width: var(--og-tooltip-arrow-Height, 4px); + --_og-tooltip-arrow-Height: var(--og-tooltip-arrow-Width, 10px); + + // start + &[class*="-start"], + &[x-placement$="-start"] { + --_og-tooltip-arrow-MarginTop: var(--_og-tooltip-arrow-Width); + } + + // end + &[class*="-end"], + &[x-placement$="-end"] { + --_og-tooltip-arrow-MarginBottom: var(--_og-tooltip-arrow-Width); + } + } + + // top & left + &[class*="og-tooltip-top"], + &[class*="og-tooltip-left"], + &[class*="og-tooltip-auto"][x-placement^="top"], + &[class*="og-tooltip-auto"][x-placement^="left"] { + // make the arrow appear after the content + --_og-tooltip-arrow-Order: 1; + } + + // top + &[class*="og-tooltip-top"], + &[class*="og-tooltip-auto"][x-placement^="top"] { + --_og-tooltip-arrow-BorderTopColor: var(--_og-tooltip-BackgroundColor); + --_og-tooltip-arrow-BorderWidth: var(--_og-tooltip-arrow-Height) calc(var(--_og-tooltip-arrow-Width) / 2) 0; + } + + // right + &[class*="og-tooltip-right"], + &[class*="og-tooltip-auto"][x-placement^="right"] { + --_og-tooltip-arrow-BorderRightColor: var(--_og-tooltip-BackgroundColor); + --_og-tooltip-arrow-BorderWidth: calc(var(--_og-tooltip-arrow-Height) / 2) var(--_og-tooltip-arrow-Width) calc(var(--_og-tooltip-arrow-Height) / 2) 0; + } + + // bottom + &[class*="og-tooltip-bottom"], + &[class*="og-tooltip-auto"][x-placement^="bottom"] { + --_og-tooltip-arrow-BorderBottomColor: var(--_og-tooltip-BackgroundColor); + --_og-tooltip-arrow-BorderWidth: 0 calc(var(--_og-tooltip-arrow-Width) / 2) var(--_og-tooltip-arrow-Height); + } + + // left + &[class*="og-tooltip-left"], + &[class*="og-tooltip-auto"][x-placement^="left"] { + --_og-tooltip-arrow-BorderLeftColor: var(--_og-tooltip-BackgroundColor); + --_og-tooltip-arrow-BorderWidth: calc(var(--_og-tooltip-arrow-Height) / 2) 0 calc(var(--_og-tooltip-arrow-Height) / 2) var(--_og-tooltip-arrow-Width); + } + +} + +// Tooltip content area +.og-tooltip__inner { + background-color: var(--_og-tooltip-BackgroundColor); + border-radius: var(--_og-tooltip-BorderRadius); + color: var(--_og-tooltip-Color); + padding: var(--_og-tooltip-Padding); + overflow: hidden; + word-break: break-all; + // Non standard for WebKit + word-break: break-word; +} + +// Tooltip arrow +.og-tooltip__arrow { + order: var(--_og-tooltip-arrow-Order); + display: block; + flex: none; + width: var(--_og-tooltip-arrow-Width); + height: var(--_og-tooltip-arrow-Height); + margin-top: var(--_og-tooltip-arrow-MarginTop); + margin-right: var(--_og-tooltip-arrow-MarginRight); + margin-bottom: var(--_og-tooltip-arrow-MarginBottom); + margin-left: var(--_og-tooltip-arrow-MarginLeft); + + &::before { + position: absolute; + content: ""; + border-top-color: var(--_og-tooltip-arrow-BorderTopColor); + border-right-color: var(--_og-tooltip-arrow-BorderRightColor); + border-bottom-color: var(--_og-tooltip-arrow-BorderBottomColor); + border-left-color: var(--_og-tooltip-arrow-BorderLeftColor); + border-width: var(--_og-tooltip-arrow-BorderWidth); + border-style: solid; + } +} + +// detect if the user has requested that the system minimize the amount of +// animation or motion it uses +@media (prefers-reduced-motion: reduce) { + .og-tooltip.fade { + transition: none; + } +} + +// make sure that the dimensions of the tooltip have a relation to the +// (increasing) viewport size +@media (min-width: 768px) { + .og-tooltip { + --_og-tooltip-MaxWidth: 50vw; + --_og-tooltip-MaxHeight: 50vw; + } +} + +@media (min-width: 1200px) { + .og-tooltip { + --_og-tooltip-MaxWidth: 33vw; + } +} + +@media (min-width: 1800px) { + .og-tooltip { + --_og-tooltip-MaxWidth: 25vw; + } +} diff --git a/src/components/og-tooltip/og-tooltip.tsx b/src/components/og-tooltip/og-tooltip.tsx new file mode 100644 index 00000000..1713c814 --- /dev/null +++ b/src/components/og-tooltip/og-tooltip.tsx @@ -0,0 +1,1201 @@ +/** + * ORGENIC-UI + * @license MIT + * See LICENSE file at https://github.com/orgenic/orgenic-ui/blob/master/LICENSE + **/ + +// Stencil core +import { + Component, + Element, + h, + Method, + Prop, + State, + Watch +} from '@stencil/core'; + +// 3rd party libraries +import _ from 'lodash'; +import Popper from 'popper.js'; + +// ORGENIC-UI helper classes +import addClass from '../../utils/add-class'; +import getTransitionDurationFromElement from '../../utils/get-transition-duration-from-element'; +import hasClass from '../../utils/has-class'; +import ogCustomEvent from '../../utils/custom-event'; +import removeClass from '../../utils/remove-class'; +import uuidv4 from '../../utils/uuidv4'; + +/** + * This interface describes the configuration of tooltip´s delay time for its + * appearance and disappearance. + * + * @export + * @interface OgTooltipDelay + */ +export interface OgTooltipDelay { + /** + * The delay value to show the tooltip (in ms). + * + * @type {number} + * @memberof OgTooltipDelay + */ + show: number; + + /** + * The delay value to hide the tooltip (in ms). + * + * @type {number} + * @memberof OgTooltipDelay + */ + hide: number; +} + +/** + * This interface describes the configuration of the tooltip. + * + * @export + * @interface OgTooltipConfig + */ +export interface OgTooltipConfig { + /** + * Whether or not to apply a CSS fade transition to the tooltip. + * + * @type {boolean} + * @memberof OgTooltipConfig + */ + animation: boolean; + + /** + * Overflow constraint boundary of the tooltip. Accepts the values of + * `viewport`, `window` or `scrollParent`. For more information refer to + * Popper.js's preventOverflow docs + * https://popper.js.org/docs/v1/#modifiers..preventOverflow + * + * @type {Popper.Boundary | Element} + * @memberof OgTooltipConfig + */ + boundary: Popper.Boundary | Element; + + /** + * Appends the tooltip to a specific element. Example: `container: 'body'`. + * This option is particularly useful in that it allows you to position the + * tooltip in the flow of the document near the triggering element - which + * will prevent the tooltip from floating away from the triggering element + * during a window resize. + * + * @type {any} + * @memberof OgTooltipConfig + */ + container: any; + + /** + * Delay showing and hiding the tooltip (ms) - does not apply to `manual` + * trigger type. If a number is supplied, delay is applied to both `hide` and + * `show`. Object structure is: `delay: { "show": 500, "hide": 100 }` + * + * @type {number | OgTooltipDelay} + * @memberof OgTooltipConfig + */ + delay: number | OgTooltipDelay; + + /** + * Delay removing the tooltip element from the DOM (ms). + * + * @type {number} + * @memberof OgTooltipConfig + */ + disposeTimeToWait: number; + + /** + * Allow HTML in the tooltip. If `true`, HTML tags in the tooltip's title will + * be rendered in the tooltip. If `false`, .innerText method will be used to + * insert content into the DOM. Use text if you're worried about XSS attacks. + * + * @type {boolean} + * @memberof OgTooltipConfig + */ + html: boolean; + + /** + * Offset of the tooltip relative to its target. For more information refer to + * Popper.js's offset docs https://popper.js.org/docs/v1/#modifiers..offset + * + * @type {number} + * @memberof OgTooltipConfig + */ + offset: number; + + /** + * Allow to specify which position Popper will use on fallback. For more + * information refer to Popper.js's behavior doc + * https://popper.js.org/docs/v1/#modifiers..flip.behavior + * + * @type {'flip' | 'clockwise' | 'counterclockwise' | Popper.Position[]} + * @memberof OgTooltipConfig + */ + fallbackPlacement: 'flip' | 'clockwise' | 'counterclockwise' | Popper.Position[]; + + /** + * How to position the tooltip. `auto|top|bottom|left|right(-start/-end)`. + * When `auto` is specified, it will dynamically position the tooltip. When a + * function is used to determine the placement, it is called with the tooltip + * DOM node as its first argument and the triggering element DOM node as its + * second argument. The this context is set to the tooltip instance. + * + * @type {Popper.Placement | Function} + * @memberof OgTooltipConfig + */ + placement: Popper.Placement | Function; + + /** + * Base HTML to use when creating the tooltip. The tooltip's title will be + * injected into the `.og-tooltip__inner`. `og-tooltip__arrow` will become the + * tooltip's arrow. The outermost wrapper element should have the + * `.og-tooltip` class and `role="tooltip"`. + * + * @type {string} + * @memberof OgTooltipConfig + */ + template: string; + + /** + * Default title value if `title` attribute isn't present. If a function is + * given, it will be called with its reference set to the element that the + * tooltip is attached to. + * + * @type {string | Element | Function} + * @memberof OgTooltipConfig + */ + title: string | Element | Function; + + /** + * How the tooltip is triggered (e.g. `hover`, `focus`). Accepts a whitespace + * separated list of strings. + * + * @type {string} + * @memberof OgTooltipConfig + */ + trigger: string; +} + +@Component({ + tag: 'og-tooltip', + styleUrl: 'og-tooltip.scss', + shadow: false +}) + +export class OgTooltip { + @Element() public tooltipEl: HTMLElement; + + /** + * Name of the event that fires immediately when the show instance method is + * called. + */ + @Prop({ mutable: true }) public showEventName: string = 'og.tooltip.show'; + + /** + * Name of the event that is fired when the tooltip has been made visible to + * the user (will wait for CSS transitions to complete). + */ + @Prop({ mutable: true }) public shownEventName: string = 'og.tooltip.shown'; + + /** + * Name of the event that event is fired immediately when the hide instance + * method has been called. + */ + @Prop({ mutable: true }) public hideEventName: string = 'og.tooltip.hide'; + + /** + * Name of the event that is fired when the tooltip has finished being hidden + * from the user (will wait for CSS transitions to complete). + */ + @Prop({ mutable: true }) public hiddenEventName: string = 'og.tooltip.hidden'; + + /** + * Name of the event that is fired after the `og.tooltip.show` event when the + * tooltip template has been added to the DOM. + */ + @Prop({ mutable: true }) public insertedEventName: string = 'og.tooltip.inserted'; + + /** + * Name of the event that fires immediately when a tooltip is first enabled. + * If .defaultPrevented() is used on the event then the tooltip will not be + * enabled. + */ + @Prop({ mutable: true }) public enableEventName: string = 'og.tooltip.enable'; + + /** + * Name of the event that is fired when a tooltip has finished being enabled. + */ + @Prop({ mutable: true }) public enabledEventName: string = 'og.tooltip.enabled'; + + /** + * Name of the event that fires immediately when a tooltip is first disabled. + * If .defaultPrevented() is used on the event then the tooltip will not be + * disabled. + */ + @Prop({ mutable: true }) public disableEventName: string = 'og.tooltip.disable'; + + /** + * Name of the event that is fired when a tooltip has finished being disabled. + */ + @Prop({ mutable: true }) public disabledEventName: string = 'og.tooltip.disabled'; + + /** + * The config property allows for overriding the tooltip´s default + * configuration. + */ + @Prop({ mutable: true }) public config: Partial = {}; + + /** + * When set to `true` the tooltip will be disabled. + */ + @Prop({ mutable: true }) public disabled: boolean = false; + + /** + * It will override any other `title` settings and use this for the tooltip + * title. The value is watched so that it will update dynamically. + */ + @Prop({ mutable: true }) public ogTitle: string = ''; + + /** + * When set to `true`, the `og-tooltip` will toggle open. + */ + @Prop({ mutable: true }) public showTooltip: boolean = false; + + /** + * When tabindex is set to a negative value (smaller than `0`), the tooltip + * will work on hover, but it will not appear via keyboard raised focus. + */ + @Prop({ mutable: true, reflectToAttr: true }) public tabindex: string | number = '0'; + + // States + @State() public activeTrigger: any; + @State() public disposeTimeout: any; + @State() public hoverState: 'in' | 'out'; + @State() public isEnabled: boolean; + @State() public popperHandle: Popper; + @State() public showHideTimeout: any; + @State() public tip: HTMLElement; + @State() public tooltipId: string; + + private tooltipDefaultConfig: OgTooltipConfig = { + animation: true, + boundary: 'scrollParent', + container: false, + delay: 0, + disposeTimeToWait: 0, + fallbackPlacement: 'flip', + html: false, + offset: 0, + placement: 'top', + template: '', + title: '', + trigger: 'hover focus' + } + + private tooltipAppliedPopperPlacementClasses: string[] = []; + private tooltipBaseClass: string = 'og-tooltip'; + private tooltipFadeClass: string = 'fade'; + private tooltipShowClass: string = 'show'; + + /** + * `.tooltip(Partial)` attaches a tooltip handler to a + * element. + * `.tooltip('show')` reveals an element´s tooltip. + * `.tooltip('hide')` hides an element´s tooltip. + * `.tooltip('toggle')` toggles an element´s tooltip. + * `.tooltip('enable')` gives an element’s tooltip the ability to be shown. + * `.tooltip('disable')` removes the ability for an element´s tooltip to be + * shown. The tooltip will only be able to be shown if it is re-enabled.. + * `.tooltip('toggleEnabled')` toggles the ability for an element´s tooltip to + * be shown or hidden. + * `.tooltip('update')` updates the position of an element´s tooltip. + */ + @Method() + public async tooltip(tooltipOptions: string | Partial): Promise { + return this.setupMethod(tooltipOptions); + } + + + + // Public methods + // --------------------------------------------------------------------------- + public componentWillLoad() { + if (this.tabindex === '-1') { + this.tabindex = -1; + } + + if (this.disabled) { + this.disableTooltip(); + return; + } + + if (!this.isEnabled) { + this.enableTooltip(); + } + + if (this.showTooltip === true) { + this.setInitialOpenState(); + } + } + + public componentDidUnload() { + this.disableTooltip(); + this.config = {}; + } + + public render(): HTMLElement { + return (); + } + + + + // Watches + // --------------------------------------------------------------------------- + @Watch('ogTitle') + public handleWatchOgTitle(newValue: string) { + if (!this.isEnabled) { + return; + } + + if (this.config.title !== newValue) { + this.config.title = newValue; + } + + if (this.isWithActiveTrigger() || this.tooltipHtmlElementHasClass(this.tooltipShowClass) || this.hoverState === 'in') { + const tooltipElement = this.getTooltipHtmlElement(); + const tooltipContentPlaceholder = tooltipElement.querySelector('.og-tooltip__inner'); + if (tooltipContentPlaceholder) { + this.setElementContent(tooltipContentPlaceholder, this.getTitle()); + if (this.popperHandle && this.popperHandle.scheduleUpdate) { + this.popperHandle.scheduleUpdate(); + } + } + } + } + + @Watch('disabled') + public handleDisabledWatch(newValue: boolean) { + if (this.disabled === newValue && !this.isEnabled === newValue) { + return; + } + + if (newValue === true) { + this.disableTooltip(); + return; + } + + this.enableTooltip(); + } + + @Watch('showTooltip') + public handleShowTooltipWatch(newValue: boolean) { + if (!this.isEnabled) { + return; + } + + if (newValue === true) { + this.enter(); + return; + } + + this.leave(); + } + + + + // Init + // --------------------------------------------------------------------------- + private setInitialOpenState() { + if (!this.isEnabled ) { + return; + } + + const hasAnimation = this.config.animation; + if (hasAnimation) { + this.setConfig({ animation: false }); + } + + // Listen to the defined triggers. + this.tooltipEl.addEventListener(this.shownEventName, () => { + if (_.includes(this.config.trigger, 'hover') && !_.includes(this.config.trigger, 'manual')) { + this.activeTrigger.hover = true; + this.hoverState = 'in'; + } else if (_.includes(this.config.trigger, 'focus') && !_.includes(this.config.trigger, 'manual')) { + this.activeTrigger.focus = true; + } + if (hasAnimation) { + this.setConfig(); + const tipEl = this.tip || this.createTooltipHtmlElement(); + addClass(tipEl, this.tooltipFadeClass); + } + }, { once: true }); + this.enter(); + } + + + + // Event handing + // --------------------------------------------------------------------------- + private handleMouseEnter = (event) => { + this.enter(event); + } + + private handleFocusIn = (event) => { + this.enter(event); + } + + private handleMouseLeave = (event) => { + this.leave(event); + } + + private handleFocusOut = (event) => { + this.leave(event); + } + + private handlePopperOnCreate = (data: Popper.Data) => { + if (data.originalPlacement !== data.placement) { + this.handlePopperPlacementChange(data); + } + } + + private handlePopperOnUpdate = (data: Popper.Data) => { + this.handlePopperPlacementChange(data); + } + + private handlePopperPlacementChange(popperData: Popper.Data) { + const popperInstance = popperData.instance; + this.tip = popperInstance.popper as HTMLElement; + this.cleanTooltipHtmlElementClasses(); + this.addAttachmentClassToTooltipHtmlElement(popperData.placement); + } + + private setListeners() { + const triggers = _.split(_.toLower(this.config.trigger), ' '); + + if (_.includes(triggers, 'manual')) { + // hover and focus events are ignored if `manual` is included. + return; + } + + if (_.includes(triggers, 'hover')) { + this.ogAddEventListener('mouseenter', this.handleMouseEnter); + this.ogAddEventListener('mouseleave', this.handleMouseLeave); + } + if (_.includes(triggers, 'focus')) { + this.ogAddEventListener('focusin', this.handleFocusIn); + this.ogAddEventListener('focusout', this.handleFocusOut); + } + } + + + + // Config + // --------------------------------------------------------------------------- + private setConfig(overrideConfig: Partial = {}) { + this.config = {}; + const config: any = {}; + + if (_.has(overrideConfig, 'animation')) { + config.animation = this.getConfigBoolean(overrideConfig.animation); + } else if (_.has(this.tooltipEl.dataset, 'animation')) { + config.animation = this.getConfigBoolean(this.tooltipEl.dataset.animation); + } else { + config.animation = this.tooltipDefaultConfig.animation; + } + + if (_.has(overrideConfig, 'trigger')) { + config.trigger = overrideConfig.trigger; + } else if (_.has(this.tooltipEl.dataset, 'trigger')) { + config.trigger = this.tooltipEl.dataset.trigger; + } else { + config.trigger = this.tooltipDefaultConfig.trigger; + } + + const titleAttribute = this.tooltipEl.getAttribute('title'); + let newConfigTitle; + if (_.size(this.ogTitle) > 0) { + newConfigTitle = this.ogTitle; + } else if (titleAttribute) { + newConfigTitle = titleAttribute; + } else if (_.has(overrideConfig, 'title')) { + if (typeof overrideConfig.title === 'object' && overrideConfig.title.nodeValue) { + newConfigTitle = overrideConfig.title.nodeValue; + } else { + newConfigTitle = overrideConfig.title; + } + } else if (_.has(this.tooltipEl.dataset, 'title')) { + newConfigTitle = this.tooltipEl.dataset.title; + } else { + newConfigTitle = this.tooltipDefaultConfig.title; + } + if (_.isNumber(config.title)) { + newConfigTitle = _.toString(config.title); + } + config.title = newConfigTitle; + + let newConfigDelay; + if (_.has(overrideConfig, 'delay')) { + newConfigDelay = overrideConfig.delay; + } else if (_.has(this.tooltipEl.dataset, 'delay')) { + newConfigDelay = this.tooltipEl.dataset.delay; + } + if (_.isInteger(newConfigDelay)) { + config.delay = { + show: newConfigDelay, + hide: newConfigDelay, + }; + } else if (_.isObject(newConfigDelay)) { + config.delay = { + show: _.get(newConfigDelay, 'show', this.tooltipDefaultConfig.delay), + hide: _.get(newConfigDelay, 'hide', this.tooltipDefaultConfig.delay), + }; + } else if (_.isString(newConfigDelay) && _.size(newConfigDelay) > 0) { + const configDelayInteger = _.toInteger(newConfigDelay); + if (!_.isNaN(configDelayInteger)) { + config.delay = { + show: configDelayInteger, + hide: configDelayInteger, + }; + } else { + const configDelayObj = JSON.parse(newConfigDelay); + config.delay = { + show: _.get(configDelayObj, 'show', this.tooltipDefaultConfig.delay), + hide: _.get(configDelayObj, 'hide', this.tooltipDefaultConfig.delay), + }; + } + } else { + config.delay = { + show: this.tooltipDefaultConfig.delay, + hide: this.tooltipDefaultConfig.delay, + }; + } + + if (_.has(overrideConfig, 'html')) { + config.html = this.getConfigBoolean(overrideConfig.html); + } else if (_.has(this.tooltipEl.dataset, 'html')) { + config.html = this.getConfigBoolean(this.tooltipEl.dataset.html); + } else { + config.html = this.tooltipDefaultConfig.html; + } + + if (_.has(overrideConfig, 'placement')) { + config.placement = overrideConfig.placement; + } else if (_.has(this.tooltipEl.dataset, 'placement')) { + config.placement = this.tooltipEl.dataset.placement; + } else { + config.placement = this.tooltipDefaultConfig.placement; + } + + if (_.has(overrideConfig, 'offset')) { + config.offset = _.toInteger(overrideConfig.offset); + } else if (_.has(this.tooltipEl.dataset, 'offset')) { + config.offset = _.toInteger(this.tooltipEl.dataset.offset); + } else { + config.offset = this.tooltipDefaultConfig.offset; + } + if (_.isNaN(config.offset)) { + config.offset = this.tooltipDefaultConfig.offset; + } + + if (_.has(overrideConfig, 'container')) { + config.container = this.getConfigBoolean(overrideConfig.container); + } else if (_.has(this.tooltipEl.dataset, 'container')) { + config.container = this.getConfigBoolean(this.tooltipEl.dataset.container); + } else { + config.container = this.tooltipDefaultConfig.container; + } + + if (_.has(overrideConfig, 'fallbackPlacement')) { + config.fallbackPlacement = overrideConfig.fallbackPlacement; + } else if (_.has(this.tooltipEl.dataset, 'fallbackPlacement')) { + config.fallbackPlacement = this.tooltipEl.dataset.fallbackPlacement; + } else { + config.fallbackPlacement = this.tooltipDefaultConfig.fallbackPlacement; + } + + if (_.has(overrideConfig, 'boundary')) { + config.boundary = overrideConfig.boundary; + } else if (_.has(this.tooltipEl.dataset, 'boundary')) { + config.boundary = this.tooltipEl.dataset.boundary; + } else { + config.boundary = this.tooltipDefaultConfig.boundary; + } + + if (_.has(overrideConfig, 'disposeTimeToWait')) { + config.disposeTimeToWait = _.toInteger(overrideConfig.disposeTimeToWait); + } else if (_.has(this.tooltipEl.dataset, 'disposeTimeToWait')) { + config.disposeTimeToWait = _.toInteger(this.tooltipEl.dataset.disposeTimeToWait); + } else { + config.disposeTimeToWait = this.tooltipDefaultConfig.disposeTimeToWait; + } + if (_.isNaN(config.disposeTimeToWait)) { + config.disposeTimeToWait = this.tooltipDefaultConfig.disposeTimeToWait; + } + + if (_.has(overrideConfig, 'template')) { + config.template = overrideConfig.template; + } else if (_.has(this.tooltipEl.dataset, 'template')) { + config.template = this.tooltipEl.dataset.template; + } else { + config.template = this.tooltipDefaultConfig.template; + } + + this.config = config; + } + + private setupMethod(tooltipOptions: string | Partial): boolean | HTMLElement { + if (_.size(tooltipOptions) === 0) { + if (!this.isEnabled) { + this.enableTooltip(); + return true; + } + return this.tooltipEl; + } + + if (tooltipOptions === 'enable') { + this.enableTooltip(); + return true; + } + + if (tooltipOptions === 'disable') { + this.disableTooltip(); + return true; + } + + if (tooltipOptions === 'toggleEnabled') { + if (this.isEnabled) { + this.disableTooltip(); + } else { + this.enableTooltip(); + } + return true; + } + + if (tooltipOptions === 'show') { + if (!this.isEnabled) { + return null; + } + this.enter(); + return true; + } + + if (tooltipOptions === 'hide') { + this.activeTrigger = {}; + if (!this.isEnabled) { + return null; + } + this.leave(); + return true; + } + + if (tooltipOptions === 'toggle') { + if (!this.isEnabled) { + return null; + } + this.toggle(); + return true; + } + + if (tooltipOptions === 'update') { + if (this.popperHandle && this.popperHandle.scheduleUpdate) { + this.popperHandle.scheduleUpdate(); + return true; + } + return false; + } + + if (typeof tooltipOptions === 'object') { + if (this.isEnabled) { + this.disableTooltip(); + } + this.setConfig(tooltipOptions); + this.enableTooltip(); + return true; + } + + if (typeof tooltipOptions === 'string') { + throw new Error(`No method named "${tooltipOptions}"`); + } + return null; + + } + + + + // Tip HTMLElement + // --------------------------------------------------------------------------- + private createTooltipHtmlElement(): HTMLElement { + const tipAlreadyPresentInDom = document.getElementById(this.tooltipId); + if (tipAlreadyPresentInDom) { + return tipAlreadyPresentInDom; + } + + const container = !this.config.container ? document.body : document.querySelector(this.config.container); + const template = document.createElement('div'); + template.innerHTML = _.trim(this.config.template); + const innerTemplateTooltip = template.firstChild; + const newTip = container.appendChild(innerTemplateTooltip); + newTip.setAttribute('id', this.tooltipId); + return newTip; + } + + private removeTooltipHtmlElementFromDom() { + if (this.tip && this.tip.parentNode && this.hoverState !== 'in') { + this.tip.parentNode.removeChild(this.tip); + this.tip = null; + } + } + + private getTooltipHtmlElement(): HTMLElement { + clearTimeout(this.disposeTimeout); + this.tip = this.tip || this.createTooltipHtmlElement(); + return this.tip; + } + + private addAttachmentClassToTooltipHtmlElement(attachment: string) { + const tooltipAttachmentClass = `${this.tooltipBaseClass}-${attachment}`; + if (!_.includes(this.tooltipAppliedPopperPlacementClasses, tooltipAttachmentClass)) { + this.tooltipAppliedPopperPlacementClasses.push(tooltipAttachmentClass); + } + addClass(this.getTooltipHtmlElement(), tooltipAttachmentClass); + } + + private cleanTooltipHtmlElementClasses() { + const tip = this.getTooltipHtmlElement(); + const classesToRemove = _.intersection(this.tooltipAppliedPopperPlacementClasses, tip.classList); + classesToRemove.forEach(className => { + removeClass(tip, className); + }); + } + + private tooltipHtmlElementHasClass(className: string): boolean { + if (!this.tip) { + return false; + } + + return hasClass(this.tip, className); + } + + private setContent() { + const tip = this.getTooltipHtmlElement(); + const tooltipTitleEl = tip.querySelector('.og-tooltip__inner'); + if (tooltipTitleEl) { + this.setElementContent(tooltipTitleEl, this.getTitle()); + } + removeClass(tip, this.tooltipFadeClass); + removeClass(tip, this.tooltipShowClass); + } + + + + // Enable/Disable tooltip + // --------------------------------------------------------------------------- + private enableTooltip() { + if (this.disabled === true) { + return; + } + + const enableEvent = ogCustomEvent(this.tooltipEl, this.enableEventName); + if (enableEvent.defaultPrevented) { + return; + } + + this.isEnabled = true; + this.activeTrigger = {}; + this.hoverState = null; + this.tooltipId = uuidv4(); + this.tooltipEl.dataset.bsId = this.tooltipId; + this.tip = null; + if (_.size(this.config) === 0) { + this.setConfig(); + } + this.fixTitle(); + this.setListeners(); + window.requestAnimationFrame(() => { // trick to ensure all page updates are completed before running code + window.requestAnimationFrame(() => { // discussed here: https://www.youtube.com/watch?v=aCMbSyngXB4&t=11m + setTimeout(() => { + ogCustomEvent(this.tooltipEl, this.enabledEventName); + }, 0); + }); + }); + } + + private disableTooltip() { + const disableEvent = ogCustomEvent(this.tooltipEl, this.disableEventName); + if (disableEvent.defaultPrevented) { + return; + } + + this.isEnabled = false; + clearTimeout(this.showHideTimeout); + this.ogRemoveEventListener('mouseenter', this.handleMouseEnter); + this.ogRemoveEventListener('mouseleave', this.handleMouseLeave); + this.ogRemoveEventListener('focusin', this.handleFocusIn); + this.ogRemoveEventListener('focusout', this.handleFocusOut); + + const { originalTitle } = this.tooltipEl.dataset; + if (_.size(originalTitle) > 0) { + this.tooltipEl.title = originalTitle; + } + this.activeTrigger = {}; + this.hoverState = null; + clearTimeout(this.disposeTimeout); + this.removeTooltipHtmlElementFromDom(); + if (this.popperHandle && this.popperHandle.destroy) { + this.popperHandle.destroy(); + this.popperHandle = null; + } + window.requestAnimationFrame(() => { // trick to ensure all page updates are completed before running code + window.requestAnimationFrame(() => { // discussed here: https://www.youtube.com/watch?v=aCMbSyngXB4&t=11m + setTimeout(() => { + ogCustomEvent(this.tooltipEl, this.disabledEventName); + }, 0); + }); + }); + } + + + + // Enter/Leave + // --------------------------------------------------------------------------- + private enter(event: Event = null) { + // Determine the active trigger + if (event) { + if (event.type === 'focusin') { + this.activeTrigger.focus = true; + } else { + this.activeTrigger.hover = true; + } + } + + if (this.tooltipHtmlElementHasClass(this.tooltipShowClass) || this.hoverState === 'in') { + this.hoverState = 'in'; + return; + } + + clearTimeout(this.showHideTimeout); + + this.hoverState = 'in'; + + if (!this.config.delay || !this.config.delay.hasOwnProperty['show']) { + this.show(); + return; + } + + let showDelay = 0; + if (this.config.delay) { + if (this.config.delay.hasOwnProperty('show')) { + showDelay = this.config.delay['show']; + } else { + showDelay = this.config.delay as number; + } + } + this.showHideTimeout = setTimeout(() => { + if (this.hoverState === 'in') { + this.show(); + } + }, showDelay); + } + + private leave(event: any = null) { + if (event) { + if (event.type === 'focusout') { + this.activeTrigger.focus = false; + } else { + this.activeTrigger.hover = false; + } + } + + if (this.isWithActiveTrigger()) { + return; + } + + clearTimeout(this.showHideTimeout); + this.hoverState = 'out'; + + if (!this.config.delay || !this.config.delay.hasOwnProperty['hide']) { + this.hide(); + return; + } + + let hideDelay = 0; + if (this.config.delay) { + if (this.config.delay.hasOwnProperty('hide')) { + hideDelay = this.config.delay['hide']; + } else { + hideDelay = this.config.delay as number; + } + } + this.showHideTimeout = setTimeout(() => { + if (this.hoverState === 'out') { + this.hide(); + } + }, hideDelay); + } + + private toggle(event: any = null) { + if (!this.isEnabled) { + return; + } + if (event) { + this.activeTrigger.click = !this.activeTrigger.click; + if (this.isWithActiveTrigger()) { + this.enter(); + } else { + this.leave(); + } + } else if (this.tooltipHtmlElementHasClass(this.tooltipShowClass)) { + this.leave(); + } else { + this.enter(); + } + } + + + + // Show/Hide tooltip + // --------------------------------------------------------------------------- + private tooltipCanBeShown(): boolean { + if (!this.isEnabled) { + return false; + } + + const title = this.getTitle(); + if (title && _.isString(title) && title.toString().length > 0) { + return true; + } + + if (title && title instanceof Element) { + return true; + } + + return false; + } + + private show() { + if (this.tooltipEl.style.display === 'none') { + throw new Error('Please use show on visible elements'); + } + if (this.tooltipCanBeShown()) { + const showEvent = ogCustomEvent(this.tooltipEl, this.showEventName); + this.tooltipEl.setAttribute('aria-describedby', this.tooltipId); + const isInTheDom = this.tooltipEl.ownerDocument.documentElement.contains(this.tooltipEl); + if (showEvent.defaultPrevented || !isInTheDom) { + return; + } + + const tip = this.getTooltipHtmlElement(); + this.setContent(); + if (this.config.animation) { + addClass(tip, this.tooltipFadeClass); + } + const placement = typeof this.config.placement === 'function' ? this.config.placement.call(this, tip, this.tooltipEl) : this.config.placement; + this.addAttachmentClassToTooltipHtmlElement(placement); + // the point of inserted event is to know when the tip is in the DOM but before it has been placed using popper + ogCustomEvent(this.tooltipEl, this.insertedEventName); + this.popperHandle = new Popper(this.tooltipEl, tip, { + placement: placement, + modifiers: { + offset: { + offset: this.config.offset, + }, + flip: { + behavior: this.config.fallbackPlacement, + }, + arrow: { + element: '.arrow', + }, + preventOverflow: { + boundariesElement: this.config.boundary, + }, + }, + onCreate: this.handlePopperOnCreate, + onUpdate: this.handlePopperOnUpdate, + }); + addClass(tip, this.tooltipShowClass); + + if (hasClass(tip, this.tooltipFadeClass)) { + const transitionDuration = getTransitionDurationFromElement(tip); + setTimeout(() => { + this.showComplete(); + }, transitionDuration); + } else { + this.showComplete(); + } + } + } + + private showComplete() { + if (this.config.animation) { + this.fixTransition(); + } + + const previousHoverState = this.hoverState; + this.hoverState = null; + + window.requestAnimationFrame(() => { // trick to ensure all page updates are completed before running code + window.requestAnimationFrame(() => { // discussed here: https://www.youtube.com/watch?v=aCMbSyngXB4&t=11m + setTimeout(() => { + ogCustomEvent(this.tooltipEl, this.shownEventName); + }, 0); + }); + }); + + if (previousHoverState === 'out') { + this.leave(); + } + } + + private hide(callback: any = () => { }) { + const tip = this.getTooltipHtmlElement(); + const hideEvent = ogCustomEvent(this.tooltipEl, this.hideEventName); + if (hideEvent.defaultPrevented) { + return; + } + + removeClass(tip, this.tooltipShowClass); + + // If this is a touch-enabled device we remove the extra + // empty mouseover listeners we added for iOS support + // if ('ontouchstart' in document.documentElement) { + // $$$1(document.body).children().off('mouseover', null, $$$1.noop); + // } + + this.activeTrigger.click = false; + this.activeTrigger.focus = false; + this.activeTrigger.hover = false; + this.tooltipEl.removeAttribute('aria-describedby'); + + if (hasClass(tip, this.tooltipFadeClass)) { + const transitionDuration = getTransitionDurationFromElement(tip); + setTimeout(() => { + this.hideComplete(callback); + }, transitionDuration); + } else { + this.hideComplete(); + } + this.hoverState = null; + } + + private hideComplete(callback: any = () => { }) { + this.cleanTooltipHtmlElementClasses(); + + if (this.popperHandle && this.popperHandle.destroy) { + this.popperHandle.destroy(); + } + + this.disposeTimeout = setTimeout(() => { + this.removeTooltipHtmlElementFromDom(); + }, this.config.disposeTimeToWait); + + window.requestAnimationFrame(() => { // trick to ensure all page updates are completed before running code + window.requestAnimationFrame(() => { // discussed here: https://www.youtube.com/watch?v=aCMbSyngXB4&t=11m + setTimeout(() => { + ogCustomEvent(this.tooltipEl, this.hiddenEventName); + }, 0); + }); + }); + + callback(); + } + + // Title + // --------------------------------------------------------------------------- + private getTitle(): string | Element { + if (this.config.title) { + if (typeof this.config.title === 'function') { + const newTitle = this.config.title.call(this.tooltipEl); + return newTitle; + } + return this.config.title; + } + + if (this.tooltipEl.dataset.originalTitle) { + return this.tooltipEl.dataset.originalTitle; + } + + return ''; + } + + private fixTitle() { + if (_.size(this.tooltipEl.title) > 0) { + this.tooltipEl.dataset.originalTitle = this.tooltipEl.title || ''; + this.tooltipEl.title = ''; + } + } + + + + // Helpers + // --------------------------------------------------------------------------- + private ogAddEventListener(eventName, listenerFunction) { + this.ogRemoveEventListener(eventName, listenerFunction, () => { + this.tooltipEl.addEventListener(eventName, listenerFunction); + }); + } + + private ogRemoveEventListener(eventName, listenerFunction, callback = () => {}) { + this.tooltipEl.removeEventListener(eventName, listenerFunction); + callback(); + } + + private fixTransition() { + const tip = this.getTooltipHtmlElement(); + const initConfigAnimation = this.config.animation; + + if (tip.getAttribute('x-placement') !== null) { + return; + } + + removeClass(tip, this.tooltipFadeClass); + this.config.animation = false; + this.hide(); + this.show(); + this.config.animation = initConfigAnimation; + } + + private getConfigBoolean(configValue: any): boolean { + if (configValue === true || + _.toLower(configValue) === 'true' || + configValue === 1 || + configValue === '1') { + return true; + } + + return false; + } + + private isWithActiveTrigger(): boolean { + const activeTriggerKeys = Object.keys(this.activeTrigger); + return activeTriggerKeys.some((triggerKey) => { + this.activeTrigger[triggerKey]; + }); + } + + private setElementContent(el: Element, content: Element | string) { + let castedContent; + + if (this.config.html) { + if (content instanceof Element) { + castedContent = content as Element; + el.appendChild(castedContent); + return; + } + castedContent = content as string; + el.innerHTML = castedContent; + } else { + if (content instanceof Element) { + castedContent = content as Element; + el.textContent = castedContent.innerText; + return; + } + castedContent = content as string; + el.textContent = castedContent; + } + } + +} diff --git a/src/examples/Tooltip.html b/src/examples/Tooltip.html new file mode 100644 index 00000000..508ada0b --- /dev/null +++ b/src/examples/Tooltip.html @@ -0,0 +1,143 @@ + + +
+ + Simple tooltip above +

HTML code:

+ + <og-tooltip data-title="A simple text title" data-placement="top">Simple tooltip above</og-tooltip> + +
+ + + Tooltip on hover only (no tab focus) +

HTML code:

+ + <og-tooltip data-title="A simple text title" data-placement="top" tabindex="-1">Tooltip on hover only (no tab focus)</og-tooltip> + +
+ + + +

+ +

+ +

HTML code:

+ + <og-tooltip id="tooltipWithDynamicTitle" data-title="Title" [og-title]="dynamicTitle" data-placement="right" tabindex="-1"><og-button label="Button with a tooltip"></og-button><og-tooltip> + +

Note: The way to correctly bind to 'dynamicTitle' depends on the framework in use. Since the button itself is focusable via keyboard (tab), we can set the tabindex of the tooltip to '-1' and will still be able to having it appear on keyboard focus.

+
+ + + Tooltip at bottom-end with HTML content +

HTML code:

+ + <og-tooltip og-title='<h3>Headline</h3><br /><span style="font-weight: bold">Bold text<span> and normal text.' data-html="true" data-placement="bottom-end">Tooltip at bottom-end with HTML content</og-tooltip> + +
+ + + Tooltip with config applied via code +

HTML code:

+ + <og-tooltip id="customTooltipOne">Tooltip with config applied via code</og-tooltip> + +

JS code:

+ + document.getElementById('customTooltipOne').addEventListener('og.tooltip.enabled', function(event) { + event.target.setAttribute('og-title', 'This tooltip title has been set via code'); + }); + +
+ + + Tooltip initially shown via code +

HTML code:

+ + <og-tooltip id="customTooltipTwo" data-title="Another custom tooltip">Tooltip initially shown via code</og-tooltip> + +

JS code:

+ + document.getElementById('customTooltipTwo').addEventListener('og.tooltip.enabled', function(event) { + event.target.tooltip('show'); + }); + +
+
+
+ + + + diff --git a/src/index.html b/src/index.html index c81af964..c5e3fa33 100644 --- a/src/index.html +++ b/src/index.html @@ -201,6 +201,7 @@

ORGENIC UI

Button List Combobox + Tooltip
@@ -244,7 +245,8 @@

ORGENIC UI

'toggle-switch': () => { loadHTML('./examples/ToggleSwitch.html', view); }, 'button': () => { loadHTML('./examples/Button.html', view); }, 'list': () => { loadHTML('./examples/List.html', view); }, - 'combobox': () => { loadHTML('./examples/Combobox.html', view); } + 'combobox': () => { loadHTML('./examples/Combobox.html', view); }, + 'tooltip': () => { loadHTML('./examples/Tooltip.html', view); } }); // set the default route diff --git a/src/styles/themes/dark.theme.scss b/src/styles/themes/dark.theme.scss index 72c19bf7..bc97af81 100644 --- a/src/styles/themes/dark.theme.scss +++ b/src/styles/themes/dark.theme.scss @@ -107,6 +107,11 @@ // ------------------------------------------------------------------------- --OG-MARGIN: 8px; --OG-PADDING: 8px; + + // ------------------------------------------------------------------------- + // COMPONENT : Tooltip + // ------------------------------------------------------------------------- + --OG-TOOLTIP-BACKGROUND-COLOR: hsla(210, 14%, 23%, 1); } @import 'font.scss'; diff --git a/src/styles/themes/light.theme.scss b/src/styles/themes/light.theme.scss index f42f325a..235b0ba0 100644 --- a/src/styles/themes/light.theme.scss +++ b/src/styles/themes/light.theme.scss @@ -107,6 +107,11 @@ // ------------------------------------------------------------------------- --OG-MARGIN: 8px; --OG-PADDING: 8px; + + // ------------------------------------------------------------------------- + // COMPONENT : Tooltip + // ------------------------------------------------------------------------- + --OG-TOOLTIP-BACKGROUND-COLOR: hsla(0, 0%, 100%, 1); } @import 'font.scss'; diff --git a/src/styles/variables/color.variable.scss b/src/styles/variables/color.variable.scss index d88d89fb..f648233e 100644 --- a/src/styles/variables/color.variable.scss +++ b/src/styles/variables/color.variable.scss @@ -70,3 +70,9 @@ $og-color-shade--100--20: hsla(200, 25%, 30%, 0.2); $og-color-shade--100--30: hsla(200, 25%, 30%, 0.3); $og-color-shade--100--50: hsla(200, 25%, 30%, 0.5); $og-color-shade--100--60: hsla(200, 25%, 30%, 0.6); + +// ------------------------------------------------------------------------- +// COMPONENT : Tooltip +// ------------------------------------------------------------------------- +$og-tooltip-background-color: hsla(0, 0%, 100%, 1); +$og-tooltip-drop-shadow: drop-shadow(0 0 8px rgba(0, 0, 0, .05)); diff --git a/src/styles/variables/sizing.variable.scss b/src/styles/variables/sizing.variable.scss index 46bcd9fc..977048e5 100644 --- a/src/styles/variables/sizing.variable.scss +++ b/src/styles/variables/sizing.variable.scss @@ -2,5 +2,10 @@ // BASIC VARIABLES : SIZING // ============================================================================= -$og-border-radius: 3px; -$og-border-width: 1px; +$og-z-index-dropdown: 1000; +$og-z-index-dialog: 1100; +$og-z-index-popover: 1200; +$og-z-index-tooltip: 1300; + +$og-border-radius: 3px; +$og-border-width: 1px; diff --git a/src/utils/add-class.ts b/src/utils/add-class.ts new file mode 100644 index 00000000..d39c0218 --- /dev/null +++ b/src/utils/add-class.ts @@ -0,0 +1,7 @@ +export default function addClass(el: Element, className: string) { + if (el.classList) { + el.classList.add(className); + } else { + el.className += ` ${className}`; + } +} diff --git a/src/utils/custom-event.ts b/src/utils/custom-event.ts new file mode 100644 index 00000000..a80be9ce --- /dev/null +++ b/src/utils/custom-event.ts @@ -0,0 +1,46 @@ +function preventDefault() { + // needed for IE compatibility + Object.defineProperty(this, "defaultPrevented", { + get: () => true + }); +} + +// eslint-disable-next-line @typescript-eslint/promise-function-async +export default function ogCustomEvent(el: Element, eventName: string, payload = {}, relatedTarget = {}) { + const myPayload = Object.assign({}, { sentAtTime: new Date().getTime() }, payload); + let event; + + if (typeof (window as any).CustomEvent === "function") { + event = new CustomEvent(eventName, { + bubbles: true, + cancelable: true, + detail: myPayload + }); + } else { + const bubbles = true; + const cancelable = true; + event = document.createEvent("CustomEvent"); + event.initCustomEvent(eventName, bubbles, cancelable, { + detail: myPayload + }); + } + + Object.defineProperties(event, { + preventDefault: { + value: preventDefault, + writable: true + } + }); + + if (relatedTarget instanceof Element) { + Object.defineProperties(event, { + relatedTarget: { + value: relatedTarget, + writable: true + } + }); + } + + el.dispatchEvent(event); + return event; +} diff --git a/src/utils/get-transition-duration-from-element.ts b/src/utils/get-transition-duration-from-element.ts new file mode 100644 index 00000000..e314e692 --- /dev/null +++ b/src/utils/get-transition-duration-from-element.ts @@ -0,0 +1,26 @@ +import _ from 'lodash'; + +function transformTransitionDuration(stringDuration: string): number { + // always get the first duration + const durationArr = _.split(stringDuration, ','); + const timeInSeconds = _.toNumber(_.trimEnd(_.toLower(durationArr[0]), 's')); + if (_.isNaN(timeInSeconds)) { + return 0; + } + + return timeInSeconds * 1000; +} + +export default function getTransitionDurationFromElement(element: Element): number { + if (!element) { + return 0; + } + + const elementStyle = window.getComputedStyle(element, null); + const animationDuration = elementStyle.getPropertyValue('animation-duration'); + const transitionDuration = elementStyle.getPropertyValue('transition-duration'); + const durationArr = []; + durationArr.push(transformTransitionDuration(animationDuration)); + durationArr.push(transformTransitionDuration(transitionDuration)); + return _.max(durationArr); +} diff --git a/src/utils/has-class.ts b/src/utils/has-class.ts new file mode 100644 index 00000000..499da0a9 --- /dev/null +++ b/src/utils/has-class.ts @@ -0,0 +1,7 @@ +export default function hasClass(el: Element, className: string): boolean { + if (el.classList) { + return el.classList.contains(className); + } + + return new RegExp("(^| )" + className + "( |$)", "gi").test(el.className); +} diff --git a/src/utils/remove-class.ts b/src/utils/remove-class.ts new file mode 100644 index 00000000..fe40eb19 --- /dev/null +++ b/src/utils/remove-class.ts @@ -0,0 +1,7 @@ +export default function removeClass(el: Element, className: string) { + if (el.classList) { + el.classList.remove(className); + } else { + el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); + } +} diff --git a/src/utils/uuidv4.ts b/src/utils/uuidv4.ts new file mode 100644 index 00000000..83b46288 --- /dev/null +++ b/src/utils/uuidv4.ts @@ -0,0 +1,7 @@ +export default function uuidv4(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0; + const v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} From fb465592f63e50deb12b4142a3360b688100cfc0 Mon Sep 17 00:00:00 2001 From: Christian Otto Date: Fri, 24 Jan 2020 14:14:04 +0100 Subject: [PATCH 2/5] style(og-tooltip): update comments in style file --- src/components/og-tooltip/og-tooltip.scss | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/og-tooltip/og-tooltip.scss b/src/components/og-tooltip/og-tooltip.scss index 3736baa6..025e9deb 100644 --- a/src/components/og-tooltip/og-tooltip.scss +++ b/src/components/og-tooltip/og-tooltip.scss @@ -63,8 +63,7 @@ og-tooltip { margin: var(--_og-tooltip-Margin); filter: var(--_og-tooltip-DropShadow); opacity: 0; - - // tame the dimensions of the tooltip (e.g. viewport-width/height values) + // Tame the overall tooltip dimension (e.g. viewport-width/height values) max-width: var(--_og-tooltip-MaxWidth); max-height: var(--_og-tooltip-MaxHeight); @@ -127,7 +126,7 @@ og-tooltip { &[class*="og-tooltip-left"], &[class*="og-tooltip-auto"][x-placement^="top"], &[class*="og-tooltip-auto"][x-placement^="left"] { - // make the arrow appear after the content + // Make the arrow appear after the content --_og-tooltip-arrow-Order: 1; } @@ -197,7 +196,7 @@ og-tooltip { } } -// detect if the user has requested that the system minimize the amount of +// Detect if the user has requested that the system shall minimize the amount of // animation or motion it uses @media (prefers-reduced-motion: reduce) { .og-tooltip.fade { @@ -205,7 +204,7 @@ og-tooltip { } } -// make sure that the dimensions of the tooltip have a relation to the +// Make sure that the overall tooltip dimension has a relation to the // (increasing) viewport size @media (min-width: 768px) { .og-tooltip { From 9c25f21c9903acf9253a42130898c4f97916549d Mon Sep 17 00:00:00 2001 From: Christian Otto Date: Fri, 24 Jan 2020 14:26:36 +0100 Subject: [PATCH 3/5] style(og-tooltip): renaming methods --- src/components/og-tooltip/og-tooltip.tsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/og-tooltip/og-tooltip.tsx b/src/components/og-tooltip/og-tooltip.tsx index 1713c814..908f04a8 100644 --- a/src/components/og-tooltip/og-tooltip.tsx +++ b/src/components/og-tooltip/og-tooltip.tsx @@ -482,12 +482,12 @@ export class OgTooltip { } if (_.includes(triggers, 'hover')) { - this.ogAddEventListener('mouseenter', this.handleMouseEnter); - this.ogAddEventListener('mouseleave', this.handleMouseLeave); + this.tooltipAddEventListener('mouseenter', this.handleMouseEnter); + this.tooltipAddEventListener('mouseleave', this.handleMouseLeave); } if (_.includes(triggers, 'focus')) { - this.ogAddEventListener('focusin', this.handleFocusIn); - this.ogAddEventListener('focusout', this.handleFocusOut); + this.tooltipAddEventListener('focusin', this.handleFocusIn); + this.tooltipAddEventListener('focusout', this.handleFocusOut); } } @@ -832,10 +832,10 @@ export class OgTooltip { this.isEnabled = false; clearTimeout(this.showHideTimeout); - this.ogRemoveEventListener('mouseenter', this.handleMouseEnter); - this.ogRemoveEventListener('mouseleave', this.handleMouseLeave); - this.ogRemoveEventListener('focusin', this.handleFocusIn); - this.ogRemoveEventListener('focusout', this.handleFocusOut); + this.tooltipRemoveEventListener('mouseenter', this.handleMouseEnter); + this.tooltipRemoveEventListener('mouseleave', this.handleMouseLeave); + this.tooltipRemoveEventListener('focusin', this.handleFocusIn); + this.tooltipRemoveEventListener('focusout', this.handleFocusOut); const { originalTitle } = this.tooltipEl.dataset; if (_.size(originalTitle) > 0) { @@ -1132,13 +1132,13 @@ export class OgTooltip { // Helpers // --------------------------------------------------------------------------- - private ogAddEventListener(eventName, listenerFunction) { - this.ogRemoveEventListener(eventName, listenerFunction, () => { + private tooltipAddEventListener(eventName, listenerFunction) { + this.tooltipRemoveEventListener(eventName, listenerFunction, () => { this.tooltipEl.addEventListener(eventName, listenerFunction); }); } - private ogRemoveEventListener(eventName, listenerFunction, callback = () => {}) { + private tooltipRemoveEventListener(eventName, listenerFunction, callback = () => {}) { this.tooltipEl.removeEventListener(eventName, listenerFunction); callback(); } From dc6a1d2afa36305ef4eb15dd616c98748d8a1da5 Mon Sep 17 00:00:00 2001 From: Christian Otto Date: Fri, 24 Jan 2020 14:30:27 +0100 Subject: [PATCH 4/5] style(og-tooltip): remove commented out code --- src/components/og-tooltip/og-tooltip.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/og-tooltip/og-tooltip.tsx b/src/components/og-tooltip/og-tooltip.tsx index 908f04a8..8054bddf 100644 --- a/src/components/og-tooltip/og-tooltip.tsx +++ b/src/components/og-tooltip/og-tooltip.tsx @@ -1059,12 +1059,6 @@ export class OgTooltip { removeClass(tip, this.tooltipShowClass); - // If this is a touch-enabled device we remove the extra - // empty mouseover listeners we added for iOS support - // if ('ontouchstart' in document.documentElement) { - // $$$1(document.body).children().off('mouseover', null, $$$1.noop); - // } - this.activeTrigger.click = false; this.activeTrigger.focus = false; this.activeTrigger.hover = false; From 7be18ef4a48743a25e8ffa66b7534f561ae84a74 Mon Sep 17 00:00:00 2001 From: Christian Otto Date: Tue, 28 Jan 2020 09:49:39 +0100 Subject: [PATCH 5/5] refactor(og-tooltip): make direct use of classList methods --- src/components/og-tooltip/og-tooltip.tsx | 58 ++++++++++++------------ src/utils/add-class.ts | 7 --- src/utils/has-class.ts | 7 --- src/utils/remove-class.ts | 7 --- 4 files changed, 29 insertions(+), 50 deletions(-) delete mode 100644 src/utils/add-class.ts delete mode 100644 src/utils/has-class.ts delete mode 100644 src/utils/remove-class.ts diff --git a/src/components/og-tooltip/og-tooltip.tsx b/src/components/og-tooltip/og-tooltip.tsx index 8054bddf..0e55b333 100644 --- a/src/components/og-tooltip/og-tooltip.tsx +++ b/src/components/og-tooltip/og-tooltip.tsx @@ -20,11 +20,8 @@ import _ from 'lodash'; import Popper from 'popper.js'; // ORGENIC-UI helper classes -import addClass from '../../utils/add-class'; import getTransitionDurationFromElement from '../../utils/get-transition-duration-from-element'; -import hasClass from '../../utils/has-class'; import ogCustomEvent from '../../utils/custom-event'; -import removeClass from '../../utils/remove-class'; import uuidv4 from '../../utils/uuidv4'; /** @@ -366,8 +363,8 @@ export class OgTooltip { } if (this.isWithActiveTrigger() || this.tooltipHtmlElementHasClass(this.tooltipShowClass) || this.hoverState === 'in') { - const tooltipElement = this.getTooltipHtmlElement(); - const tooltipContentPlaceholder = tooltipElement.querySelector('.og-tooltip__inner'); + const tooltip = this.getTooltipHtmlElement(); + const tooltipContentPlaceholder = tooltip.querySelector('.og-tooltip__inner'); if (tooltipContentPlaceholder) { this.setElementContent(tooltipContentPlaceholder, this.getTitle()); if (this.popperHandle && this.popperHandle.scheduleUpdate) { @@ -430,7 +427,7 @@ export class OgTooltip { if (hasAnimation) { this.setConfig(); const tipEl = this.tip || this.createTooltipHtmlElement(); - addClass(tipEl, this.tooltipFadeClass); + tipEl.classList.add(this.tooltipFadeClass); } }, { once: true }); this.enter(); @@ -761,14 +758,16 @@ export class OgTooltip { if (!_.includes(this.tooltipAppliedPopperPlacementClasses, tooltipAttachmentClass)) { this.tooltipAppliedPopperPlacementClasses.push(tooltipAttachmentClass); } - addClass(this.getTooltipHtmlElement(), tooltipAttachmentClass); + + const tooltip = this.getTooltipHtmlElement(); + tooltip.classList.add(tooltipAttachmentClass); } private cleanTooltipHtmlElementClasses() { - const tip = this.getTooltipHtmlElement(); - const classesToRemove = _.intersection(this.tooltipAppliedPopperPlacementClasses, tip.classList); + const tooltip = this.getTooltipHtmlElement(); + const classesToRemove = _.intersection(this.tooltipAppliedPopperPlacementClasses, tooltip.classList); classesToRemove.forEach(className => { - removeClass(tip, className); + tooltip.classList.remove(className); }); } @@ -777,17 +776,17 @@ export class OgTooltip { return false; } - return hasClass(this.tip, className); + return this.tip.classList.contains(className); } private setContent() { - const tip = this.getTooltipHtmlElement(); - const tooltipTitleEl = tip.querySelector('.og-tooltip__inner'); + const tooltip = this.getTooltipHtmlElement(); + const tooltipTitleEl = tooltip.querySelector('.og-tooltip__inner'); if (tooltipTitleEl) { this.setElementContent(tooltipTitleEl, this.getTitle()); } - removeClass(tip, this.tooltipFadeClass); - removeClass(tip, this.tooltipShowClass); + tooltip.classList.remove(this.tooltipFadeClass); + tooltip.classList.remove(this.tooltipShowClass); } @@ -988,16 +987,16 @@ export class OgTooltip { return; } - const tip = this.getTooltipHtmlElement(); + const tooltip = this.getTooltipHtmlElement(); this.setContent(); if (this.config.animation) { - addClass(tip, this.tooltipFadeClass); + tooltip.classList.add(this.tooltipFadeClass); } - const placement = typeof this.config.placement === 'function' ? this.config.placement.call(this, tip, this.tooltipEl) : this.config.placement; + const placement = typeof this.config.placement === 'function' ? this.config.placement.call(this, tooltip, this.tooltipEl) : this.config.placement; this.addAttachmentClassToTooltipHtmlElement(placement); // the point of inserted event is to know when the tip is in the DOM but before it has been placed using popper ogCustomEvent(this.tooltipEl, this.insertedEventName); - this.popperHandle = new Popper(this.tooltipEl, tip, { + this.popperHandle = new Popper(this.tooltipEl, tooltip, { placement: placement, modifiers: { offset: { @@ -1016,10 +1015,11 @@ export class OgTooltip { onCreate: this.handlePopperOnCreate, onUpdate: this.handlePopperOnUpdate, }); - addClass(tip, this.tooltipShowClass); - if (hasClass(tip, this.tooltipFadeClass)) { - const transitionDuration = getTransitionDurationFromElement(tip); + tooltip.classList.add(this.tooltipShowClass); + + if (tooltip.classList.contains(this.tooltipFadeClass)) { + const transitionDuration = getTransitionDurationFromElement(tooltip); setTimeout(() => { this.showComplete(); }, transitionDuration); @@ -1051,21 +1051,21 @@ export class OgTooltip { } private hide(callback: any = () => { }) { - const tip = this.getTooltipHtmlElement(); + const tooltip = this.getTooltipHtmlElement(); const hideEvent = ogCustomEvent(this.tooltipEl, this.hideEventName); if (hideEvent.defaultPrevented) { return; } - removeClass(tip, this.tooltipShowClass); + tooltip.classList.remove(this.tooltipShowClass); this.activeTrigger.click = false; this.activeTrigger.focus = false; this.activeTrigger.hover = false; this.tooltipEl.removeAttribute('aria-describedby'); - if (hasClass(tip, this.tooltipFadeClass)) { - const transitionDuration = getTransitionDurationFromElement(tip); + if (tooltip.classList.contains(this.tooltipFadeClass)) { + const transitionDuration = getTransitionDurationFromElement(tooltip); setTimeout(() => { this.hideComplete(callback); }, transitionDuration); @@ -1138,14 +1138,14 @@ export class OgTooltip { } private fixTransition() { - const tip = this.getTooltipHtmlElement(); + const tooltip = this.getTooltipHtmlElement(); const initConfigAnimation = this.config.animation; - if (tip.getAttribute('x-placement') !== null) { + if (tooltip.getAttribute('x-placement') !== null) { return; } - removeClass(tip, this.tooltipFadeClass); + tooltip.classList.remove(this.tooltipFadeClass); this.config.animation = false; this.hide(); this.show(); diff --git a/src/utils/add-class.ts b/src/utils/add-class.ts deleted file mode 100644 index d39c0218..00000000 --- a/src/utils/add-class.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default function addClass(el: Element, className: string) { - if (el.classList) { - el.classList.add(className); - } else { - el.className += ` ${className}`; - } -} diff --git a/src/utils/has-class.ts b/src/utils/has-class.ts deleted file mode 100644 index 499da0a9..00000000 --- a/src/utils/has-class.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default function hasClass(el: Element, className: string): boolean { - if (el.classList) { - return el.classList.contains(className); - } - - return new RegExp("(^| )" + className + "( |$)", "gi").test(el.className); -} diff --git a/src/utils/remove-class.ts b/src/utils/remove-class.ts deleted file mode 100644 index fe40eb19..00000000 --- a/src/utils/remove-class.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default function removeClass(el: Element, className: string) { - if (el.classList) { - el.classList.remove(className); - } else { - el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); - } -}