diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2017-08-03 09:33:00 +0000 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2017-08-03 09:33:00 +0000 |
commit | beaa0723b864371421bf4b07dbba09c5a17277e2 (patch) | |
tree | 03f60a315c1f65f77e65344eeda52ecc920c5935 | |
parent | 9d905e684d5ebe86293e40ad9661fbcef3a1b58e (diff) | |
parent | e4c20cd3fe330faa415493ee2fe30dc16fbaca80 (diff) | |
download | gitlab-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.js | 51 | ||||
-rw-r--r-- | app/assets/javascripts/layout_nav.js | 3 | ||||
-rw-r--r-- | app/assets/stylesheets/new_sidebar.scss | 96 | ||||
-rw-r--r-- | app/views/layouts/nav/_new_project_sidebar.html.haml | 43 | ||||
-rw-r--r-- | spec/javascripts/fly_out_nav_spec.js | 180 |
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(); + }); + }); +}); |