Commit f1e74d6b authored by Enrique Alcántara's avatar Enrique Alcántara

Merge branch 'prototype-gitlab-ui-tooltips-from-haml' into 'master'

GitLab UI tooltips in HAML/Vanilla JS: Display tooltips

See merge request gitlab-org/gitlab!36452
parents c32f8fd7 c73677c8
<script>
import { GlTooltip, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { uniqueId } from 'lodash';
const getTooltipTitle = element => {
return element.getAttribute('title') || element.dataset.title;
};
const newTooltip = (element, config = {}) => {
const { placement, container, boundary, html, triggers } = element.dataset;
const title = getTooltipTitle(element);
return {
id: uniqueId('gl-tooltip'),
target: element,
title,
html,
placement,
container,
boundary,
triggers,
disabled: !title,
...config,
};
};
export default {
components: {
GlTooltip,
},
directives: {
SafeHtml,
},
data() {
return {
tooltips: [],
};
},
methods: {
addTooltips(elements, config) {
const newTooltips = elements
.filter(element => !this.tooltipExists(element))
.map(element => newTooltip(element, config));
this.tooltips.push(...newTooltips);
},
tooltipExists(element) {
return this.tooltips.some(tooltip => tooltip.target === element);
},
},
};
</script>
<template>
<div>
<gl-tooltip
v-for="(tooltip, index) in tooltips"
:id="tooltip.id"
:key="index"
:target="tooltip.target"
:triggers="tooltip.triggers"
:placement="tooltip.placement"
:container="tooltip.container"
:boundary="tooltip.boundary"
:disabled="tooltip.disabled"
>
<span v-if="tooltip.html" v-safe-html="tooltip.title"></span>
<span v-else>{{ tooltip.title }}</span>
</gl-tooltip>
</div>
</template>
import Vue from 'vue';
import Tooltips from './components/tooltips.vue';
let app;
const EVENTS_MAP = {
hover: 'mouseenter',
click: 'click',
focus: 'focus',
};
const DEFAULT_TRIGGER = 'hover focus';
const tooltipsApp = () => {
if (!app) {
app = new Vue({
render(h) {
return h(Tooltips, {
props: {
elements: this.elements,
},
ref: 'tooltips',
});
},
}).$mount();
}
return app;
};
const isTooltip = (node, selector) => node.matches && node.matches(selector);
const addTooltips = (elements, config) => {
tooltipsApp().$refs.tooltips.addTooltips(Array.from(elements), config);
};
const handleTooltipEvent = (rootTarget, e, selector, config = {}) => {
for (let { target } = e; target && target !== rootTarget; target = target.parentNode) {
if (isTooltip(target, selector)) {
addTooltips([target], {
show: true,
...config,
});
break;
}
}
};
export const initTooltips = (selector, config = {}) => {
const triggers = config?.triggers || DEFAULT_TRIGGER;
const events = triggers.split(' ').map(trigger => EVENTS_MAP[trigger]);
events.forEach(event => {
document.addEventListener(event, e => handleTooltipEvent(document, e, selector, config), true);
});
return tooltipsApp();
};
......@@ -18,8 +18,16 @@ jest.mock('@gitlab/ui/dist/directives/tooltip.js', () => ({
}));
jest.mock('@gitlab/ui/dist/components/base/tooltip/tooltip.js', () => ({
props: ['target', 'id', 'triggers', 'placement', 'container', 'boundary', 'disabled'],
render(h) {
return h('div', this.$attrs, this.$slots.default);
return h(
'div',
{
class: 'gl-tooltip',
...this.$attrs,
},
this.$slots.default,
);
},
}));
......
import { shallowMount } from '@vue/test-utils';
import { GlTooltip } from '@gitlab/ui';
import Tooltips from '~/tooltips/components/tooltips.vue';
describe('tooltips/components/tooltips.vue', () => {
let wrapper;
const buildWrapper = () => {
wrapper = shallowMount(Tooltips);
};
const createTooltipTarget = (attributes = {}) => {
const target = document.createElement('button');
const defaults = {
title: 'default title',
...attributes,
};
Object.keys(defaults).forEach(name => {
target.setAttribute(name, defaults[name]);
});
return target;
};
afterEach(() => {
wrapper.destroy();
});
describe('addTooltips', () => {
let target;
beforeEach(() => {
buildWrapper();
target = createTooltipTarget();
});
it('attaches tooltips to the targets specified', async () => {
wrapper.vm.addTooltips([target]);
await wrapper.vm.$nextTick();
expect(wrapper.find(GlTooltip).props('target')).toBe(target);
});
it('does not attach a tooltip twice to the same element', async () => {
wrapper.vm.addTooltips([target]);
wrapper.vm.addTooltips([target]);
await wrapper.vm.$nextTick();
expect(wrapper.findAll(GlTooltip)).toHaveLength(1);
});
it('sets tooltip content from title attribute', async () => {
wrapper.vm.addTooltips([target]);
await wrapper.vm.$nextTick();
expect(wrapper.find(GlTooltip).text()).toBe(target.getAttribute('title'));
});
it('supports HTML content', async () => {
target = createTooltipTarget({
title: 'content with <b>HTML</b>',
'data-html': true,
});
wrapper.vm.addTooltips([target]);
await wrapper.vm.$nextTick();
expect(wrapper.find(GlTooltip).html()).toContain(target.getAttribute('title'));
});
it.each`
attribute | value | prop
${'data-placement'} | ${'bottom'} | ${'placement'}
${'data-container'} | ${'custom-container'} | ${'container'}
${'data-boundary'} | ${'viewport'} | ${'boundary'}
${'data-triggers'} | ${'manual'} | ${'triggers'}
`(
'sets $prop to $value when $attribute is set in target',
async ({ attribute, value, prop }) => {
target = createTooltipTarget({ [attribute]: value });
wrapper.vm.addTooltips([target]);
await wrapper.vm.$nextTick();
expect(wrapper.find(GlTooltip).props(prop)).toBe(value);
},
);
});
});
import { initTooltips } from '~/tooltips';
describe('tooltips/index.js', () => {
const createTooltipTarget = () => {
const target = document.createElement('button');
const attributes = {
title: 'default title',
};
Object.keys(attributes).forEach(name => {
target.setAttribute(name, attributes[name]);
});
target.classList.add('has-tooltip');
return target;
};
const triggerEvent = (target, eventName = 'mouseenter') => {
const event = new Event(eventName);
target.dispatchEvent(event);
};
describe('initTooltip', () => {
it('attaches a GlTooltip for the elements specified in the selector', async () => {
const target = createTooltipTarget();
const tooltipsApp = initTooltips('.has-tooltip');
document.body.appendChild(tooltipsApp.$el);
document.body.appendChild(target);
triggerEvent(target);
await tooltipsApp.$nextTick();
expect(document.querySelector('.gl-tooltip')).not.toBe(null);
expect(document.querySelector('.gl-tooltip').innerHTML).toContain('default title');
});
it('supports triggering a tooltip in custom events', async () => {
const target = createTooltipTarget();
const tooltipsApp = initTooltips('.has-tooltip', {
triggers: 'click',
});
document.body.appendChild(tooltipsApp.$el);
document.body.appendChild(target);
triggerEvent(target, 'click');
await tooltipsApp.$nextTick();
expect(document.querySelector('.gl-tooltip')).not.toBe(null);
expect(document.querySelector('.gl-tooltip').innerHTML).toContain('default title');
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment