From 2dc48e8edfdd1ef36a0e115e271f51a3f4c2cc5a Mon Sep 17 00:00:00 2001 From: Nitwel Date: Tue, 18 Feb 2020 23:58:24 +0100 Subject: [PATCH] Custom Tooltip (#43) * implemented basic tooltip * add animation * finish up tooltip, added instant option * implemented basic tooltip * add animation * finish up tooltip, added instant option * Uninstall v-tooltip * Match folder structure of focus to tooltip * Register new directives * remove duplicate folder * Export functions, cleanup animate * Export update tooltip function * Increase test covergae * Added start and end options * Structure positioning tests * tooltip right end will now show on the right end * Add tests for modifier states * Update test coverage * Fix stylelint issues * made top as default position * added inverted option * fix lint * Move tooltip style vars to theme * Remove line clamp * Update tests Co-authored-by: Rijk van Zanten --- .storybook/preview.js | 2 + package.json | 1 - src/directives/focus.ts | 9 - src/directives/focus/focus.readme.md | 7 + src/directives/focus/focus.story.ts | 27 ++ src/directives/{ => focus}/focus.test.ts | 4 +- src/directives/focus/focus.ts | 9 + src/directives/index.ts | 2 - src/directives/register.ts | 7 + src/directives/tooltip.ts | 1 - src/directives/tooltip/tooltip.readme.md | 21 ++ src/directives/tooltip/tooltip.story.ts | 57 ++++ src/directives/tooltip/tooltip.test.ts | 357 +++++++++++++++++++++++ src/directives/tooltip/tooltip.ts | 175 +++++++++++ src/main.ts | 1 + src/plugins.ts | 2 - src/styles/_tooltip.scss | 110 +++++++ src/styles/lib/_tooltip.scss | 216 -------------- src/styles/main.scss | 2 +- src/styles/themes/_default.scss | 5 + src/views/public/public-view.test.ts | 4 +- yarn.lock | 16 +- 22 files changed, 784 insertions(+), 251 deletions(-) delete mode 100644 src/directives/focus.ts create mode 100644 src/directives/focus/focus.readme.md create mode 100644 src/directives/focus/focus.story.ts rename src/directives/{ => focus}/focus.test.ts (73%) create mode 100644 src/directives/focus/focus.ts delete mode 100644 src/directives/index.ts create mode 100644 src/directives/register.ts delete mode 100644 src/directives/tooltip.ts create mode 100644 src/directives/tooltip/tooltip.readme.md create mode 100644 src/directives/tooltip/tooltip.story.ts create mode 100644 src/directives/tooltip/tooltip.test.ts create mode 100644 src/directives/tooltip/tooltip.ts create mode 100644 src/styles/_tooltip.scss delete mode 100644 src/styles/lib/_tooltip.scss diff --git a/.storybook/preview.js b/.storybook/preview.js index 07625945de..16e56a0330 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -3,6 +3,8 @@ import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport'; import "../src/styles/main.scss"; import "../src/plugins"; +import "../src/components/register"; +import "../src/directives/register"; addParameters({ docs: { diff --git a/package.json b/package.json index ea443c3c92..1fdba36239 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "lodash": "^4.17.15", "nanoid": "^2.1.11", "pinia": "0.0.5", - "v-tooltip": "^2.0.3", "vue": "^2.6.11", "vue-i18n": "^8.15.3", "vue-router": "^3.1.5", diff --git a/src/directives/focus.ts b/src/directives/focus.ts deleted file mode 100644 index fb7310fa9f..0000000000 --- a/src/directives/focus.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Vue, { DirectiveOptions, DirectiveFunction } from 'vue'; - -export const definition: DirectiveOptions = { - inserted(el) { - el.focus(); - } -}; - -Vue.directive('focus', definition); diff --git a/src/directives/focus/focus.readme.md b/src/directives/focus/focus.readme.md new file mode 100644 index 0000000000..659125bcbb --- /dev/null +++ b/src/directives/focus/focus.readme.md @@ -0,0 +1,7 @@ +# Focus + +The focus directive is basically `autofocus`, but works in Vue. Because of the way HTML works, `autofocus` isn't triggered for DOM elements that are added after first load, which means that `autofocus` basically never works in the context of the Directus app. That's where you can use `v-focus` instead: + +```html + +``` diff --git a/src/directives/focus/focus.story.ts b/src/directives/focus/focus.story.ts new file mode 100644 index 0000000000..f43348308f --- /dev/null +++ b/src/directives/focus/focus.story.ts @@ -0,0 +1,27 @@ +import Vue from 'vue'; +import VInput from '../../components/v-input'; +import Focus from './focus'; +import markdown from './focus.readme.md'; +import withPadding from '../../../.storybook/decorators/with-padding'; + +Vue.component('v-input', VInput); +Vue.directive('focus', Focus); + +export default { + title: 'Directives / Focus', + component: VInput, + decorators: [withPadding], + parameters: { + notes: markdown + } +}; + +export const withText = () => ({ + template: ` +
+ + + +
+ ` +}); diff --git a/src/directives/focus.test.ts b/src/directives/focus/focus.test.ts similarity index 73% rename from src/directives/focus.test.ts rename to src/directives/focus/focus.test.ts index 31dbd2824e..d31034d49a 100644 --- a/src/directives/focus.test.ts +++ b/src/directives/focus/focus.test.ts @@ -1,11 +1,11 @@ -import { definition } from './focus'; +import Focus from './focus'; describe('Directives / Focus', () => { it('Calls focus() on the element on insertion', () => { const el = { focus: jest.fn() }; // I don't care about the exact types of this Vue internal function. We just want to make // sure `focus()` is being called on `el`. - definition.inserted!(el as any, null as any, null as any, null as any); + Focus.inserted!(el as any, null as any, null as any, null as any); expect(el.focus).toHaveBeenCalled(); }); }); diff --git a/src/directives/focus/focus.ts b/src/directives/focus/focus.ts new file mode 100644 index 0000000000..d869fccb8c --- /dev/null +++ b/src/directives/focus/focus.ts @@ -0,0 +1,9 @@ +import { DirectiveOptions } from 'vue'; + +const Focus: DirectiveOptions = { + inserted(el) { + el.focus(); + } +}; + +export default Focus; diff --git a/src/directives/index.ts b/src/directives/index.ts deleted file mode 100644 index 8ec6657d6b..0000000000 --- a/src/directives/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import './focus'; -import './tooltip'; diff --git a/src/directives/register.ts b/src/directives/register.ts new file mode 100644 index 0000000000..2b31a98820 --- /dev/null +++ b/src/directives/register.ts @@ -0,0 +1,7 @@ +import Vue from 'vue'; + +import Focus from './focus/focus'; +import Tooltip from './tooltip/tooltip'; + +Vue.directive('focus', Focus); +Vue.directive('tooltip', Tooltip); diff --git a/src/directives/tooltip.ts b/src/directives/tooltip.ts deleted file mode 100644 index ec10f3e9de..0000000000 --- a/src/directives/tooltip.ts +++ /dev/null @@ -1 +0,0 @@ -console.log('hi'); diff --git a/src/directives/tooltip/tooltip.readme.md b/src/directives/tooltip/tooltip.readme.md new file mode 100644 index 0000000000..69657eba6e --- /dev/null +++ b/src/directives/tooltip/tooltip.readme.md @@ -0,0 +1,21 @@ +# Tooltip + +The tooltip can display more detailed information about an element to clarify its use. + +```html +This is a button +``` + +## Options + +The tooltip is displayed at the bottom of an element by default. + +| Option | Description | +|-----------|-------------------------------------------------| +| `left` | Display the tooltip to the left of the element | +| `right` | Display the tooltip to the right of the element | +| `bottom` | Display the tooltip on bottom of the element | +| `start` | Display the tooltip to the end of the element | +| `end` | Display the tooltip to the start of the element | +| `instant` | Shows the tooltip instantly on hover | +| `inverted`| Inverts all colors | \ No newline at end of file diff --git a/src/directives/tooltip/tooltip.story.ts b/src/directives/tooltip/tooltip.story.ts new file mode 100644 index 0000000000..f7f933e0a1 --- /dev/null +++ b/src/directives/tooltip/tooltip.story.ts @@ -0,0 +1,57 @@ +import { + withKnobs, + text, + boolean, + number, + color, + optionsKnob as options +} from '@storybook/addon-knobs'; +import Vue from 'vue'; +import VButton from '../../components/v-button'; +import VIcon from '../../components/v-icon'; +import markdown from './tooltip.readme.md'; +import withPadding from '../../../.storybook/decorators/with-padding'; + +Vue.component('v-button', VButton); +Vue.component('v-icon', VIcon); + +export default { + title: 'Directives / Tooltip', + component: VButton, + decorators: [withKnobs, withPadding], + parameters: { + notes: markdown + } +}; + +export const withText = () => ({ + template: ` +
+
+ Bottom start + Bottom + Bottom end +
+
+ Left start + Left + Left end +
+
+ Right start + Right + Right end +
+
+ Top start + Top + Top end +
+ Instant Tooltip + Inverted Tooltip + + + +
+ ` +}); diff --git a/src/directives/tooltip/tooltip.test.ts b/src/directives/tooltip/tooltip.test.ts new file mode 100644 index 0000000000..46e522076c --- /dev/null +++ b/src/directives/tooltip/tooltip.test.ts @@ -0,0 +1,357 @@ +import { createLocalVue } from '@vue/test-utils'; +import VueCompositionAPI from '@vue/composition-api'; +import VButton from '../../components/v-button'; +import Tooltip, { + getTooltip, + animateIn, + animateOut, + onLeaveTooltip, + updateTooltip, + onEnterTooltip +} from './tooltip'; + +jest.useFakeTimers(); + +const localVue = createLocalVue(); +localVue.use(VueCompositionAPI); +localVue.component('v-button', VButton); +localVue.directive('tooltip', Tooltip); + +describe('Tooltip', () => { + afterEach(() => { + document.getElementsByTagName('html')[0].innerHTML = ''; + }); + + describe('onEnterTooltip', () => { + it('Instant does not wait to show the tooltip', () => { + const div = document.createElement('div'); + const tooltip = document.createElement('div'); + tooltip.id = 'tooltip'; + document.body.appendChild(tooltip); + + onEnterTooltip(div, { + name: 'tooltip', + modifiers: { + top: true, + instant: true + } + }); + + expect(tooltip.className).toBe('visible enter top'); + }); + + it('Default waits 600ms to show tooltip', () => { + const div = document.createElement('div'); + const tooltip = document.createElement('div'); + tooltip.id = 'tooltip'; + document.body.appendChild(tooltip); + + onEnterTooltip(div, { + name: 'tooltip', + modifiers: { + top: true, + instant: false + } + }); + + expect(tooltip.className).toBe(''); + jest.advanceTimersByTime(650); + expect(tooltip.className).toBe('visible top enter-active'); + }); + }); + + describe('updateTooltip', () => { + describe('Styles and classes', () => { + type Modifier = { + right: boolean; + bottom: boolean; + left: boolean; + start: boolean; + end: boolean; + inverted: boolean; + }; + + function testUpdateTooltip(modifiers: Modifier) { + const div = document.createElement('div'); + const tooltip = document.createElement('div'); + updateTooltip( + div, + { + name: 'tooltip', + modifiers: modifiers + }, + tooltip + ); + return tooltip; + } + + test('top (default)', () => { + const tooltip = testUpdateTooltip({ + right: false, + bottom: false, + left: false, + start: false, + end: false, + inverted: false + }); + expect(tooltip.className).toBe('top'); + expect(tooltip.getAttribute('style')).toBe( + 'transform: translate(calc(0px - 50%), calc(-10px - 100%));' + ); + }); + + test('top start', () => { + const tooltip = testUpdateTooltip({ + right: false, + bottom: false, + left: false, + start: true, + end: false, + inverted: false + }); + expect(tooltip.className).toBe('start top'); + expect(tooltip.getAttribute('style')).toBe( + 'transform: translate(calc(20px - 100%), calc(-10px - 100%));' + ); + }); + + test('top end', () => { + const tooltip = testUpdateTooltip({ + right: false, + bottom: false, + left: false, + start: false, + end: true, + inverted: false + }); + expect(tooltip.className).toBe('end top'); + expect(tooltip.getAttribute('style')).toBe( + 'transform: translate(calc(-20px - 0%), calc(-10px - 100%));' + ); + }); + + test('right', () => { + const tooltip = testUpdateTooltip({ + right: true, + bottom: false, + left: false, + start: false, + end: false, + inverted: false + }); + expect(tooltip.className).toBe('right'); + expect(tooltip.getAttribute('style')).toBe( + 'transform: translate(10px, calc(0px - 50%));' + ); + }); + + test('right start', () => { + const tooltip = testUpdateTooltip({ + right: true, + bottom: false, + left: false, + start: true, + end: false, + inverted: false + }); + expect(tooltip.className).toBe('start right'); + expect(tooltip.getAttribute('style')).toBe( + 'transform: translate(10px, calc(20px - 100%));' + ); + }); + + test('right end', () => { + const tooltip = testUpdateTooltip({ + right: true, + bottom: false, + left: false, + start: false, + end: true, + inverted: false + }); + expect(tooltip.className).toBe('end right'); + expect(tooltip.getAttribute('style')).toBe( + 'transform: translate(10px, calc(-20px - 0%));' + ); + }); + + test('bottom', () => { + const tooltip = testUpdateTooltip({ + right: false, + bottom: true, + left: false, + start: false, + end: false, + inverted: false + }); + expect(tooltip.className).toBe('bottom'); + expect(tooltip.getAttribute('style')).toBe( + 'transform: translate(calc(0px - 50%), 10px);' + ); + }); + + test('bottom start', () => { + const tooltip = testUpdateTooltip({ + right: false, + bottom: true, + left: false, + start: true, + end: false, + inverted: false + }); + expect(tooltip.className).toBe('start bottom'); + expect(tooltip.getAttribute('style')).toBe( + 'transform: translate(calc(20px - 100%), 10px);' + ); + }); + + test('bottom end', () => { + const tooltip = testUpdateTooltip({ + right: false, + bottom: true, + left: false, + start: false, + end: true, + inverted: false + }); + expect(tooltip.className).toBe('end bottom'); + expect(tooltip.getAttribute('style')).toBe( + 'transform: translate(calc(-20px - 0%), 10px);' + ); + }); + + test('left', () => { + const tooltip = testUpdateTooltip({ + right: false, + bottom: false, + left: true, + start: false, + end: false, + inverted: false + }); + expect(tooltip.className).toBe('left'); + expect(tooltip.getAttribute('style')).toBe( + 'transform: translate(calc(-10px - 100%), calc(0px - 50%));' + ); + }); + + test('left start', () => { + const tooltip = testUpdateTooltip({ + right: false, + bottom: false, + left: true, + start: true, + end: false, + inverted: false + }); + expect(tooltip.className).toBe('start left'); + expect(tooltip.getAttribute('style')).toBe( + 'transform: translate(calc(-10px - 100%), calc(20px - 100%));' + ); + }); + + test('left end', () => { + const tooltip = testUpdateTooltip({ + right: false, + bottom: false, + left: true, + start: false, + end: true, + inverted: false + }); + expect(tooltip.className).toBe('end left'); + expect(tooltip.getAttribute('style')).toBe( + 'transform: translate(calc(-10px - 100%), calc(-20px - 0%));' + ); + }); + + test('Inverted', () => { + const tooltip = testUpdateTooltip({ + right: false, + bottom: false, + left: false, + start: false, + end: false, + inverted: true + }); + expect(tooltip.className).toBe('inverted top'); + }); + }); + }); + + describe('onLeaveTooltip', () => { + it('Clears the timeout', () => { + onLeaveTooltip(); + expect(clearTimeout).toHaveBeenCalled(); + }); + }); + + describe('animateIn / animateOut', () => { + it('Adds the appropriate classes on entering', () => { + const div = document.createElement('div'); + animateIn(div); + + expect(div.classList).toContain('enter'); + jest.advanceTimersByTime(5); + expect(div.classList).toContain('enter-active'); + jest.advanceTimersByTime(225); + expect(div.classList.contains('enter-active')).toBe(false); + }); + + it('Stops animating in when it already has enter / enter-active class', () => { + const tooltip = document.createElement('div'); + + animateIn(tooltip); + tooltip.classList.remove('enter'); + + jest.advanceTimersByTime(5); + expect(tooltip.classList.contains('enter-active')).toBe(false); + jest.advanceTimersByTime(225); + expect(tooltip.classList.contains('enter-active')).toBe(false); + }); + + it('Adds the appropriate classes on leave', () => { + const div = document.createElement('div'); + div.classList.add('visible'); + + animateOut(div); + + expect(div.classList).toContain('leave'); + jest.advanceTimersByTime(5); + expect(div.classList).toContain('leave-active'); + jest.advanceTimersByTime(225); + expect(div.classList.contains('leave-active')).toBe(false); + }); + + it('Stops animating out when it does not have leave / leave-active class', () => { + const tooltip = document.createElement('div'); + tooltip.classList.add('visible'); + + animateOut(tooltip); + tooltip.classList.remove('leave'); + + jest.advanceTimersByTime(5); + expect(tooltip.classList.contains('leave-active')).toBe(false); + jest.advanceTimersByTime(225); + expect(tooltip.classList.contains('visible')).toBe(true); + }); + }); + + describe('getTooltip', () => { + it('Creates a new div element if tooltip does not exist yet', () => { + const spy = jest.spyOn(document, 'createElement'); + getTooltip(); + expect(spy).toHaveBeenCalledWith('div'); + }); + + it('Returns the existing div if found in dom', () => { + const div = document.createElement('div'); + div.id = 'tooltip'; + div.setAttribute('test', 'true'); + document.body.appendChild(div); + + const returnedDiv = getTooltip(); + expect(returnedDiv.getAttribute('test')).toBe('true'); + }); + }); +}); diff --git a/src/directives/tooltip/tooltip.ts b/src/directives/tooltip/tooltip.ts new file mode 100644 index 0000000000..e337935660 --- /dev/null +++ b/src/directives/tooltip/tooltip.ts @@ -0,0 +1,175 @@ +import { DirectiveOptions } from 'vue'; +import { DirectiveBinding } from 'vue/types/options'; + +const Tooltip: DirectiveOptions = { + inserted(element, binding) { + element.onmouseenter = () => onEnterTooltip(element, binding); + element.onmouseleave = () => onLeaveTooltip(); + } +}; + +export default Tooltip; + +let tooltipTimer: number; + +export function onEnterTooltip(element: HTMLElement, binding: DirectiveBinding) { + const tooltip = getTooltip(); + + if (binding.modifiers.instant) { + animateIn(tooltip); + updateTooltip(element, binding, tooltip); + } else { + tooltipTimer = setTimeout(() => { + animateIn(tooltip); + updateTooltip(element, binding, tooltip); + }, 600); + } +} + +export function updateTooltip( + element: HTMLElement, + binding: DirectiveBinding, + tooltip: HTMLElement +) { + const offset = 10; + const arrowAlign = 20; + + const bounds = element.getBoundingClientRect(); + let top = bounds.top + pageYOffset; + let left = bounds.left + pageXOffset; + let transformPos; + + tooltip.innerText = binding.value; + tooltip.classList.remove('top', 'bottom', 'left', 'right', 'start', 'end'); + + if (binding.modifiers.inverted) { + tooltip.classList.add('inverted'); + } else { + tooltip.classList.remove('inverted'); + } + + if (binding.modifiers.bottom) { + if (binding.modifiers.start) { + left += arrowAlign; + transformPos = 100; + tooltip.classList.add('start'); + } else if (binding.modifiers.end) { + left += bounds.width - arrowAlign; + transformPos = 0; + tooltip.classList.add('end'); + } else { + left += bounds.width / 2; + transformPos = 50; + } + + top += bounds.height + offset; + tooltip.style.transform = `translate(calc(${left}px - ${transformPos}%), ${top}px)`; + tooltip.classList.add('bottom'); + } else if (binding.modifiers.left) { + if (binding.modifiers.start) { + top += arrowAlign; + transformPos = 100; + tooltip.classList.add('start'); + } else if (binding.modifiers.end) { + top += bounds.height - arrowAlign; + transformPos = 0; + tooltip.classList.add('end'); + } else { + top += bounds.height / 2; + transformPos = 50; + } + + left -= offset; + tooltip.style.transform = `translate(calc(${left}px - 100%), calc(${top}px - ${transformPos}%))`; + tooltip.classList.add('left'); + } else if (binding.modifiers.right) { + if (binding.modifiers.start) { + top += arrowAlign; + transformPos = 100; + tooltip.classList.add('start'); + } else if (binding.modifiers.end) { + top += bounds.height - arrowAlign; + transformPos = 0; + tooltip.classList.add('end'); + } else { + top += bounds.height / 2; + transformPos = 50; + } + + left += bounds.width + offset; + tooltip.style.transform = `translate(${left}px, calc(${top}px - ${transformPos}%))`; + tooltip.classList.add('right'); + } else { + if (binding.modifiers.start) { + left += arrowAlign; + transformPos = 100; + tooltip.classList.add('start'); + } else if (binding.modifiers.end) { + left += bounds.width - arrowAlign; + transformPos = 0; + tooltip.classList.add('end'); + } else { + left += bounds.width / 2; + transformPos = 50; + } + + top -= offset; + tooltip.style.transform = `translate(calc(${left}px - ${transformPos}%), calc(${top}px - 100%))`; + tooltip.classList.add('top'); + } +} + +export function onLeaveTooltip() { + const tooltip = getTooltip(); + + clearTimeout(tooltipTimer); + animateOut(tooltip); +} + +export function animateIn(tooltip: HTMLElement) { + tooltip.classList.add('visible', 'enter'); + tooltip.classList.remove('leave', 'leave-active'); + + setTimeout(() => { + if (tooltip.classList.contains('enter') === false) return; + tooltip.classList.add('enter-active'); + tooltip.classList.remove('enter'); + }, 1); + + setTimeout(() => { + tooltip.classList.remove('enter-active'); + }, 200); +} + +export function animateOut(tooltip: HTMLElement) { + if (tooltip.classList.contains('visible') === false) return; + + tooltip.classList.add('visible', 'leave'); + tooltip.classList.remove('enter', 'enter-active'); + + setTimeout(() => { + if (tooltip.classList.contains('leave') === false) return; + tooltip.classList.add('leave-active'); + tooltip.classList.remove('leave'); + }, 1); + + setTimeout(() => { + if (tooltip.classList.contains('leave-active') === false) return; + tooltip.classList.remove('leave-active'); + tooltip.classList.remove('visible'); + }, 200); +} + +export function getTooltip(): HTMLElement { + let tooltip = document.getElementById('tooltip'); + + if (tooltip instanceof HTMLElement) { + return tooltip; + } + + tooltip = document.createElement('div'); + tooltip.id = 'tooltip'; + document.body.appendChild(tooltip); + + return tooltip; +} diff --git a/src/main.ts b/src/main.ts index d50b101d25..185e9593cd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,6 +2,7 @@ import Vue from 'vue'; import './styles/main.scss'; import './plugins'; +import './directives/register'; import './components/register'; import './modules/register'; diff --git a/src/plugins.ts b/src/plugins.ts index ebf2b114eb..06b1b4ccbd 100644 --- a/src/plugins.ts +++ b/src/plugins.ts @@ -1,8 +1,6 @@ import Vue from 'vue'; import VueCompositionAPI from '@vue/composition-api'; import VueRouter from 'vue-router'; -import { VTooltip } from 'v-tooltip'; Vue.use(VueCompositionAPI); Vue.use(VueRouter); -Vue.directive('tooltip', VTooltip); diff --git a/src/styles/_tooltip.scss b/src/styles/_tooltip.scss new file mode 100644 index 0000000000..88d4ff9be3 --- /dev/null +++ b/src/styles/_tooltip.scss @@ -0,0 +1,110 @@ +#tooltip { + $arrow-alignment: 5px; + + position: absolute; + top: 0; + left: 0; + display: none; + max-width: 260px; + padding: 4px 8px; + color: var(--tooltip-foreground-color); + background-color: var(--tooltip-background-color); + border-radius: 4px; + transition: opacity 200ms; + + &.inverted { + --tooltip-foreground-color: var(--tooltip-foreground-color-inverted); + --tooltip-background-color: var(--tooltip-background-color-inverted); + } + + &.visible { + display: block; + } + + &.enter, + &.leave-active { + opacity: 0; + } + + &.enter-active, + &.leave { + opacity: 1; + } + + &::after { + position: absolute; + top: -5px; + left: calc(50% - 5px); + width: 0; + height: 0; + border-right: 5px solid transparent; + border-bottom: 5px solid var(--tooltip-background-color); + border-left: 5px solid transparent; + content: ''; + } + + &.start::after { + right: $arrow-alignment; + left: unset; + } + + &.end::after { + left: $arrow-alignment; + } + + &.top::after { + top: unset; + bottom: -5px; + left: calc(50% - 5px); + border-top: 5px solid var(--tooltip-background-color); + border-right: 5px solid transparent; + border-bottom: unset; + border-left: 5px solid transparent; + } + + &.top.start::after { + right: $arrow-alignment; + left: unset; + } + + &.top.end::after { + left: $arrow-alignment; + } + + &.left::after { + top: calc(50% - 5px); + right: -5px; + left: unset; + border-top: 5px solid transparent; + border-right: unset; + border-bottom: 5px solid transparent; + border-left: 5px solid var(--tooltip-background-color); + } + + &.left.start::after { + top: unset; + bottom: $arrow-alignment; + } + + &.left.end::after { + top: $arrow-alignment; + } + + &.right::after { + top: calc(50% - 5px); + left: -5px; + border-top: 5px solid transparent; + border-right: 5px solid var(--tooltip-background-color); + border-bottom: 5px solid transparent; + border-left: unset; + } + + &.right.start::after { + top: unset; + bottom: $arrow-alignment; + } + + &.right.end::after { + top: $arrow-alignment; + } +} diff --git a/src/styles/lib/_tooltip.scss b/src/styles/lib/_tooltip.scss deleted file mode 100644 index b3bfbfde29..0000000000 --- a/src/styles/lib/_tooltip.scss +++ /dev/null @@ -1,216 +0,0 @@ -.tooltip { - z-index: 10000; - display: block !important; - background: var(--tooltip-background-color); - border-radius: 3px; - - & .tooltip-inner { - padding: 5px 10px 6px; - color: var(--tooltip-text-color); - } - - & .tooltip-arrow { - position: absolute; - width: 0; - height: 0; - margin: 5px; - border-color: var(--tooltip-background-color); - border-style: solid; - } - - &.popover-arrow { - &::after { - border-color: var(--tooltip-background-color); - } - } - - &.popover { - background: var(--popover-background-color); - border: 2px solid var(--input-border-color); - } - - & .popover-inner { - padding: 6px 0; - color: var(--black); - } - - & .popover-arrow { - border-color: var(--input-border-color); - - &::after { - border-color: var(--popover-background-color); - } - } - - &.inverted { - background: var(--blue-grey-50); - - & .tooltip-inner { - color: var(--blue-grey-800); - } - - & .tooltip-arrow { - border-color: var(--blue-grey-50); - } - } - - &[x-placement^='top'] { - margin-bottom: 4px; - } - - &[x-placement^='top'] .tooltip-arrow { - bottom: -4px; - left: calc(50% - 4px); - margin-top: 0; - margin-bottom: 0; - border-width: 4px 4px 0 4px; - border-right-color: transparent !important; - border-bottom-color: transparent !important; - border-left-color: transparent !important; - - &.popover-arrow { - bottom: -8px; - left: calc(50% - 8px); - border-width: 8px 8px 0 8px; - - &::after { - position: absolute; - bottom: 3px; - left: calc(50% - 8px); - width: 0; - height: 0; - margin-top: 0; - margin-bottom: 0; - border-style: solid; - border-width: 8px 8px 0 8px; - border-right-color: transparent !important; - border-bottom-color: transparent !important; - border-left-color: transparent !important; - } - } - } - - &[x-placement^='bottom'] { - margin-top: 4px; - } - - &[x-placement^='bottom'] .tooltip-arrow { - top: -4px; - left: calc(50% - 4px); - margin-top: 0; - margin-bottom: 0; - border-width: 0 4px 4px 4px; - border-top-color: transparent !important; - border-right-color: transparent !important; - border-left-color: transparent !important; - - &.popover-arrow { - top: -8px; - left: calc(50% - 8px); - border-width: 0 8px 8px 8px; - - &::after { - position: absolute; - top: 3px; - left: calc(50% - 8px); - width: 0; - height: 0; - margin-top: 0; - margin-bottom: 0; - border-style: solid; - border-width: 0 8px 8px 8px; - border-top-color: transparent !important; - border-right-color: transparent !important; - border-left-color: transparent !important; - content: ''; - } - } - } - - &[x-placement^='right'] { - margin-left: 4px; - } - - &[x-placement^='right'] .tooltip-arrow { - top: calc(50% - 4px); - left: -4px; - margin-right: 0; - margin-left: 0; - border-width: 4px 4px 4px 0; - border-top-color: transparent !important; - border-bottom-color: transparent !important; - border-left-color: transparent !important; - - &.popover-arrow { - top: calc(50% - 8px); - left: -8px; - border-width: 8px 8px 8px 0; - - &::after { - position: absolute; - top: calc(50% - 8px); - left: 3px; - width: 0; - height: 0; - margin-right: 0; - margin-left: 0; - border-style: solid; - border-width: 8px 8px 8px 0; - border-top-color: transparent !important; - border-bottom-color: transparent !important; - border-left-color: transparent !important; - content: ''; - } - } - } - - &[x-placement^='left'] { - margin-right: 4px; - } - - &[x-placement^='left'] .tooltip-arrow { - top: calc(50% - 4px); - right: -4px; - margin-right: 0; - margin-left: 0; - border-width: 4px 0 4px 4px; - border-top-color: transparent !important; - border-right-color: transparent !important; - border-bottom-color: transparent !important; - - &.popover-arrow { - top: calc(50% - 8px); - right: -8px; - border-width: 8px 0 8px 8px; - - &::after { - position: absolute; - top: calc(50% - 8px); - right: 3px; - width: 0; - height: 0; - margin-right: 0; - margin-left: 0; - border-style: solid; - border-width: 8px 0 8px 8px; - border-top-color: transparent !important; - border-right-color: transparent !important; - border-bottom-color: transparent !important; - content: ''; - } - } - } - - &[aria-hidden='true'] { - visibility: hidden; - opacity: 0; - transition: var(--fast) var(--transition-in); - transition-property: opacity, visibility; - } - - &[aria-hidden='false'] { - visibility: visible; - opacity: 1; - transition: var(--fast) var(--transition-out); - } -} diff --git a/src/styles/main.scss b/src/styles/main.scss index 3ba6306d0b..289ccd1e1a 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -3,7 +3,7 @@ @import 'fonts'; @import 'type-styles'; @import 'variables'; +@import 'tooltip'; @import 'themes/default'; @import 'themes/dark-mode'; @import 'lib/codemirror'; -@import 'lib/tooltip'; diff --git a/src/styles/themes/_default.scss b/src/styles/themes/_default.scss index 23e30bdfac..f606e320db 100644 --- a/src/styles/themes/_default.scss +++ b/src/styles/themes/_default.scss @@ -82,4 +82,9 @@ body { */ --overlay-color: rgba(38, 50, 56, 0.75); --divider-color: var(--blue-grey-50); + + --tooltip-foreground-color: var(--button-primary-foreground-color); + --tooltip-background-color: var(--button-primary-background-color-hover); + --tooltip-foreground-color-inverted: var(--input-foreground-color); + --tooltip-background-color-inverted: var(--button-secondary-background-color); } diff --git a/src/views/public/public-view.test.ts b/src/views/public/public-view.test.ts index 0349806a58..886954fb58 100644 --- a/src/views/public/public-view.test.ts +++ b/src/views/public/public-view.test.ts @@ -1,14 +1,14 @@ import Vue from 'vue'; import VueCompositionAPI from '@vue/composition-api'; import { mount, createLocalVue, Wrapper } from '@vue/test-utils'; -import { VTooltip } from 'v-tooltip'; import VIcon from '@/components/v-icon/'; import { useProjectsStore, ProjectWithKey } from '@/stores/projects'; +import Tooltip from '@/directives/tooltip/tooltip'; const localVue = createLocalVue(); localVue.use(VueCompositionAPI); -localVue.directive('tooltip', VTooltip); localVue.component('v-icon', VIcon); +localVue.directive('tooltip', Tooltip); import PublicView from './public-view.vue'; diff --git a/yarn.lock b/yarn.lock index 3680b11443..844a5a5106 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10752,7 +10752,7 @@ polished@^3.3.1: dependencies: "@babel/runtime" "^7.6.3" -popper.js@^1.14.4, popper.js@^1.14.7, popper.js@^1.16.0: +popper.js@^1.14.4, popper.js@^1.14.7: version "1.16.1" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ== @@ -14435,15 +14435,6 @@ uuid@^3.0.1, uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -v-tooltip@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/v-tooltip/-/v-tooltip-2.0.3.tgz#34fd64096656f032b1616567bf62f6165c57d529" - integrity sha512-KZZY3s+dcijzZmV2qoDH4rYmjMZ9YKGBVoUznZKQX0e3c2GjpJm3Sldzz8HHH2Ud87JqhZPB4+4gyKZ6m98cKQ== - dependencies: - lodash "^4.17.15" - popper.js "^1.16.0" - vue-resize "^0.4.5" - v8-compile-cache@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" @@ -14569,11 +14560,6 @@ vue-loader@^15.7.1, vue-loader@^15.7.2, vue-loader@^15.8.3: vue-hot-reload-api "^2.3.0" vue-style-loader "^4.1.0" -vue-resize@^0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-0.4.5.tgz#4777a23042e3c05620d9cbda01c0b3cc5e32dcea" - integrity sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg== - vue-router@^3.1.3, vue-router@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.1.5.tgz#ff29b8a1e1306c526b52d4dc0532109f16c41231"