Commit 12ac5227 authored by Stanislav Lashmanov's avatar Stanislav Lashmanov Committed by Stanislav Lashmanov

Lazy initialization for user popovers

User popovers are shown only on user interaction, so it is safe to lazy init them on that interaction.

Changelog: other
parent 4224b844
......@@ -57,41 +57,35 @@ const populateUserInfo = (user) => {
);
};
const initializedPopovers = new Map();
let domObservedForChanges = false;
function initPopover(el, user, mountPopover) {
const preloadedUserInfo = getPreloadedUserInfo(el.dataset);
const addPopoversToModifiedTree = new MutationObserver(() => {
const userLinks = document?.querySelectorAll('.js-user-link, .gfm-project_member');
Object.assign(user, preloadedUserInfo);
if (userLinks) {
addPopovers(userLinks); /* eslint-disable-line no-use-before-define */
if (preloadedUserInfo.userId) {
populateUserInfo(user);
}
});
function observeBody() {
if (!domObservedForChanges) {
addPopoversToModifiedTree.observe(document.body, {
subtree: true,
childList: true,
});
domObservedForChanges = true;
}
}
export default function addPopovers(elements = document.querySelectorAll('.js-user-link')) {
const userLinks = Array.from(elements);
const UserPopoverComponent = Vue.extend(UserPopover);
const popoverInstance = new UserPopoverComponent({
propsData: {
target: el,
user,
},
});
mountPopover(popoverInstance);
// wait for component to actually mount
setTimeout(() => {
// trigger an event to force tooltip to show
const event = new MouseEvent('mouseenter');
event.isSelfTriggered = true;
el.dispatchEvent(event);
});
}
observeBody();
return userLinks
.filter(({ dataset }) => dataset.user || dataset.userId)
.map((el) => {
if (initializedPopovers.has(el)) {
return initializedPopovers.get(el);
}
function initPopovers(userLinks, mountPopover) {
userLinks
.filter(({ dataset, user }) => !user && (dataset.user || dataset.userId))
.forEach((el) => {
const user = {
location: null,
bio: null,
......@@ -99,31 +93,52 @@ export default function addPopovers(elements = document.querySelectorAll('.js-us
status: null,
loaded: false,
};
const renderedPopover = new UserPopoverComponent({
propsData: {
target: el,
user,
},
el.user = user;
const init = initPopover.bind(null, el, user, mountPopover);
el.addEventListener('mouseenter', init, { once: true });
el.addEventListener('mouseenter', ({ target, isSelfTriggered }) => {
if (!isSelfTriggered) return;
removeTitle(target);
});
el.addEventListener('mouseleave', ({ target }) => {
target.removeAttribute('aria-describedby');
});
});
}
initializedPopovers.set(el, renderedPopover);
const isUserLinkNode = (node) =>
node.nodeType === 1 &&
(node.classList.contains('js-user-link') || node.classList.contains('gfm-project_member'));
renderedPopover.$mount();
let observer;
el.addEventListener('mouseenter', ({ target }) => {
removeTitle(target);
const preloadedUserInfo = getPreloadedUserInfo(target.dataset);
export default function addPopovers(
elements = document.querySelectorAll('.js-user-link'),
mountPopover = (popoverInstance) => popoverInstance.$mount(),
) {
const userLinks = Array.from(elements);
Object.assign(user, preloadedUserInfo);
initPopovers(userLinks, mountPopover);
if (preloadedUserInfo.userId) {
populateUserInfo(user);
}
});
el.addEventListener('mouseleave', ({ target }) => {
target.removeAttribute('aria-describedby');
});
if (!observer) {
observer = new MutationObserver((mutationsList) => {
mutationsList.filter((mutation) => mutation.type === 'childList' && mutation.addedNodes);
return renderedPopover;
const newUserLinks = mutationsList.flatMap((mutation) =>
Array.from(mutation.addedNodes).filter(isUserLinkNode),
);
if (newUserLinks.length !== 0) {
initPopovers(newUserLinks, mountPopover);
}
});
observer.observe(document.body, {
subtree: true,
childList: true,
});
document.addEventListener('beforeunload', () => {
observer.disconnect();
});
}
}
import UsersCache from '~/lib/utils/users_cache';
import waitUsingRealTimer from 'helpers/wait_using_real_timer';
import initUserPopovers from '~/user_popovers';
describe('User Popovers', () => {
......@@ -18,12 +19,13 @@ describe('User Popovers', () => {
return link;
};
const findPopovers = () => {
return Array.from(document.querySelectorAll('[data-testid="user-popover"]'));
};
const dummyUser = { name: 'root' };
const dummyUserStatus = { message: 'active' };
let popovers;
const triggerEvent = (eventName, el) => {
const event = new MouseEvent(eventName, {
bubbles: true,
......@@ -45,29 +47,49 @@ describe('User Popovers', () => {
.spyOn(UsersCache, 'retrieveStatusById')
.mockImplementation((userId) => userStatusCacheSpy(userId));
popovers = initUserPopovers(document.querySelectorAll(selector));
initUserPopovers(document.querySelectorAll(selector), (popoverInstance) => {
const mountingRoot = document.createElement('div');
document.body.appendChild(mountingRoot);
popoverInstance.$mount(mountingRoot);
});
});
it('initializes a popover for each user link with a user id', () => {
const linksWithUsers = findFixtureLinks();
describe('shows a placeholder popover on hover', () => {
let linksWithUsers;
beforeEach(() => {
linksWithUsers = findFixtureLinks();
linksWithUsers.forEach((el) => {
triggerEvent('mouseenter', el);
});
});
expect(linksWithUsers.length).toBe(popovers.length);
});
it('for initial links', () => {
expect(findPopovers().length).toBe(linksWithUsers.length);
});
it('adds popovers to user links added to the DOM tree after the initial call', async () => {
document.body.appendChild(createUserLink());
document.body.appendChild(createUserLink());
it('for elements added after initial load', async () => {
const addedLinks = [createUserLink(), createUserLink()];
addedLinks.forEach((link) => {
document.body.appendChild(link);
});
const linksWithUsers = findFixtureLinks();
await waitUsingRealTimer();
expect(linksWithUsers.length).toBe(popovers.length + 2);
addedLinks.forEach((link) => {
triggerEvent('mouseenter', link);
});
expect(findPopovers().length).toBe(linksWithUsers.length + addedLinks.length);
});
});
it('does not initialize the user popovers twice for the same element', () => {
const newPopovers = initUserPopovers(document.querySelectorAll(selector));
const samePopovers = popovers.every((popover, index) => newPopovers[index] === popover);
const [firstUserLink] = findFixtureLinks();
triggerEvent('mouseenter', firstUserLink);
triggerEvent('mouseleave', firstUserLink);
triggerEvent('mouseenter', firstUserLink);
expect(samePopovers).toBe(true);
expect(findPopovers().length).toBe(1);
});
describe('when user link emits mouseenter event', () => {
......@@ -86,11 +108,11 @@ describe('User Popovers', () => {
expect(userLink.dataset.originalTitle).toBeFalsy();
});
it('populates popovers with preloaded user data', () => {
it('populates popover with preloaded user data', () => {
const { name, userId, username } = userLink.dataset;
const [firstPopover] = popovers;
const [firstPopover] = findFixtureLinks();
expect(firstPopover.$props.user).toEqual(
expect(firstPopover.user).toEqual(
expect.objectContaining({
name,
userId,
......
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