Commit e0b027b6 authored by Enrique Alcantara's avatar Enrique Alcantara

Allow to dispose tooltips

Tooltips should be disposed when their
target is removed. Also, provide an API
remove tooltips.
parent d7917641
......@@ -36,14 +36,42 @@ export default {
tooltips: [],
};
},
created() {
this.observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
this.dispose(mutation.removedNodes);
});
});
},
methods: {
addTooltips(elements, config) {
const newTooltips = elements
.filter(element => !this.tooltipExists(element))
.map(element => newTooltip(element, config));
newTooltips.forEach(tooltip => this.observe(tooltip));
this.tooltips.push(...newTooltips);
},
observe(tooltip) {
this.observer.observe(tooltip.target.parentElement, {
childList: true,
});
},
dispose(elements) {
if (!elements) {
this.tooltips = [];
return;
}
elements.forEach(element => {
const index = this.tooltips.findIndex(tooltip => tooltip.target === element);
if (index > -1) {
this.tooltips.splice(index, 1);
}
});
},
tooltipExists(element) {
return this.tooltips.some(tooltip => tooltip.target === element);
},
......
......@@ -10,9 +10,15 @@ const EVENTS_MAP = {
};
const DEFAULT_TRIGGER = 'hover focus';
const APP_ELEMENT_ID = 'gl-tooltips-app';
const tooltipsApp = () => {
if (!app) {
const container = document.createElement('div');
container.setAttribute('id', APP_ELEMENT_ID);
document.body.appendChild(container);
app = new Vue({
render(h) {
return h(Tooltips, {
......@@ -22,7 +28,7 @@ const tooltipsApp = () => {
ref: 'tooltips',
});
},
}).$mount();
}).$mount(container);
}
return app;
......@@ -56,3 +62,12 @@ export const initTooltips = (selector, config = {}) => {
return tooltipsApp();
};
export const dispose = elements => {
return tooltipsApp().$refs.tooltips.dispose(elements);
};
export const destroy = () => {
tooltipsApp().$destroy();
app = null;
};
import { shallowMount } from '@vue/test-utils';
import { GlTooltip } from '@gitlab/ui';
import Tooltips from '~/tooltips/components/tooltips.vue';
import { useMockMutationObserver } from 'helpers/mock_dom_observer';
describe('tooltips/components/tooltips.vue', () => {
const { trigger: triggerMutate } = useMockMutationObserver();
let wrapper;
const buildWrapper = () => {
......@@ -20,11 +22,16 @@ describe('tooltips/components/tooltips.vue', () => {
target.setAttribute(name, defaults[name]);
});
document.body.appendChild(target);
return target;
};
const allTooltips = () => wrapper.findAll(GlTooltip);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('addTooltips', () => {
......@@ -91,4 +98,53 @@ describe('tooltips/components/tooltips.vue', () => {
},
);
});
describe('dispose', () => {
beforeEach(() => {
buildWrapper();
});
it('removes all tooltips when elements is nil', async () => {
wrapper.vm.addTooltips([createTooltipTarget(), createTooltipTarget()]);
await wrapper.vm.$nextTick();
wrapper.vm.dispose();
await wrapper.vm.$nextTick();
expect(allTooltips()).toHaveLength(0);
});
it('removes the tooltips that target the elements specified', async () => {
const target = createTooltipTarget();
wrapper.vm.addTooltips([target, createTooltipTarget()]);
await wrapper.vm.$nextTick();
wrapper.vm.dispose([target]);
await wrapper.vm.$nextTick();
expect(allTooltips()).toHaveLength(1);
});
});
describe('observe', () => {
beforeEach(() => {
buildWrapper();
});
it('removes tooltip when target is removed from the document', async () => {
const target = createTooltipTarget();
wrapper.vm.addTooltips([target, createTooltipTarget()]);
await wrapper.vm.$nextTick();
triggerMutate(document.body, {
entry: { removedNodes: [target] },
options: { childList: true },
});
await wrapper.vm.$nextTick();
expect(allTooltips()).toHaveLength(1);
});
});
});
import { initTooltips } from '~/tooltips';
import { initTooltips, dispose, destroy } from '~/tooltips';
describe('tooltips/index.js', () => {
let tooltipsApp;
const createTooltipTarget = () => {
const target = document.createElement('button');
const attributes = {
......@@ -13,22 +15,31 @@ describe('tooltips/index.js', () => {
target.classList.add('has-tooltip');
document.body.appendChild(target);
return target;
};
const buildTooltipsApp = () => {
tooltipsApp = initTooltips('.has-tooltip');
};
const triggerEvent = (target, eventName = 'mouseenter') => {
const event = new Event(eventName);
target.dispatchEvent(event);
};
afterEach(() => {
document.body.childNodes.forEach(node => node.remove());
destroy();
});
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);
buildTooltipsApp();
triggerEvent(target);
......@@ -40,13 +51,8 @@ describe('tooltips/index.js', () => {
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);
buildTooltipsApp();
triggerEvent(target, 'click');
await tooltipsApp.$nextTick();
......@@ -55,4 +61,23 @@ describe('tooltips/index.js', () => {
expect(document.querySelector('.gl-tooltip').innerHTML).toContain('default title');
});
});
describe('dispose', () => {
it('removes tooltips that target the elements specified', async () => {
const target = createTooltipTarget();
buildTooltipsApp();
triggerEvent(target);
await tooltipsApp.$nextTick();
expect(document.querySelector('.gl-tooltip')).not.toBe(null);
dispose([target]);
await tooltipsApp.$nextTick();
expect(document.querySelector('.gl-tooltip')).toBe(null);
});
});
});
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