From 5a12fa150833e8f9aabb23bd64aa0244b344d921 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 18 Aug 2017 22:37:26 +0100 Subject: Fixes the stable branches fly out nav JS --- app/assets/javascripts/fly_out_nav.js | 145 +++++-------------------------- spec/javascripts/fly_out_nav_spec.js | 156 ++++++---------------------------- 2 files changed, 47 insertions(+), 254 deletions(-) diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 1b61392aae3..0be44a27215 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -1,20 +1,6 @@ import Cookies from 'js-cookie'; import bp from './breakpoints'; -const HIDE_INTERVAL_TIMEOUT = 300; -const IS_OVER_CLASS = 'is-over'; -const IS_ABOVE_CLASS = 'is-above'; -const IS_SHOWING_FLY_OUT_CLASS = 'is-showing-fly-out'; -let currentOpenMenu = null; -let menuCornerLocs; -let timeoutId; - -export const mousePos = []; - -export const setOpenMenu = (menu = null) => { currentOpenMenu = menu; }; - -export const slope = (a, b) => (b.y - a.y) / (b.x - a.x); - let headerHeight = 50; export const getHeaderHeight = () => headerHeight; @@ -28,28 +14,8 @@ export const canShowActiveSubItems = (el) => { return true; }; - export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg'; -export const getHideSubItemsInterval = () => { - if (!currentOpenMenu) return 0; - - const currentMousePos = mousePos[mousePos.length - 1]; - const prevMousePos = mousePos[0]; - const currentMousePosY = currentMousePos.y; - const [menuTop, menuBottom] = menuCornerLocs; - - if (currentMousePosY < menuTop.y || - currentMousePosY > menuBottom.y) return 0; - - if (slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) && - slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)) { - return HIDE_INTERVAL_TIMEOUT; - } - - return 0; -}; - export const calculateTop = (boundingRect, outerHeight) => { const windowHeight = window.innerHeight; const bottomOverflow = windowHeight - (boundingRect.top + outerHeight); @@ -58,120 +24,47 @@ export const calculateTop = (boundingRect, outerHeight) => { boundingRect.top; }; -export const hideMenu = (el) => { - if (!el) return; - - const parentEl = el.parentNode; +export const showSubLevelItems = (el) => { + const subItems = el.querySelector('.sidebar-sub-level-items'); - el.style.display = ''; // eslint-disable-line no-param-reassign - el.style.transform = ''; // eslint-disable-line no-param-reassign - el.classList.remove(IS_ABOVE_CLASS); - parentEl.classList.remove(IS_OVER_CLASS); - parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS); + if (!subItems || !canShowSubItems() || !canShowActiveSubItems(el)) return; - setOpenMenu(); -}; + subItems.style.display = 'block'; + el.classList.add('is-showing-fly-out'); + el.classList.add('is-over'); -export const moveSubItemsToPosition = (el, subItems) => { const boundingRect = el.getBoundingClientRect(); const top = calculateTop(boundingRect, subItems.offsetHeight); const isAbove = top < boundingRect.top; subItems.classList.add('fly-out-list'); - subItems.style.transform = `translate3d(0, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign - - const subItemsRect = subItems.getBoundingClientRect(); - - menuCornerLocs = [ - { - x: subItemsRect.left, // left position of the sub items - y: subItemsRect.top, // top position of the sub items - }, - { - x: subItemsRect.left, // left position of the sub items - y: subItemsRect.top + subItemsRect.height, // bottom position of the sub items - }, - ]; + subItems.style.transform = `translate3d(0, ${Math.floor(top) - headerHeight}px, 0)`; if (isAbove) { - subItems.classList.add(IS_ABOVE_CLASS); + subItems.classList.add('is-above'); } }; -export const showSubLevelItems = (el) => { +export const hideSubLevelItems = (el) => { const subItems = el.querySelector('.sidebar-sub-level-items'); - if (!canShowSubItems() || !canShowActiveSubItems(el)) return; - - el.classList.add(IS_OVER_CLASS); + if (!subItems || !canShowSubItems() || !canShowActiveSubItems(el)) return; - if (!subItems) return; - - subItems.style.display = 'block'; - el.classList.add(IS_SHOWING_FLY_OUT_CLASS); - - setOpenMenu(subItems); - moveSubItemsToPosition(el, subItems); -}; - -export const mouseEnterTopItems = (el) => { - clearTimeout(timeoutId); - - timeoutId = setTimeout(() => { - if (currentOpenMenu) hideMenu(currentOpenMenu); - - showSubLevelItems(el); - }, getHideSubItemsInterval()); -}; - -export const mouseLeaveTopItem = (el) => { - const subItems = el.querySelector('.sidebar-sub-level-items'); - - if (!canShowSubItems() || !canShowActiveSubItems(el) || - (subItems && subItems === currentOpenMenu)) return; - - el.classList.remove(IS_OVER_CLASS); -}; - -export const documentMouseMove = (e) => { - mousePos.push({ - x: e.clientX, - y: e.clientY, - }); - - if (mousePos.length > 6) mousePos.shift(); + el.classList.remove('is-showing-fly-out'); + el.classList.remove('is-over'); + subItems.style.display = ''; + subItems.style.transform = ''; + subItems.classList.remove('is-above'); }; export default () => { - const sidebar = document.querySelector('.sidebar-top-level-items'); - - if (!sidebar) return; - - const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')]; - - sidebar.addEventListener('mouseleave', () => { - clearTimeout(timeoutId); - - timeoutId = setTimeout(() => { - if (currentOpenMenu) hideMenu(currentOpenMenu); - }, getHideSubItemsInterval()); - }); + const items = [...document.querySelectorAll('.sidebar-top-level-items > li')] + .filter(el => el.querySelector('.sidebar-sub-level-items')); headerHeight = document.querySelector('.nav-sidebar').offsetTop; items.forEach((el) => { - const subItems = el.querySelector('.sidebar-sub-level-items'); - - if (subItems) { - subItems.addEventListener('mouseleave', () => { - clearTimeout(timeoutId); - hideMenu(currentOpenMenu); - }); - } - - el.addEventListener('mouseenter', e => mouseEnterTopItems(e.currentTarget)); - el.addEventListener('mouseleave', e => mouseLeaveTopItem(e.currentTarget)); + el.addEventListener('mouseenter', e => showSubLevelItems(e.currentTarget)); + el.addEventListener('mouseleave', e => hideSubLevelItems(e.currentTarget)); }); - - document.addEventListener('mousemove', documentMouseMove); }; diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index 2e81a1b056b..08803478bef 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -1,15 +1,10 @@ import Cookies from 'js-cookie'; import { calculateTop, + hideSubLevelItems, showSubLevelItems, canShowSubItems, canShowActiveSubItems, - mouseEnterTopItems, - mouseLeaveTopItem, - setOpenMenu, - mousePos, - getHideSubItemsInterval, - documentMouseMove, getHeaderHeight, } from '~/fly_out_nav'; import bp from '~/breakpoints'; @@ -24,14 +19,11 @@ describe('Fly out sidebar navigation', () => { document.body.appendChild(el); spyOn(bp, 'getBreakpointSize').and.callFake(() => breakpointSize); - - setOpenMenu(null); }); afterEach(() => { - document.body.innerHTML = ''; + el.remove(); breakpointSize = 'lg'; - mousePos.length = 0; }); describe('calculateTop', () => { @@ -58,153 +50,61 @@ describe('Fly out sidebar navigation', () => { }); }); - describe('getHideSubItemsInterval', () => { + describe('hideSubLevelItems', () => { beforeEach(() => { - el.innerHTML = ''; + el.innerHTML = ''; }); - it('returns 0 if currentOpenMenu is nil', () => { - 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, - }); + it('hides subitems', () => { + hideSubLevelItems(el); expect( - getHideSubItemsInterval(), - ).toBe(0); + el.querySelector('.sidebar-sub-level-items').style.display, + ).toBe(''); }); - it('returns 0 when mouse is below sub-items', () => { - const subItems = el.querySelector('.sidebar-sub-level-items'); - - 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('does not hude subitems on mobile', () => { + breakpointSize = 'xs'; - 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, - }); - console.log(el); + hideSubLevelItems(el); expect( - getHideSubItemsInterval(), - ).toBe(300); + el.querySelector('.sidebar-sub-level-items').style.display, + ).not.toBe('none'); }); - }); - describe('mouseLeaveTopItem', () => { - beforeEach(() => { + it('removes is-over class', () => { spyOn(el.classList, 'remove'); - }); - it('removes is-over class if currentOpenMenu is null', () => { - mouseLeaveTopItem(el); + hideSubLevelItems(el); expect( el.classList.remove, ).toHaveBeenCalledWith('is-over'); }); - it('removes is-over class if currentOpenMenu is null & there are sub-items', () => { - el.innerHTML = ''; - - mouseLeaveTopItem(el); - - expect( - el.classList.remove, - ).toHaveBeenCalledWith('is-over'); - }); - - it('does not remove is-over class if currentOpenMenu is the passed in sub-items', () => { - el.innerHTML = ''; - - setOpenMenu(el.querySelector('.sidebar-sub-level-items')); - mouseLeaveTopItem(el); - - expect( - el.classList.remove, - ).not.toHaveBeenCalled(); - }); - }); - - describe('mouseEnterTopItems', () => { - beforeEach(() => { - jasmine.clock().install(); - - el.innerHTML = ''; - }); - - afterEach(() => { - jasmine.clock().uninstall(); - }); - - it('shows sub-items after 0ms if no menu is open', () => { - mouseEnterTopItems(el); + it('removes is-above class from sub-items', () => { + const subItems = el.querySelector('.sidebar-sub-level-items'); - expect( - getHideSubItemsInterval(), - ).toBe(0); + spyOn(subItems.classList, 'remove'); - jasmine.clock().tick(0); + hideSubLevelItems(el); expect( - el.querySelector('.sidebar-sub-level-items').style.display, - ).toBe('block'); + subItems.classList.remove, + ).toHaveBeenCalledWith('is-above'); }); - it('shows sub-items after 300ms if a menu is currently open', () => { - documentMouseMove({ - clientX: el.getBoundingClientRect().left, - clientY: el.getBoundingClientRect().top, - }); - - setOpenMenu(el.querySelector('.sidebar-sub-level-items')); - - documentMouseMove({ - clientX: el.getBoundingClientRect().left + 20, - clientY: el.getBoundingClientRect().top + 10, - }); + it('does nothing if el has no sub-items', () => { + el.innerHTML = ''; - mouseEnterTopItems(el); - - expect( - getHideSubItemsInterval(), - ).toBe(300); + spyOn(el.classList, 'remove'); - jasmine.clock().tick(300); + hideSubLevelItems(el); expect( - el.querySelector('.sidebar-sub-level-items').style.display, - ).toBe('block'); + el.classList.remove, + ).not.toHaveBeenCalledWith(); }); }); @@ -233,7 +133,7 @@ describe('Fly out sidebar navigation', () => { ).not.toBe('block'); }); - it('shows sub-items', () => { + it('does not shows sub-items', () => { showSubLevelItems(el); expect( -- cgit v1.2.1 From 150e8b30f9b80c3f207a60d93d875dd6933c5064 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Sat, 19 Aug 2017 22:23:15 +0100 Subject: fixes for sidebar jumping --- app/assets/javascripts/fly_out_nav.js | 24 ++++++++++++++---------- spec/javascripts/fly_out_nav_spec.js | 33 ++++++++++----------------------- 2 files changed, 24 insertions(+), 33 deletions(-) diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 0be44a27215..adf397ca0fe 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -1,15 +1,15 @@ -import Cookies from 'js-cookie'; import bp from './breakpoints'; let headerHeight = 50; +let sidebar; + +export const setSidebar = (el) => { sidebar = el; }; export const getHeaderHeight = () => headerHeight; export const canShowActiveSubItems = (el) => { - const isHiddenByMedia = bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md'; - - if (el.classList.contains('active') && !isHiddenByMedia) { - return Cookies.get('sidebar_collapsed') === 'true'; + if (el.classList.contains('active') && (sidebar && !sidebar.classList.contains('sidebar-icons-only'))) { + return false; } return true; @@ -61,10 +61,14 @@ export default () => { const items = [...document.querySelectorAll('.sidebar-top-level-items > li')] .filter(el => el.querySelector('.sidebar-sub-level-items')); - headerHeight = document.querySelector('.nav-sidebar').offsetTop; + sidebar = document.querySelector('.nav-sidebar'); - items.forEach((el) => { - el.addEventListener('mouseenter', e => showSubLevelItems(e.currentTarget)); - el.addEventListener('mouseleave', e => hideSubLevelItems(e.currentTarget)); - }); + if (sidebar) { + headerHeight = sidebar.offsetTop; + + items.forEach((el) => { + el.addEventListener('mouseenter', e => showSubLevelItems(e.currentTarget)); + el.addEventListener('mouseleave', e => hideSubLevelItems(e.currentTarget)); + }); + } }; diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index 08803478bef..e13c4629214 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -1,4 +1,3 @@ -import Cookies from 'js-cookie'; import { calculateTop, hideSubLevelItems, @@ -6,6 +5,7 @@ import { canShowSubItems, canShowActiveSubItems, getHeaderHeight, + setSidebar, } from '~/fly_out_nav'; import bp from '~/breakpoints'; @@ -183,7 +183,7 @@ describe('Fly out sidebar navigation', () => { describe('canShowActiveSubItems', () => { afterEach(() => { - Cookies.remove('sidebar_collapsed'); + setSidebar(null); }); it('returns true by default', () => { @@ -192,36 +192,23 @@ describe('Fly out sidebar navigation', () => { ).toBeTruthy(); }); - it('returns false when cookie is false & element is active', () => { - Cookies.set('sidebar_collapsed', 'false'); + it('returns false when active & expanded sidebar', () => { + const sidebar = document.createElement('div'); el.classList.add('active'); - expect( - canShowActiveSubItems(el), - ).toBeFalsy(); - }); - - it('returns true when cookie is false & element is active', () => { - Cookies.set('sidebar_collapsed', 'true'); - el.classList.add('active'); + setSidebar(sidebar); expect( canShowActiveSubItems(el), - ).toBeTruthy(); + ).toBeFalsy(); }); - it('returns true when element is active & breakpoint is sm', () => { - breakpointSize = 'sm'; + it('returns true when active & collapsed sidebar', () => { + const sidebar = document.createElement('div'); + sidebar.classList.add('sidebar-icons-only'); el.classList.add('active'); - expect( - canShowActiveSubItems(el), - ).toBeTruthy(); - }); - - it('returns true when element is active & breakpoint is md', () => { - breakpointSize = 'md'; - el.classList.add('active'); + setSidebar(sidebar); expect( canShowActiveSubItems(el), -- cgit v1.2.1