added mouseleave timeout with JS
This commit is contained in:
parent
f20a48494a
commit
20bfc4f679
3 changed files with 204 additions and 18 deletions
|
@ -1,3 +1,16 @@
|
||||||
|
let hideTimeoutInterval = 0;
|
||||||
|
let hideTimeout;
|
||||||
|
let subitems;
|
||||||
|
|
||||||
|
export const getHideTimeoutInterval = () => hideTimeoutInterval;
|
||||||
|
|
||||||
|
export const hideAllSubItems = () => {
|
||||||
|
subitems.forEach((el) => {
|
||||||
|
el.parentNode.classList.remove('is-over');
|
||||||
|
el.style.display = 'none'; // eslint-disable-line no-param-reassign
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const calculateTop = (boundingRect, outerHeight) => {
|
export const calculateTop = (boundingRect, outerHeight) => {
|
||||||
const windowHeight = window.innerHeight;
|
const windowHeight = window.innerHeight;
|
||||||
const bottomOverflow = windowHeight - (boundingRect.top + outerHeight);
|
const bottomOverflow = windowHeight - (boundingRect.top + outerHeight);
|
||||||
|
@ -6,23 +19,64 @@ export const calculateTop = (boundingRect, outerHeight) => {
|
||||||
boundingRect.top;
|
boundingRect.top;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default () => {
|
export const showSubLevelItems = (el) => {
|
||||||
$('.sidebar-top-level-items > li:not(.active)').on('mouseover', (e) => {
|
const $subitems = el.querySelector('.sidebar-sub-level-items');
|
||||||
const $this = e.currentTarget;
|
|
||||||
const $subitems = $('.sidebar-sub-level-items', $this).show();
|
|
||||||
|
|
||||||
if ($subitems.length) {
|
if (!$subitems) return;
|
||||||
const boundingRect = $this.getBoundingClientRect();
|
|
||||||
const top = calculateTop(boundingRect, $subitems.outerHeight());
|
|
||||||
const isAbove = top < boundingRect.top;
|
|
||||||
|
|
||||||
$subitems.css({
|
hideAllSubItems();
|
||||||
transform: `translate3d(0, ${top}px, 0)`,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isAbove) {
|
if (el.classList.contains('is-over')) {
|
||||||
$subitems.addClass('is-above');
|
clearTimeout(hideTimeout);
|
||||||
}
|
} else {
|
||||||
}
|
$subitems.style.display = 'block';
|
||||||
}).on('mouseout', e => $('.sidebar-sub-level-items', e.currentTarget).hide().removeClass('is-above'));
|
el.classList.add('is-over');
|
||||||
|
}
|
||||||
|
|
||||||
|
const boundingRect = el.getBoundingClientRect();
|
||||||
|
const top = calculateTop(boundingRect, $subitems.offsetHeight);
|
||||||
|
const isAbove = top < boundingRect.top;
|
||||||
|
|
||||||
|
$subitems.style.transform = `translate3d(0, ${top}px, 0)`;
|
||||||
|
|
||||||
|
if (isAbove) {
|
||||||
|
$subitems.classList.add('is-above');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hideSubLevelItems = (el) => {
|
||||||
|
const $subitems = el.querySelector('.sidebar-sub-level-items');
|
||||||
|
const hideFn = () => {
|
||||||
|
el.classList.remove('is-over');
|
||||||
|
$subitems.style.display = 'none';
|
||||||
|
$subitems.classList.remove('is-above');
|
||||||
|
|
||||||
|
hideTimeoutInterval = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($subitems && hideTimeoutInterval) {
|
||||||
|
hideTimeout = setTimeout(hideFn, hideTimeoutInterval);
|
||||||
|
} else if ($subitems) {
|
||||||
|
hideFn();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setMouseOutTimeout = (el) => {
|
||||||
|
if (el.closest('.sidebar-sub-level-items')) {
|
||||||
|
hideTimeoutInterval = 250;
|
||||||
|
} else {
|
||||||
|
hideTimeoutInterval = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const items = [...document.querySelectorAll('.sidebar-top-level-items > li:not(.active)')];
|
||||||
|
subitems = [...document.querySelectorAll('.sidebar-top-level-items > li:not(.active) .sidebar-sub-level-items')];
|
||||||
|
|
||||||
|
items.forEach((el) => {
|
||||||
|
el.addEventListener('mouseenter', e => showSubLevelItems(e.currentTarget));
|
||||||
|
el.addEventListener('mouseleave', e => hideSubLevelItems(e.currentTarget));
|
||||||
|
});
|
||||||
|
|
||||||
|
subitems.forEach(el => el.addEventListener('mouseleave', e => setMouseOutTimeout(e.target)));
|
||||||
};
|
};
|
||||||
|
|
|
@ -292,7 +292,8 @@ $new-sidebar-width: 220px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.active):hover > a,
|
&:not(.active):hover > a,
|
||||||
> a:hover {
|
> a:hover,
|
||||||
|
&.is-over > a {
|
||||||
background-color: $white-light;
|
background-color: $white-light;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,22 @@
|
||||||
import { calculateTop } from '~/fly_out_nav';
|
import {
|
||||||
|
calculateTop,
|
||||||
|
setMouseOutTimeout,
|
||||||
|
getHideTimeoutInterval,
|
||||||
|
hideSubLevelItems,
|
||||||
|
showSubLevelItems,
|
||||||
|
} from '~/fly_out_nav';
|
||||||
|
|
||||||
describe('Fly out sidebar navigation', () => {
|
describe('Fly out sidebar navigation', () => {
|
||||||
|
let el;
|
||||||
|
beforeEach(() => {
|
||||||
|
el = document.createElement('div');
|
||||||
|
document.body.appendChild(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
el.remove();
|
||||||
|
});
|
||||||
|
|
||||||
describe('calculateTop', () => {
|
describe('calculateTop', () => {
|
||||||
it('returns boundingRect top', () => {
|
it('returns boundingRect top', () => {
|
||||||
const boundingRect = {
|
const boundingRect = {
|
||||||
|
@ -24,4 +40,119 @@ describe('Fly out sidebar navigation', () => {
|
||||||
).toBe(window.innerHeight - 50);
|
).toBe(window.innerHeight - 50);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('setMouseOutTimeout', () => {
|
||||||
|
it('sets hideTimeoutInterval to 150 when inside sub items', () => {
|
||||||
|
el.innerHTML = '<div class="sidebar-sub-level-items"><div class="js-test"></div></div>';
|
||||||
|
|
||||||
|
setMouseOutTimeout(el.querySelector('.js-test'));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getHideTimeoutInterval(),
|
||||||
|
).toBe(150);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets hideTimeoutInterval when not inside sub items', () => {
|
||||||
|
setMouseOutTimeout(el);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getHideTimeoutInterval(),
|
||||||
|
).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hideSubLevelItems', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
el.innerHTML = '<div class="sidebar-sub-level-items"></div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides subitems', () => {
|
||||||
|
hideSubLevelItems(el);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
el.querySelector('.sidebar-sub-level-items').style.display,
|
||||||
|
).toBe('none');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes is-over class', () => {
|
||||||
|
spyOn(el.classList, 'remove');
|
||||||
|
|
||||||
|
hideSubLevelItems(el);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
el.classList.remove,
|
||||||
|
).toHaveBeenCalledWith('is-over');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes is-above class from sub-items', () => {
|
||||||
|
const subItems = el.querySelector('.sidebar-sub-level-items');
|
||||||
|
|
||||||
|
spyOn(subItems.classList, 'remove');
|
||||||
|
|
||||||
|
hideSubLevelItems(el);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
subItems.classList.remove,
|
||||||
|
).toHaveBeenCalledWith('is-above');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if el has no sub-items', () => {
|
||||||
|
el.innerHTML = '';
|
||||||
|
|
||||||
|
spyOn(el.classList, 'remove');
|
||||||
|
|
||||||
|
hideSubLevelItems(el);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
el.classList.remove,
|
||||||
|
).not.toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('showSubLevelItems', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
el.innerHTML = '<div class="sidebar-sub-level-items"></div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds is-over class to el', () => {
|
||||||
|
spyOn(el.classList, 'add');
|
||||||
|
|
||||||
|
showSubLevelItems(el);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
el.classList.add,
|
||||||
|
).toHaveBeenCalledWith('is-over');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows sub-items', () => {
|
||||||
|
showSubLevelItems(el);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
el.querySelector('.sidebar-sub-level-items').style.display,
|
||||||
|
).toBe('block');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets transform of sub-items', () => {
|
||||||
|
showSubLevelItems(el);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
el.querySelector('.sidebar-sub-level-items').style.transform,
|
||||||
|
).toBe(`translate3d(0px, ${el.offsetTop}px, 0px)`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets is-above when element is above', () => {
|
||||||
|
const subItems = el.querySelector('.sidebar-sub-level-items');
|
||||||
|
subItems.style.height = '5000px';
|
||||||
|
el.style.position = 'relative';
|
||||||
|
el.style.top = '1000px';
|
||||||
|
|
||||||
|
spyOn(el.classList, 'add');
|
||||||
|
|
||||||
|
showSubLevelItems(el);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
el.classList.add,
|
||||||
|
).toHaveBeenCalledWith('is-above');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue