Commit beaa0723 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'sidebar-fly-out-sub-nav' into 'master'

Fly-out dropdown menu in new sidebar

Closes #34026

See merge request !12938
parents 9d905e68 e4c20cd3
/* global bp */
import './breakpoints';
export const canShowSubItems = () => bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg';
export const calculateTop = (boundingRect, outerHeight) => {
const windowHeight = window.innerHeight;
const bottomOverflow = windowHeight - (boundingRect.top + outerHeight);
return bottomOverflow < 0 ? (boundingRect.top - outerHeight) + boundingRect.height :
boundingRect.top;
};
export const showSubLevelItems = (el) => {
const subItems = el.querySelector('.sidebar-sub-level-items');
if (!subItems || !canShowSubItems()) return;
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, ${Math.floor(top)}px, 0)`;
if (isAbove) {
subItems.classList.add('is-above');
}
};
export const hideSubLevelItems = (el) => {
const subItems = el.querySelector('.sidebar-sub-level-items');
if (!subItems || !canShowSubItems()) return;
el.classList.remove('is-over');
subItems.style.display = 'none';
subItems.classList.remove('is-above');
};
export default () => {
const items = [...document.querySelectorAll('.sidebar-top-level-items > li:not(.active)')]
.filter(el => el.querySelector('.sidebar-sub-level-items'));
items.forEach((el) => {
el.addEventListener('mouseenter', e => showSubLevelItems(e.currentTarget));
el.addEventListener('mouseleave', e => hideSubLevelItems(e.currentTarget));
});
};
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import NewNavSidebar from './new_sidebar'; import NewNavSidebar from './new_sidebar';
import initFlyOutNav from './fly_out_nav';
(function() { (function() {
var hideEndFade; var hideEndFade;
...@@ -58,6 +59,8 @@ import NewNavSidebar from './new_sidebar'; ...@@ -58,6 +59,8 @@ import NewNavSidebar from './new_sidebar';
if (Cookies.get('new_nav') === 'true') { if (Cookies.get('new_nav') === 'true') {
const newNavSidebar = new NewNavSidebar(); const newNavSidebar = new NewNavSidebar();
newNavSidebar.bindEvents(); newNavSidebar.bindEvents();
initFlyOutNav();
} }
$(window).on('scroll', _.throttle(applyScrollNavClass, 100)); $(window).on('scroll', _.throttle(applyScrollNavClass, 100));
......
...@@ -220,6 +220,82 @@ $new-sidebar-width: 220px; ...@@ -220,6 +220,82 @@ $new-sidebar-width: 220px;
.sidebar-top-level-items { .sidebar-top-level-items {
> li { > li {
> a {
@media (min-width: $screen-sm-min) {
margin-right: 2px;
}
&:hover {
color: $gl-text-color;
}
}
&:not(.active) {
> a {
margin-left: 1px;
margin-right: 3px;
}
.sidebar-sub-level-items {
@media (min-width: $screen-sm-min) {
position: fixed;
top: 0;
left: 220px;
width: 150px;
margin-top: -1px;
padding: 8px 1px;
background-color: $white-light;
box-shadow: 2px 1px 3px $dropdown-shadow-color;
border: 1px solid $gray-darker;
border-left: 0;
border-radius: 0 3px 3px 0;
&::before {
content: "";
position: absolute;
top: -30px;
bottom: -30px;
left: 0;
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);
}
}
a {
padding: 8px 16px;
color: $gl-text-color;
&:hover,
&:focus {
background-color: $gray-darker;
}
}
}
}
}
.badge { .badge {
background-color: $inactive-badge-background; background-color: $inactive-badge-background;
color: $inactive-color; color: $inactive-color;
...@@ -228,6 +304,10 @@ $new-sidebar-width: 220px; ...@@ -228,6 +304,10 @@ $new-sidebar-width: 220px;
&.active { &.active {
background: $active-background; background: $active-background;
> a {
margin-left: 4px;
}
.badge { .badge {
color: $active-color; color: $active-color;
font-weight: 600; font-weight: 600;
...@@ -238,18 +318,10 @@ $new-sidebar-width: 220px; ...@@ -238,18 +318,10 @@ $new-sidebar-width: 220px;
} }
} }
> a:hover { &:not(.active):hover > a,
background-color: $hover-background; > a:hover,
color: $hover-color; &.is-over > a {
background-color: $white-light;
svg {
fill: $hover-color;
}
.badge {
background-color: $indigo-500;
color: $hover-color;
}
} }
} }
} }
......
...@@ -91,7 +91,6 @@ ...@@ -91,7 +91,6 @@
%span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
%ul.sidebar-sub-level-items %ul.sidebar-sub-level-items
- if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
= nav_link(controller: :issues) do = nav_link(controller: :issues) do
= link_to project_issues_path(@project), title: 'Issues' do = link_to project_issues_path(@project), title: 'Issues' do
%span %span
...@@ -102,19 +101,11 @@ ...@@ -102,19 +101,11 @@
%span %span
Board Board
- if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
= nav_link(controller: :merge_requests) do
= link_to project_merge_requests_path(@project), title: 'Merge Requests' do
%span
Merge Requests
- if project_nav_tab? :labels
= nav_link(controller: :labels) do = nav_link(controller: :labels) do
= link_to project_labels_path(@project), title: 'Labels' do = link_to project_labels_path(@project), title: 'Labels' do
%span %span
Labels Labels
- if project_nav_tab? :milestones
= nav_link(controller: :milestones) do = nav_link(controller: :milestones) do
= link_to project_milestones_path(@project), title: 'Milestones' do = link_to project_milestones_path(@project), title: 'Milestones' do
%span %span
...@@ -195,7 +186,7 @@ ...@@ -195,7 +186,7 @@
%ul.sidebar-sub-level-items %ul.sidebar-sub-level-items
- can_edit = can?(current_user, :admin_project, @project) - can_edit = can?(current_user, :admin_project, @project)
- if can_edit - if can_edit
= nav_link(controller: :projects) do = nav_link(path: %w[projects#edit]) do
= link_to edit_project_path(@project), title: 'General' do = link_to edit_project_path(@project), title: 'General' do
%span %span
General General
......
/* global bp */
import {
calculateTop,
hideSubLevelItems,
showSubLevelItems,
canShowSubItems,
} from '~/fly_out_nav';
describe('Fly out sidebar navigation', () => {
let el;
let breakpointSize = 'lg';
beforeEach(() => {
el = document.createElement('div');
el.style.position = 'relative';
document.body.appendChild(el);
spyOn(bp, 'getBreakpointSize').and.callFake(() => breakpointSize);
});
afterEach(() => {
el.remove();
breakpointSize = 'lg';
});
describe('calculateTop', () => {
it('returns boundingRect top', () => {
const boundingRect = {
top: 100,
height: 100,
};
expect(
calculateTop(boundingRect, 100),
).toBe(100);
});
it('returns boundingRect - bottomOverflow', () => {
const boundingRect = {
top: window.innerHeight - 50,
height: 100,
};
expect(
calculateTop(boundingRect, 100),
).toBe(window.innerHeight - 50);
});
});
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('does not hude subitems on mobile', () => {
breakpointSize = 'sm';
hideSubLevelItems(el);
expect(
el.querySelector('.sidebar-sub-level-items').style.display,
).not.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" style="position: absolute;"></div>';
});
it('adds is-over class to el', () => {
spyOn(el.classList, 'add');
showSubLevelItems(el);
expect(
el.classList.add,
).toHaveBeenCalledWith('is-over');
});
it('does not show sub-items on mobile', () => {
breakpointSize = 'sm';
showSubLevelItems(el);
expect(
el.querySelector('.sidebar-sub-level-items').style.display,
).not.toBe('block');
});
it('does not shows sub-items', () => {
showSubLevelItems(el);
expect(
el.querySelector('.sidebar-sub-level-items').style.display,
).toBe('block');
});
it('sets transform of sub-items', () => {
const subItems = el.querySelector('.sidebar-sub-level-items');
showSubLevelItems(el);
expect(
subItems.style.transform,
).toBe(`translate3d(0px, ${Math.floor(el.getBoundingClientRect().top)}px, 0px)`);
});
it('sets is-above when element is above', () => {
const subItems = el.querySelector('.sidebar-sub-level-items');
subItems.style.height = `${window.innerHeight + el.offsetHeight}px`;
el.style.top = `${window.innerHeight - el.offsetHeight}px`;
spyOn(subItems.classList, 'add');
showSubLevelItems(el);
expect(
subItems.classList.add,
).toHaveBeenCalledWith('is-above');
});
});
describe('canShowSubItems', () => {
it('returns true if on desktop size', () => {
expect(
canShowSubItems(),
).toBeTruthy();
});
it('returns false if on mobile size', () => {
breakpointSize = 'sm';
expect(
canShowSubItems(),
).toBeFalsy();
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment