diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index f2151396d43..93101f123b5 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -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) => { const windowHeight = window.innerHeight; const bottomOverflow = windowHeight - (boundingRect.top + outerHeight); @@ -6,23 +19,64 @@ export const calculateTop = (boundingRect, outerHeight) => { boundingRect.top; }; -export default () => { - $('.sidebar-top-level-items > li:not(.active)').on('mouseover', (e) => { - const $this = e.currentTarget; - const $subitems = $('.sidebar-sub-level-items', $this).show(); +export const showSubLevelItems = (el) => { + const $subitems = el.querySelector('.sidebar-sub-level-items'); - if ($subitems.length) { - const boundingRect = $this.getBoundingClientRect(); - const top = calculateTop(boundingRect, $subitems.outerHeight()); - const isAbove = top < boundingRect.top; + if (!$subitems) return; - $subitems.css({ - transform: `translate3d(0, ${top}px, 0)`, - }); + hideAllSubItems(); - if (isAbove) { - $subitems.addClass('is-above'); - } - } - }).on('mouseout', e => $('.sidebar-sub-level-items', e.currentTarget).hide().removeClass('is-above')); + if (el.classList.contains('is-over')) { + clearTimeout(hideTimeout); + } else { + $subitems.style.display = 'block'; + 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))); }; diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index 05b72e9f425..72c12413aba 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -292,7 +292,8 @@ $new-sidebar-width: 220px; } &:not(.active):hover > a, - > a:hover { + > a:hover, + &.is-over > a { background-color: $white-light; } } diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index d3c6dafe460..0fdaa2d8663 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -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', () => { + let el; + beforeEach(() => { + el = document.createElement('div'); + document.body.appendChild(el); + }); + + afterEach(() => { + el.remove(); + }); + describe('calculateTop', () => { it('returns boundingRect top', () => { const boundingRect = { @@ -24,4 +40,119 @@ describe('Fly out sidebar navigation', () => { ).toBe(window.innerHeight - 50); }); }); + + describe('setMouseOutTimeout', () => { + it('sets hideTimeoutInterval to 150 when inside sub items', () => { + el.innerHTML = ''; + + 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 = ''; + }); + + 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 = ''; + }); + + 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'); + }); + }); });