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 <rijkvanzanten@me.com>
This commit is contained in:
Nitwel
2020-02-18 23:58:24 +01:00
committed by GitHub
parent e2664ad3fb
commit 2dc48e8edf
22 changed files with 784 additions and 251 deletions

View File

@@ -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: {

View File

@@ -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",

View File

@@ -1,9 +0,0 @@
import Vue, { DirectiveOptions, DirectiveFunction } from 'vue';
export const definition: DirectiveOptions = {
inserted(el) {
el.focus();
}
};
Vue.directive('focus', definition);

View File

@@ -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
<v-input v-focus />
```

View File

@@ -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: `
<div style="display: flex; justify-content: space-around; flex-direction: column; align-items: center">
<v-input style="margin-bottom: 20px;" />
<v-input style="margin-bottom: 20px;" />
<v-input v-focus style="margin-bottom: 20px;" />
</div>
`
});

View File

@@ -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();
});
});

View File

@@ -0,0 +1,9 @@
import { DirectiveOptions } from 'vue';
const Focus: DirectiveOptions = {
inserted(el) {
el.focus();
}
};
export default Focus;

View File

@@ -1,2 +0,0 @@
import './focus';
import './tooltip';

View File

@@ -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);

View File

@@ -1 +0,0 @@
console.log('hi');

View File

@@ -0,0 +1,21 @@
# Tooltip
The tooltip can display more detailed information about an element to clarify its use.
```html
<v-button v-tooltip="it does absolutely nothing">This is a button</v-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 |

View File

@@ -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: `
<div style="display: flex; justify-content: space-around; flex-direction: column; align-items: center">
<div>
<v-button style="width: 150px; margin: 30px" v-tooltip.bottom.start="'Default Tooltip'">Bottom start</v-button>
<v-button style="width: 150px; margin: 30px" v-tooltip.bottom="'Default Tooltip'">Bottom</v-button>
<v-button style="width: 150px; margin: 30px" v-tooltip.bottom.end="'Default Tooltip'">Bottom end</v-button>
</div>
<div>
<v-button style="width: 150px; margin: 30px" v-tooltip.left.start="'Default Tooltip'">Left start</v-button>
<v-button style="width: 150px; margin: 30px" v-tooltip.left="'Default Tooltip'">Left</v-button>
<v-button style="width: 150px; margin: 30px" v-tooltip.left.end="'Default Tooltip'">Left end</v-button>
</div>
<div>
<v-button style="width: 150px; margin: 30px" v-tooltip.right.start="'Default Tooltip'">Right start</v-button>
<v-button style="width: 150px; margin: 30px" v-tooltip.right="'Default Tooltip'">Right</v-button>
<v-button style="width: 150px; margin: 30px" v-tooltip.right.end="'Default Tooltip'">Right end</v-button>
</div>
<div>
<v-button style="width: 150px; margin: 30px" v-tooltip.start="'Default Tooltip'">Top start</v-button>
<v-button style="width: 150px; margin: 30px" v-tooltip="'Default Tooltip'">Top</v-button>
<v-button style="width: 150px; margin: 30px" v-tooltip.end="'Default Tooltip'">Top end</v-button>
</div>
<v-button style="width: 150px; margin: 30px" v-tooltip.instant="'Tooltip'">Instant Tooltip</v-button>
<v-button style="width: 150px; margin: 30px" v-tooltip.inverted="'Tooltip'">Inverted Tooltip</v-button>
<v-icon style="margin: 30px" v-tooltip.top="'Goto home menu'" name="home" />
<v-icon style="margin: 30px" v-tooltip.bottom="'This is a really long text which wont fit into a single line!'" name="info" />
<v-icon style="margin: 30px" v-tooltip.right="'This is a really long text which wont fit into a single line!'" name="info" />
</div>
`
});

View File

@@ -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');
});
});
});

View File

@@ -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;
}

View File

@@ -2,6 +2,7 @@ import Vue from 'vue';
import './styles/main.scss';
import './plugins';
import './directives/register';
import './components/register';
import './modules/register';

View File

@@ -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);

110
src/styles/_tooltip.scss Normal file
View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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';

View File

@@ -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);
}

View File

@@ -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';

View File

@@ -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"