BigW Consortium Gitlab
Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
G
gitlab-ce
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
Forest Godfrey
gitlab-ce
Commits
56054c3f
Commit
56054c3f
authored
Aug 14, 2017
by
Filipa Lacerda
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'fly-out-tunnel' into 'master'
Add dynamic navigation tunnel to fly-out menus Closes #35949 See merge request !13315
parents
dcca25e9
56d11492
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
255 additions
and
69 deletions
+255
-69
fly_out_nav.js
app/assets/javascripts/fly_out_nav.js
+126
-19
new_sidebar.scss
app/assets/stylesheets/new_sidebar.scss
+2
-22
fly_out_nav_spec.js
spec/javascripts/fly_out_nav_spec.js
+127
-28
No files found.
app/assets/javascripts/fly_out_nav.js
View file @
56054c3f
import
Cookies
from
'js-cookie'
;
import
bp
from
'./breakpoints'
;
const
HIDE_INTERVAL_TIMEOUT
=
300
;
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
=
null
)
=>
{
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 +24,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
HIDE_INTERVAL_TIMEOUT
;
}
return
0
;
};
export
const
calculateTop
=
(
boundingRect
,
outerHeight
)
=>
{
const
windowHeight
=
window
.
innerHeight
;
const
bottomOverflow
=
windowHeight
-
(
boundingRect
.
top
+
outerHeight
);
...
...
@@ -20,45 +54,118 @@ 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
();
};
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
hide
SubLevelItems
=
(
el
)
=>
{
export
const
show
SubLevelItems
=
(
el
)
=>
{
const
subItems
=
el
.
querySelector
(
'.sidebar-sub-level-items'
);
if
(
!
subItems
||
!
canShowSubItems
()
||
!
canShowActiveSubItems
(
el
))
return
;
if
(
!
canShowSubItems
()
||
!
canShowActiveSubItems
(
el
))
return
;
el
.
classList
.
add
(
IS_OVER_CLASS
);
el
.
classList
.
remove
(
'is-showing-fly-out'
);
el
.
classList
.
remove
(
'is-over'
);
subItems
.
style
.
display
=
''
;
subItems
.
style
.
transform
=
''
;
subItems
.
classList
.
remove
(
'is-above'
);
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
(
!
canShowSubItems
()
||
!
canShowActiveSubItems
(
el
)
||
(
subItems
&&
subItems
===
currentOpenMenu
))
return
;
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'
);
if
(
!
sidebar
)
return
;
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
);
};
app/assets/stylesheets/new_sidebar.scss
View file @
56054c3f
...
...
@@ -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
;
}
...
...
spec/javascripts/fly_out_nav_spec.js
View file @
56054c3f
import
Cookies
from
'js-cookie'
;
import
{
calculateTop
,
hideSubLevelItems
,
showSubLevelItems
,
canShowSubItems
,
canShowActiveSubItems
,
mouseEnterTopItems
,
mouseLeaveTopItem
,
setOpenMenu
,
mousePos
,
getHideSubItemsInterval
,
documentMouseMove
,
}
from
'~/fly_out_nav'
;
import
bp
from
'~/breakpoints'
;
...
...
@@ -18,11 +23,14 @@ describe('Fly out sidebar navigation', () => {
document
.
body
.
appendChild
(
el
);
spyOn
(
bp
,
'getBreakpointSize'
).
and
.
callFake
(()
=>
breakpointSize
);
setOpenMenu
(
null
);
});
afterEach
(()
=>
{
el
.
remove
()
;
document
.
body
.
innerHTML
=
''
;
breakpointSize
=
'lg'
;
mousePos
.
length
=
0
;
});
describe
(
'calculateTop'
,
()
=>
{
...
...
@@ -49,61 +57,152 @@ describe('Fly out sidebar navigation', () => {
});
});
describe
(
'
hideSubLevelItems
'
,
()
=>
{
describe
(
'
getHideSubItemsInterval
'
,
()
=>
{
beforeEach
(()
=>
{
el
.
innerHTML
=
'<div class="sidebar-sub-level-items"></div>'
;
el
.
innerHTML
=
'<div class="sidebar-sub-level-items"
style="position: fixed; top: 0; left: 100px; height: 50px;"
></div>'
;
});
it
(
'hides subitems'
,
()
=>
{
hideSubLevelItems
(
el
);
it
(
'returns 0 if currentOpenMenu is nil'
,
()
=>
{
expect
(
getHideSubItemsInterval
(),
).
toBe
(
0
);
});
it
(
'returns 0 when mouse above sub-items'
,
()
=>
{
showSubLevelItems
(
el
);
documentMouseMove
({
clientX
:
el
.
getBoundingClientRect
().
left
,
clientY
:
el
.
getBoundingClientRect
().
top
,
});
documentMouseMove
({
clientX
:
el
.
getBoundingClientRect
().
left
,
clientY
:
el
.
getBoundingClientRect
().
top
-
50
,
});
expect
(
el
.
querySelector
(
'.sidebar-sub-level-items'
).
style
.
display
,
).
toBe
(
''
);
getHideSubItemsInterval
()
,
).
toBe
(
0
);
});
it
(
'
does not hude subitems on mobile
'
,
()
=>
{
breakpointSize
=
'xs'
;
it
(
'
returns 0 when mouse is below sub-items
'
,
()
=>
{
const
subItems
=
el
.
querySelector
(
'.sidebar-sub-level-items'
)
;
hideSubLevelItems
(
el
);
showSubLevelItems
(
el
);
documentMouseMove
({
clientX
:
el
.
getBoundingClientRect
().
left
,
clientY
:
el
.
getBoundingClientRect
().
top
,
});
documentMouseMove
({
clientX
:
el
.
getBoundingClientRect
().
left
,
clientY
:
(
el
.
getBoundingClientRect
().
top
-
subItems
.
getBoundingClientRect
().
height
)
+
50
,
});
expect
(
el
.
querySelector
(
'.sidebar-sub-level-items'
).
style
.
display
,
).
not
.
toBe
(
'none'
);
getHideSubItemsInterval
()
,
).
toBe
(
0
);
});
it
(
'removes is-over class'
,
()
=>
{
it
(
'returns 300 when mouse is moved towards sub-items'
,
()
=>
{
documentMouseMove
({
clientX
:
el
.
getBoundingClientRect
().
left
,
clientY
:
el
.
getBoundingClientRect
().
top
,
});
showSubLevelItems
(
el
);
documentMouseMove
({
clientX
:
el
.
getBoundingClientRect
().
left
+
20
,
clientY
:
el
.
getBoundingClientRect
().
top
+
10
,
});
expect
(
getHideSubItemsInterval
(),
).
toBe
(
300
);
});
});
describe
(
'mouseLeaveTopItem'
,
()
=>
{
beforeEach
(()
=>
{
spyOn
(
el
.
classList
,
'remove'
);
});
hideSubLevelItems
(
el
);
it
(
'removes is-over class if currentOpenMenu is null'
,
()
=>
{
mouseLeaveTopItem
(
el
);
expect
(
el
.
classList
.
remove
,
).
toHaveBeenCalledWith
(
'is-over'
);
});
it
(
'removes is-above class from sub-items'
,
()
=>
{
const
subItems
=
el
.
querySelector
(
'.sidebar-sub-level-items'
);
it
(
'removes is-over class if currentOpenMenu is null & there are sub-items'
,
()
=>
{
el
.
innerHTML
=
'<div class="sidebar-sub-level-items" style="position: absolute;"></div>'
;
mouseLeaveTopItem
(
el
);
expect
(
el
.
classList
.
remove
,
).
toHaveBeenCalledWith
(
'is-over'
);
});
it
(
'does not remove is-over class if currentOpenMenu is the passed in sub-items'
,
()
=>
{
el
.
innerHTML
=
'<div class="sidebar-sub-level-items" style="position: absolute;"></div>'
;
setOpenMenu
(
el
.
querySelector
(
'.sidebar-sub-level-items'
));
mouseLeaveTopItem
(
el
);
expect
(
el
.
classList
.
remove
,
).
not
.
toHaveBeenCalled
();
});
});
spyOn
(
subItems
.
classList
,
'remove'
);
describe
(
'mouseEnterTopItems'
,
()
=>
{
beforeEach
(()
=>
{
jasmine
.
clock
().
install
();
hideSubLevelItems
(
el
);
el
.
innerHTML
=
'<div class="sidebar-sub-level-items" style="position: absolute; top: 0; left: 100px; height: 200px;"></div>'
;
});
afterEach
(()
=>
{
jasmine
.
clock
().
uninstall
();
});
it
(
'shows sub-items after 0ms if no menu is open'
,
()
=>
{
mouseEnterTopItems
(
el
);
expect
(
subItems
.
classList
.
remove
,
).
toHaveBeenCalledWith
(
'is-above'
);
getHideSubItemsInterval
(),
).
toBe
(
0
);
jasmine
.
clock
().
tick
(
0
);
expect
(
el
.
querySelector
(
'.sidebar-sub-level-items'
).
style
.
display
,
).
toBe
(
'block'
);
});
it
(
'does nothing if el has no sub-items'
,
()
=>
{
el
.
innerHTML
=
''
;
it
(
'shows sub-items after 300ms if a menu is currently open'
,
()
=>
{
documentMouseMove
({
clientX
:
el
.
getBoundingClientRect
().
left
,
clientY
:
el
.
getBoundingClientRect
().
top
,
});
spyOn
(
el
.
classList
,
'remove'
);
setOpenMenu
(
el
.
querySelector
(
'.sidebar-sub-level-items'
));
documentMouseMove
({
clientX
:
el
.
getBoundingClientRect
().
left
+
20
,
clientY
:
el
.
getBoundingClientRect
().
top
+
10
,
});
hideSubLevel
Items
(
el
);
mouseEnterTop
Items
(
el
);
expect
(
el
.
classList
.
remove
,
).
not
.
toHaveBeenCalledWith
();
getHideSubItemsInterval
(),
).
toBe
(
300
);
jasmine
.
clock
().
tick
(
300
);
expect
(
el
.
querySelector
(
'.sidebar-sub-level-items'
).
style
.
display
,
).
toBe
(
'block'
);
});
});
...
...
@@ -132,7 +231,7 @@ describe('Fly out sidebar navigation', () => {
).
not
.
toBe
(
'block'
);
});
it
(
'
does not
shows sub-items'
,
()
=>
{
it
(
'shows sub-items'
,
()
=>
{
showSubLevelItems
(
el
);
expect
(
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment