summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2017-08-04 15:32:44 +0100
committerPhil Hughes <me@iamphill.com>2017-08-14 08:59:13 +0100
commit640cbb00e30162e11f775d03175e931a4fd1ad70 (patch)
tree2f44326859a4e10f7718e0da92d07df0501a8c9f /app
parent9419d10e86de73885d1df0d9ee182c0ed122f228 (diff)
downloadgitlab-ce-640cbb00e30162e11f775d03175e931a4fd1ad70.tar.gz
Add dynamic navigation tunnel to fly-out menus
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/fly_out_nav.js137
-rw-r--r--app/assets/stylesheets/new_sidebar.scss24
2 files changed, 120 insertions, 41 deletions
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index 56744a440e7..9012f8b1a14 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -1,6 +1,19 @@
import Cookies from 'js-cookie';
import bp from './breakpoints';
+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) => { currentOpenMenu = menu; };
+
+export const slope = (a, b) => (b.y - a.y) / (b.x - a.x);
+
export const canShowActiveSubItems = (el) => {
const isHiddenByMedia = bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md';
@@ -10,8 +23,28 @@ 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 300;
+ }
+
+ return 0;
+};
+
export const calculateTop = (boundingRect, outerHeight) => {
const windowHeight = window.innerHeight;
const bottomOverflow = windowHeight - (boundingRect.top + outerHeight);
@@ -20,45 +53,111 @@ export const calculateTop = (boundingRect, outerHeight) => {
boundingRect.top;
};
-export const showSubLevelItems = (el) => {
- const subItems = el.querySelector('.sidebar-sub-level-items');
+export const hideMenu = (el) => {
+ if (!el) return;
- if (!subItems || !canShowSubItems() || !canShowActiveSubItems(el)) return;
+ const parentEl = el.parentNode;
- subItems.style.display = 'block';
- el.classList.add('is-showing-fly-out');
- el.classList.add('is-over');
+ 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);
+
+ setOpenMenu(null);
+};
+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)}px, 0)`;
+ subItems.style.transform = `translate3d(0, ${Math.floor(top)}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
+ },
+ ];
if (isAbove) {
- subItems.classList.add('is-above');
+ subItems.classList.add(IS_ABOVE_CLASS);
}
};
-export const hideSubLevelItems = (el) => {
+export const showSubLevelItems = (el) => {
+ const subItems = el.querySelector('.sidebar-sub-level-items');
+
+ if (!canShowSubItems() || !canShowActiveSubItems(el)) return;
+
+ el.classList.add(IS_OVER_CLASS);
+
+ 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 (!subItems || !canShowSubItems() || !canShowActiveSubItems(el)) return;
+ if (!canShowSubItems() || !canShowActiveSubItems(el) || (subItems && subItems === currentOpenMenu)) return;
- el.classList.remove('is-showing-fly-out');
- el.classList.remove('is-over');
- subItems.style.display = '';
- subItems.style.transform = '';
- subItems.classList.remove('is-above');
+ el.classList.remove(IS_OVER_CLASS);
+};
+
+export const documentMouseMove = (e) => {
+ mousePos.push({ x: e.clientX, y: e.clientY });
+
+ if (mousePos.length > 6) mousePos.shift();
};
export default () => {
- const items = [...document.querySelectorAll('.sidebar-top-level-items > li')]
- .filter(el => el.querySelector('.sidebar-sub-level-items'));
+ const sidebar = document.querySelector('.sidebar-top-level-items');
+ const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')];
+
+ sidebar.addEventListener('mouseleave', () => {
+ clearTimeout(timeoutId);
+
+ timeoutId = setTimeout(() => {
+ if (currentOpenMenu) hideMenu(currentOpenMenu);
+ }, getHideSubItemsInterval());
+ });
items.forEach((el) => {
- el.addEventListener('mouseenter', e => showSubLevelItems(e.currentTarget));
- el.addEventListener('mouseleave', e => hideSubLevelItems(e.currentTarget));
+ 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));
});
+
+ document.addEventListener('mousemove', documentMouseMove);
};
diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss
index d49f23b4f5a..faedd207e01 100644
--- a/app/assets/stylesheets/new_sidebar.scss
+++ b/app/assets/stylesheets/new_sidebar.scss
@@ -250,32 +250,13 @@ $new-sidebar-collapsed-width: 50px;
position: absolute;
top: -30px;
bottom: -30px;
- left: 0;
+ left: -10px;
right: -30px;
z-index: -1;
}
- &::after {
- content: "";
- position: absolute;
- top: 44px;
- left: -30px;
- right: 35px;
- bottom: 0;
- height: 100%;
- max-height: 150px;
- z-index: -1;
- transform: skew(33deg);
- }
-
&.is-above {
margin-top: 1px;
-
- &::after {
- top: auto;
- bottom: 44px;
- transform: skew(-30deg);
- }
}
> .active {
@@ -322,8 +303,7 @@ $new-sidebar-collapsed-width: 50px;
}
}
- &:not(.active):hover > a,
- > a:hover,
+ &.active > a:hover,
&.is-over > a {
background-color: $white-light;
}