2021-08-31 23:11:03 -04:00
|
|
|
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
|
|
|
|
import { SIDEBAR_COLLAPSED_CLASS } from '~/contextual_sidebar';
|
|
|
|
import {
|
|
|
|
calculateTop,
|
|
|
|
showSubLevelItems,
|
|
|
|
canShowSubItems,
|
|
|
|
canShowActiveSubItems,
|
|
|
|
mouseEnterTopItems,
|
|
|
|
mouseLeaveTopItem,
|
|
|
|
getOpenMenu,
|
|
|
|
setOpenMenu,
|
|
|
|
mousePos,
|
|
|
|
getHideSubItemsInterval,
|
|
|
|
documentMouseMove,
|
|
|
|
getHeaderHeight,
|
|
|
|
setSidebar,
|
|
|
|
subItemsMouseLeave,
|
|
|
|
} from '~/fly_out_nav';
|
|
|
|
|
|
|
|
describe('Fly out sidebar navigation', () => {
|
|
|
|
let el;
|
|
|
|
let breakpointSize = 'lg';
|
|
|
|
|
|
|
|
const OLD_SIDEBAR_WIDTH = 200;
|
|
|
|
const CONTAINER_INITIAL_BOUNDING_RECT = {
|
|
|
|
x: 8,
|
|
|
|
y: 8,
|
|
|
|
width: 769,
|
|
|
|
height: 0,
|
|
|
|
top: 8,
|
|
|
|
right: 777,
|
|
|
|
bottom: 8,
|
|
|
|
left: 8,
|
|
|
|
};
|
|
|
|
const SUB_ITEMS_INITIAL_BOUNDING_RECT = {
|
|
|
|
x: 148,
|
|
|
|
y: 8,
|
|
|
|
width: 0,
|
|
|
|
height: 150,
|
|
|
|
top: 8,
|
|
|
|
right: 148,
|
|
|
|
bottom: 158,
|
|
|
|
left: 148,
|
|
|
|
};
|
|
|
|
const mockBoundingClientRect = (elem, rect) => {
|
|
|
|
jest.spyOn(elem, 'getBoundingClientRect').mockReturnValue(rect);
|
|
|
|
};
|
|
|
|
|
|
|
|
const findSubItems = () => document.querySelector('.sidebar-sub-level-items');
|
|
|
|
const mockBoundingRects = () => {
|
|
|
|
const subItems = findSubItems();
|
|
|
|
mockBoundingClientRect(el, CONTAINER_INITIAL_BOUNDING_RECT);
|
|
|
|
mockBoundingClientRect(subItems, SUB_ITEMS_INITIAL_BOUNDING_RECT);
|
|
|
|
};
|
|
|
|
const mockSidebarFragment = (styleProps = '') =>
|
|
|
|
`<div class="sidebar-sub-level-items" style="${styleProps}"></div>`;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
el = document.createElement('div');
|
|
|
|
el.style.position = 'relative';
|
|
|
|
document.body.appendChild(el);
|
|
|
|
|
|
|
|
jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockImplementation(() => breakpointSize);
|
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
document.body.innerHTML = '';
|
|
|
|
breakpointSize = 'lg';
|
|
|
|
mousePos.length = 0;
|
|
|
|
|
|
|
|
setSidebar(null);
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('calculateTop', () => {
|
|
|
|
it('returns boundingRect top', () => {
|
|
|
|
const boundingRect = {
|
|
|
|
top: 100,
|
|
|
|
height: 100,
|
|
|
|
};
|
|
|
|
|
|
|
|
expect(calculateTop(boundingRect, 100)).toBe(100);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('getHideSubItemsInterval', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
el.innerHTML = mockSidebarFragment('position: fixed; top: 0; left: 100px; height: 150px;');
|
|
|
|
mockBoundingRects();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('returns 0 if currentOpenMenu is nil', () => {
|
|
|
|
setOpenMenu(null);
|
|
|
|
expect(getHideSubItemsInterval()).toBe(0);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('returns 0 if mousePos is empty', () => {
|
|
|
|
expect(getHideSubItemsInterval()).toBe(0);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('returns 0 when mouse above sub-items', () => {
|
|
|
|
showSubLevelItems(el);
|
|
|
|
documentMouseMove({
|
|
|
|
clientX: el.getBoundingClientRect().left,
|
|
|
|
clientY: el.getBoundingClientRect().top,
|
|
|
|
});
|
|
|
|
documentMouseMove({
|
|
|
|
clientX: el.getBoundingClientRect().left,
|
|
|
|
clientY: el.getBoundingClientRect().top - 50,
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(getHideSubItemsInterval()).toBe(0);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('returns 0 when mouse is below sub-items', () => {
|
|
|
|
const subItems = findSubItems();
|
|
|
|
|
|
|
|
showSubLevelItems(el);
|
|
|
|
documentMouseMove({
|
|
|
|
clientX: el.getBoundingClientRect().left,
|
|
|
|
clientY: el.getBoundingClientRect().top,
|
|
|
|
});
|
|
|
|
documentMouseMove({
|
|
|
|
clientX: el.getBoundingClientRect().left,
|
|
|
|
clientY: el.getBoundingClientRect().top - subItems.getBoundingClientRect().height + 50,
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(getHideSubItemsInterval()).toBe(0);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('returns 300 when mouse is moved towards sub-items', () => {
|
|
|
|
documentMouseMove({
|
|
|
|
clientX: el.getBoundingClientRect().left,
|
|
|
|
clientY: el.getBoundingClientRect().top,
|
|
|
|
});
|
|
|
|
|
|
|
|
showSubLevelItems(el);
|
|
|
|
documentMouseMove({
|
|
|
|
clientX: el.getBoundingClientRect().left + 20,
|
|
|
|
clientY: el.getBoundingClientRect().top + 10,
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(getHideSubItemsInterval()).toBe(300);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('mouseLeaveTopItem', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
jest.spyOn(el.classList, 'remove');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('removes is-over class if currentOpenMenu is null', () => {
|
|
|
|
setOpenMenu(null);
|
|
|
|
|
|
|
|
mouseLeaveTopItem(el);
|
|
|
|
|
|
|
|
expect(el.classList.remove).toHaveBeenCalledWith('is-over');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('removes is-over class if currentOpenMenu is null & there are sub-items', () => {
|
|
|
|
setOpenMenu(null);
|
|
|
|
el.innerHTML = mockSidebarFragment('position: absolute');
|
|
|
|
|
|
|
|
mouseLeaveTopItem(el);
|
|
|
|
|
|
|
|
expect(el.classList.remove).toHaveBeenCalledWith('is-over');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('does not remove is-over class if currentOpenMenu is the passed in sub-items', () => {
|
|
|
|
setOpenMenu(null);
|
|
|
|
el.innerHTML = mockSidebarFragment('position: absolute');
|
|
|
|
|
|
|
|
setOpenMenu(findSubItems());
|
|
|
|
mouseLeaveTopItem(el);
|
|
|
|
|
|
|
|
expect(el.classList.remove).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('mouseEnterTopItems', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
el.innerHTML = mockSidebarFragment(
|
|
|
|
`position: absolute; top: 0; left: 100px; height: ${OLD_SIDEBAR_WIDTH}px;`,
|
|
|
|
);
|
|
|
|
mockBoundingRects();
|
|
|
|
});
|
|
|
|
|
2022-04-20 14:11:54 -04:00
|
|
|
it('shows sub-items after 0ms if no menu is open', () => {
|
2021-08-31 23:11:03 -04:00
|
|
|
const subItems = findSubItems();
|
|
|
|
mouseEnterTopItems(el);
|
|
|
|
|
|
|
|
expect(getHideSubItemsInterval()).toBe(0);
|
|
|
|
|
2022-04-20 14:11:54 -04:00
|
|
|
return new Promise((resolve) => {
|
|
|
|
setTimeout(() => {
|
|
|
|
expect(subItems.style.display).toBe('block');
|
|
|
|
resolve();
|
|
|
|
});
|
2021-08-31 23:11:03 -04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2022-04-20 14:11:54 -04:00
|
|
|
it('shows sub-items after 300ms if a menu is currently open', () => {
|
2021-08-31 23:11:03 -04:00
|
|
|
const subItems = findSubItems();
|
|
|
|
|
|
|
|
documentMouseMove({
|
|
|
|
clientX: el.getBoundingClientRect().left,
|
|
|
|
clientY: el.getBoundingClientRect().top,
|
|
|
|
});
|
|
|
|
|
|
|
|
setOpenMenu(subItems);
|
|
|
|
|
|
|
|
documentMouseMove({
|
|
|
|
clientX: el.getBoundingClientRect().left + 20,
|
|
|
|
clientY: el.getBoundingClientRect().top + 10,
|
|
|
|
});
|
|
|
|
|
|
|
|
mouseEnterTopItems(el, 0);
|
|
|
|
|
2022-04-20 14:11:54 -04:00
|
|
|
return new Promise((resolve) => {
|
|
|
|
setTimeout(() => {
|
|
|
|
expect(subItems.style.display).toBe('block');
|
|
|
|
resolve();
|
|
|
|
});
|
2021-08-31 23:11:03 -04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('showSubLevelItems', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
el.innerHTML = mockSidebarFragment('position: absolute');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('adds is-over class to el', () => {
|
|
|
|
jest.spyOn(el.classList, 'add');
|
|
|
|
|
|
|
|
showSubLevelItems(el);
|
|
|
|
|
|
|
|
expect(el.classList.add).toHaveBeenCalledWith('is-over');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('does not show sub-items on mobile', () => {
|
|
|
|
breakpointSize = 'xs';
|
|
|
|
|
|
|
|
showSubLevelItems(el);
|
|
|
|
|
|
|
|
expect(findSubItems().style.display).not.toBe('block');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('shows sub-items', () => {
|
|
|
|
showSubLevelItems(el);
|
|
|
|
|
|
|
|
expect(findSubItems().style.display).toBe('block');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('shows collapsed only sub-items if icon only sidebar', () => {
|
|
|
|
const subItems = findSubItems();
|
|
|
|
const sidebar = document.createElement('div');
|
|
|
|
sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS);
|
|
|
|
subItems.classList.add('is-fly-out-only');
|
|
|
|
|
|
|
|
setSidebar(sidebar);
|
|
|
|
|
|
|
|
showSubLevelItems(el);
|
|
|
|
|
|
|
|
expect(findSubItems().style.display).toBe('block');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('does not show collapsed only sub-items if icon only sidebar', () => {
|
|
|
|
const subItems = findSubItems();
|
|
|
|
subItems.classList.add('is-fly-out-only');
|
|
|
|
|
|
|
|
showSubLevelItems(el);
|
|
|
|
|
|
|
|
expect(subItems.style.display).not.toBe('block');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('sets transform of sub-items', () => {
|
|
|
|
const sidebar = document.createElement('div');
|
|
|
|
const subItems = findSubItems();
|
|
|
|
|
|
|
|
sidebar.style.width = `${OLD_SIDEBAR_WIDTH}px`;
|
|
|
|
|
|
|
|
document.body.appendChild(sidebar);
|
|
|
|
|
|
|
|
setSidebar(sidebar);
|
|
|
|
showSubLevelItems(el);
|
|
|
|
|
|
|
|
expect(subItems.style.transform).toBe(
|
|
|
|
`translate3d(${OLD_SIDEBAR_WIDTH}px, ${
|
|
|
|
Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()
|
|
|
|
}px, 0)`,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('sets is-above when element is above', () => {
|
|
|
|
const subItems = findSubItems();
|
|
|
|
mockBoundingRects();
|
|
|
|
|
|
|
|
subItems.style.height = `${window.innerHeight + el.offsetHeight}px`;
|
|
|
|
el.style.top = `${window.innerHeight - el.offsetHeight}px`;
|
|
|
|
|
|
|
|
jest.spyOn(subItems.classList, 'add');
|
|
|
|
|
|
|
|
showSubLevelItems(el);
|
|
|
|
|
|
|
|
expect(subItems.classList.add).toHaveBeenCalledWith('is-above');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('canShowSubItems', () => {
|
|
|
|
it('returns true if on desktop size', () => {
|
|
|
|
expect(canShowSubItems()).toBeTruthy();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('returns false if on mobile size', () => {
|
|
|
|
breakpointSize = 'xs';
|
|
|
|
|
|
|
|
expect(canShowSubItems()).toBeFalsy();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('canShowActiveSubItems', () => {
|
|
|
|
it('returns true by default', () => {
|
|
|
|
expect(canShowActiveSubItems(el)).toBeTruthy();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('returns false when active & expanded sidebar', () => {
|
|
|
|
const sidebar = document.createElement('div');
|
|
|
|
el.classList.add('active');
|
|
|
|
|
|
|
|
setSidebar(sidebar);
|
|
|
|
|
|
|
|
expect(canShowActiveSubItems(el)).toBeFalsy();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('returns true when active & collapsed sidebar', () => {
|
|
|
|
const sidebar = document.createElement('div');
|
|
|
|
sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS);
|
|
|
|
el.classList.add('active');
|
|
|
|
|
|
|
|
setSidebar(sidebar);
|
|
|
|
|
|
|
|
expect(canShowActiveSubItems(el)).toBeTruthy();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('subItemsMouseLeave', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
el.innerHTML = mockSidebarFragment('position: absolute');
|
|
|
|
|
|
|
|
setOpenMenu(findSubItems());
|
|
|
|
});
|
|
|
|
|
|
|
|
it('hides subMenu if element is not hovered', () => {
|
|
|
|
subItemsMouseLeave(el);
|
|
|
|
|
|
|
|
expect(getOpenMenu()).toBeNull();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('does not hide subMenu if element is hovered', () => {
|
|
|
|
el.classList.add('is-over');
|
|
|
|
subItemsMouseLeave(el);
|
|
|
|
|
|
|
|
expect(getOpenMenu()).not.toBeNull();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|