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 { ...@@ -36,14 +36,42 @@ export default {
tooltips: [], tooltips: [],
}; };
}, },
created() {
this.observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
this.dispose(mutation.removedNodes);
});
});
},
methods: { methods: {
addTooltips(elements, config) { addTooltips(elements, config) {
const newTooltips = elements const newTooltips = elements
.filter(element => !this.tooltipExists(element)) .filter(element => !this.tooltipExists(element))
.map(element => newTooltip(element, config)); .map(element => newTooltip(element, config));
newTooltips.forEach(tooltip => this.observe(tooltip));
this.tooltips.push(...newTooltips); 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) { tooltipExists(element) {
return this.tooltips.some(tooltip => tooltip.target === element); return this.tooltips.some(tooltip => tooltip.target === element);
}, },
......
...@@ -10,9 +10,15 @@ const EVENTS_MAP = { ...@@ -10,9 +10,15 @@ const EVENTS_MAP = {
}; };
const DEFAULT_TRIGGER = 'hover focus'; const DEFAULT_TRIGGER = 'hover focus';
const APP_ELEMENT_ID = 'gl-tooltips-app';
const tooltipsApp = () => { const tooltipsApp = () => {
if (!app) { if (!app) {
const container = document.createElement('div');
container.setAttribute('id', APP_ELEMENT_ID);
document.body.appendChild(container);
app = new Vue({ app = new Vue({
render(h) { render(h) {
return h(Tooltips, { return h(Tooltips, {
...@@ -22,7 +28,7 @@ const tooltipsApp = () => { ...@@ -22,7 +28,7 @@ const tooltipsApp = () => {
ref: 'tooltips', ref: 'tooltips',
}); });
}, },
}).$mount(); }).$mount(container);
} }
return app; return app;
...@@ -56,3 +62,12 @@ export const initTooltips = (selector, config = {}) => { ...@@ -56,3 +62,12 @@ export const initTooltips = (selector, config = {}) => {
return tooltipsApp(); 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 { shallowMount } from '@vue/test-utils';
import { GlTooltip } from '@gitlab/ui'; import { GlTooltip } from '@gitlab/ui';
import Tooltips from '~/tooltips/components/tooltips.vue'; import Tooltips from '~/tooltips/components/tooltips.vue';
import { useMockMutationObserver } from 'helpers/mock_dom_observer';
describe('tooltips/components/tooltips.vue', () => { describe('tooltips/components/tooltips.vue', () => {
const { trigger: triggerMutate } = useMockMutationObserver();
let wrapper; let wrapper;
const buildWrapper = () => { const buildWrapper = () => {
...@@ -20,11 +22,16 @@ describe('tooltips/components/tooltips.vue', () => { ...@@ -20,11 +22,16 @@ describe('tooltips/components/tooltips.vue', () => {
target.setAttribute(name, defaults[name]); target.setAttribute(name, defaults[name]);
}); });
document.body.appendChild(target);
return target; return target;
}; };
const allTooltips = () => wrapper.findAll(GlTooltip);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('addTooltips', () => { describe('addTooltips', () => {
...@@ -91,4 +98,53 @@ describe('tooltips/components/tooltips.vue', () => { ...@@ -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', () => { describe('tooltips/index.js', () => {
let tooltipsApp;
const createTooltipTarget = () => { const createTooltipTarget = () => {
const target = document.createElement('button'); const target = document.createElement('button');
const attributes = { const attributes = {
...@@ -13,22 +15,31 @@ describe('tooltips/index.js', () => { ...@@ -13,22 +15,31 @@ describe('tooltips/index.js', () => {
target.classList.add('has-tooltip'); target.classList.add('has-tooltip');
document.body.appendChild(target);
return target; return target;
}; };
const buildTooltipsApp = () => {
tooltipsApp = initTooltips('.has-tooltip');
};
const triggerEvent = (target, eventName = 'mouseenter') => { const triggerEvent = (target, eventName = 'mouseenter') => {
const event = new Event(eventName); const event = new Event(eventName);
target.dispatchEvent(event); target.dispatchEvent(event);
}; };
afterEach(() => {
document.body.childNodes.forEach(node => node.remove());
destroy();
});
describe('initTooltip', () => { describe('initTooltip', () => {
it('attaches a GlTooltip for the elements specified in the selector', async () => { it('attaches a GlTooltip for the elements specified in the selector', async () => {
const target = createTooltipTarget(); const target = createTooltipTarget();
const tooltipsApp = initTooltips('.has-tooltip');
document.body.appendChild(tooltipsApp.$el); buildTooltipsApp();
document.body.appendChild(target);
triggerEvent(target); triggerEvent(target);
...@@ -40,13 +51,8 @@ describe('tooltips/index.js', () => { ...@@ -40,13 +51,8 @@ describe('tooltips/index.js', () => {
it('supports triggering a tooltip in custom events', async () => { it('supports triggering a tooltip in custom events', async () => {
const target = createTooltipTarget(); const target = createTooltipTarget();
const tooltipsApp = initTooltips('.has-tooltip', {
triggers: 'click',
});
document.body.appendChild(tooltipsApp.$el);
document.body.appendChild(target);
buildTooltipsApp();
triggerEvent(target, 'click'); triggerEvent(target, 'click');
await tooltipsApp.$nextTick(); await tooltipsApp.$nextTick();
...@@ -55,4 +61,23 @@ describe('tooltips/index.js', () => { ...@@ -55,4 +61,23 @@ describe('tooltips/index.js', () => {
expect(document.querySelector('.gl-tooltip').innerHTML).toContain('default title'); 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