summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFilipa Lacerda <filipa@gitlab.com>2017-08-03 09:33:00 +0000
committerFilipa Lacerda <filipa@gitlab.com>2017-08-03 09:33:00 +0000
commitbeaa0723b864371421bf4b07dbba09c5a17277e2 (patch)
tree03f60a315c1f65f77e65344eeda52ecc920c5935
parent9d905e684d5ebe86293e40ad9661fbcef3a1b58e (diff)
parente4c20cd3fe330faa415493ee2fe30dc16fbaca80 (diff)
downloadgitlab-ce-beaa0723b864371421bf4b07dbba09c5a17277e2.tar.gz
Merge branch 'sidebar-fly-out-sub-nav' into 'master'
Fly-out dropdown menu in new sidebar Closes #34026 See merge request !12938
-rw-r--r--app/assets/javascripts/fly_out_nav.js51
-rw-r--r--app/assets/javascripts/layout_nav.js3
-rw-r--r--app/assets/stylesheets/new_sidebar.scss96
-rw-r--r--app/views/layouts/nav/_new_project_sidebar.html.haml43
-rw-r--r--spec/javascripts/fly_out_nav_spec.js180
5 files changed, 335 insertions, 38 deletions
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
new file mode 100644
index 00000000000..8e9a97fe207
--- /dev/null
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -0,0 +1,51 @@
+/* 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));
+ });
+};
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 6186ffe20b3..5c1ba416a03 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -2,6 +2,7 @@
import _ from 'underscore';
import Cookies from 'js-cookie';
import NewNavSidebar from './new_sidebar';
+import initFlyOutNav from './fly_out_nav';
(function() {
var hideEndFade;
@@ -58,6 +59,8 @@ import NewNavSidebar from './new_sidebar';
if (Cookies.get('new_nav') === 'true') {
const newNavSidebar = new NewNavSidebar();
newNavSidebar.bindEvents();
+
+ initFlyOutNav();
}
$(window).on('scroll', _.throttle(applyScrollNavClass, 100));
diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss
index 2ed6dfbe7c2..3d202183c82 100644
--- a/app/assets/stylesheets/new_sidebar.scss
+++ b/app/assets/stylesheets/new_sidebar.scss
@@ -220,6 +220,82 @@ $new-sidebar-width: 220px;
.sidebar-top-level-items {
> 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 {
background-color: $inactive-badge-background;
color: $inactive-color;
@@ -228,6 +304,10 @@ $new-sidebar-width: 220px;
&.active {
background: $active-background;
+ > a {
+ margin-left: 4px;
+ }
+
.badge {
color: $active-color;
font-weight: 600;
@@ -238,18 +318,10 @@ $new-sidebar-width: 220px;
}
}
- > a:hover {
- background-color: $hover-background;
- color: $hover-color;
-
- svg {
- fill: $hover-color;
- }
-
- .badge {
- background-color: $indigo-500;
- color: $hover-color;
- }
+ &:not(.active):hover > a,
+ > a:hover,
+ &.is-over > a {
+ background-color: $white-light;
}
}
}
diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml
index 1a2c9325583..9f1cb248c4e 100644
--- a/app/views/layouts/nav/_new_project_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_project_sidebar.html.haml
@@ -91,34 +91,25 @@
%span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
%ul.sidebar-sub-level-items
- - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
- = nav_link(controller: :issues) do
- = link_to project_issues_path(@project), title: 'Issues' do
- %span
- List
-
- = nav_link(controller: :boards) do
- = link_to project_boards_path(@project), title: 'Board' do
- %span
- Board
+ = nav_link(controller: :issues) do
+ = link_to project_issues_path(@project), title: 'Issues' do
+ %span
+ List
- - 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
+ = nav_link(controller: :boards) do
+ = link_to project_boards_path(@project), title: 'Board' do
+ %span
+ Board
- - if project_nav_tab? :labels
- = nav_link(controller: :labels) do
- = link_to project_labels_path(@project), title: 'Labels' do
- %span
- Labels
+ = nav_link(controller: :labels) do
+ = link_to project_labels_path(@project), title: 'Labels' do
+ %span
+ Labels
- - if project_nav_tab? :milestones
- = nav_link(controller: :milestones) do
- = link_to project_milestones_path(@project), title: 'Milestones' do
- %span
- Milestones
+ = nav_link(controller: :milestones) do
+ = link_to project_milestones_path(@project), title: 'Milestones' do
+ %span
+ Milestones
- if project_nav_tab? :merge_requests
= nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
@@ -195,7 +186,7 @@
%ul.sidebar-sub-level-items
- can_edit = can?(current_user, :admin_project, @project)
- if can_edit
- = nav_link(controller: :projects) do
+ = nav_link(path: %w[projects#edit]) do
= link_to edit_project_path(@project), title: 'General' do
%span
General
diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js
new file mode 100644
index 00000000000..ab74f3e00ec
--- /dev/null
+++ b/spec/javascripts/fly_out_nav_spec.js
@@ -0,0 +1,180 @@
+/* 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();
+ });
+ });
+});