summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2017-08-01 08:49:03 +0100
committerPhil Hughes <me@iamphill.com>2017-08-01 08:49:03 +0100
commit20bfc4f679bd63f71af716d4910c5c22e33180c0 (patch)
tree77875dc518481f280051ecf9a2703006d4059ab8
parentf20a48494a4d60ddf311b85ce51ba0cb788390be (diff)
downloadgitlab-ce-20bfc4f679bd63f71af716d4910c5c22e33180c0.tar.gz
added mouseleave timeout with JS
-rw-r--r--app/assets/javascripts/fly_out_nav.js90
-rw-r--r--app/assets/stylesheets/new_sidebar.scss3
-rw-r--r--spec/javascripts/fly_out_nav_spec.js133
3 files changed, 206 insertions, 20 deletions
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 const showSubLevelItems = (el) => {
+ const $subitems = el.querySelector('.sidebar-sub-level-items');
+
+ if (!$subitems) return;
+
+ hideAllSubItems();
+
+ 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 () => {
- $('.sidebar-top-level-items > li:not(.active)').on('mouseover', (e) => {
- const $this = e.currentTarget;
- const $subitems = $('.sidebar-sub-level-items', $this).show();
-
- if ($subitems.length) {
- const boundingRect = $this.getBoundingClientRect();
- const top = calculateTop(boundingRect, $subitems.outerHeight());
- const isAbove = top < boundingRect.top;
-
- $subitems.css({
- transform: `translate3d(0, ${top}px, 0)`,
- });
-
- if (isAbove) {
- $subitems.addClass('is-above');
- }
- }
- }).on('mouseout', e => $('.sidebar-sub-level-items', e.currentTarget).hide().removeClass('is-above'));
+ 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 = '<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');
+ });
+ });
});