Commit 71d74443 authored by Simon Knox's avatar Simon Knox

Merge branch 'dispose-tooltip-instances' into 'master'

GitLab UI tooltips in HAML/Vanilla JS: Dispose tooltips

See merge request gitlab-org/gitlab!39379
parents 26536559 f8dc00b4
......@@ -36,14 +36,45 @@ export default {
tooltips: [],
};
},
created() {
this.observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
this.dispose(mutation.removedNodes);
});
});
},
beforeDestroy() {
this.observer.disconnect();
},
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;
};
......@@ -84,7 +84,9 @@ const useMockObserver = (key, createMock) => {
mockObserver.$_triggerObserve(...args);
};
return { trigger };
const observersCount = () => mockObserver.$_observers.length;
return { trigger, observersCount };
};
export const useMockIntersectionObserver = () =>
......
import { shallowMount } from '@vue/test-utils';
import { GlTooltip } from '@gitlab/ui';
import { useMockMutationObserver } from 'helpers/mock_dom_observer';
import Tooltips from '~/tooltips/components/tooltips.vue';
describe('tooltips/components/tooltips.vue', () => {
const { trigger: triggerMutate, observersCount } = 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,63 @@ 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);
});
});
it('disconnects mutation observer on beforeDestroy', () => {
buildWrapper();
wrapper.vm.addTooltips([createTooltipTarget()]);
expect(observersCount()).toBe(1);
wrapper.destroy();
expect(observersCount()).toBe(0);
});
});
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