Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
640cbb00
Commit
640cbb00
authored
Aug 04, 2017
by
Phil Hughes
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add dynamic navigation tunnel to fly-out menus
parent
9419d10e
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
247 additions
and
69 deletions
+247
-69
app/assets/javascripts/fly_out_nav.js
app/assets/javascripts/fly_out_nav.js
+118
-19
app/assets/stylesheets/new_sidebar.scss
app/assets/stylesheets/new_sidebar.scss
+2
-22
spec/javascripts/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 @
640cbb00
import
Cookies
from
'
js-cookie
'
;
import
Cookies
from
'
js-cookie
'
;
import
bp
from
'
./breakpoints
'
;
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
)
=>
{
export
const
canShowActiveSubItems
=
(
el
)
=>
{
const
isHiddenByMedia
=
bp
.
getBreakpointSize
()
===
'
sm
'
||
bp
.
getBreakpointSize
()
===
'
md
'
;
const
isHiddenByMedia
=
bp
.
getBreakpointSize
()
===
'
sm
'
||
bp
.
getBreakpointSize
()
===
'
md
'
;
...
@@ -10,8 +23,28 @@ export const canShowActiveSubItems = (el) => {
...
@@ -10,8 +23,28 @@ export const canShowActiveSubItems = (el) => {
return
true
;
return
true
;
};
};
export
const
canShowSubItems
=
()
=>
bp
.
getBreakpointSize
()
===
'
sm
'
||
bp
.
getBreakpointSize
()
===
'
md
'
||
bp
.
getBreakpointSize
()
===
'
lg
'
;
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
)
=>
{
export
const
calculateTop
=
(
boundingRect
,
outerHeight
)
=>
{
const
windowHeight
=
window
.
innerHeight
;
const
windowHeight
=
window
.
innerHeight
;
const
bottomOverflow
=
windowHeight
-
(
boundingRect
.
top
+
outerHeight
);
const
bottomOverflow
=
windowHeight
-
(
boundingRect
.
top
+
outerHeight
);
...
@@ -20,45 +53,111 @@ export const calculateTop = (boundingRect, outerHeight) => {
...
@@ -20,45 +53,111 @@ export const calculateTop = (boundingRect, outerHeight) => {
boundingRect
.
top
;
boundingRect
.
top
;
};
};
export
const
showSubLevelItems
=
(
el
)
=>
{
export
const
hideMenu
=
(
el
)
=>
{
const
subItems
=
el
.
querySelector
(
'
.sidebar-sub-level-items
'
)
;
if
(
!
el
)
return
;
if
(
!
subItems
||
!
canShowSubItems
()
||
!
canShowActiveSubItems
(
el
))
return
;
const
parentEl
=
el
.
parentNode
;
subItems
.
style
.
display
=
'
block
'
;
el
.
style
.
display
=
''
;
// eslint-disable-line no-param-reassign
el
.
classList
.
add
(
'
is-showing-fly-out
'
);
el
.
style
.
transform
=
''
;
// eslint-disable-line no-param-reassign
el
.
classList
.
add
(
'
is-over
'
);
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
boundingRect
=
el
.
getBoundingClientRect
();
const
top
=
calculateTop
(
boundingRect
,
subItems
.
offsetHeight
);
const
top
=
calculateTop
(
boundingRect
,
subItems
.
offsetHeight
);
const
isAbove
=
top
<
boundingRect
.
top
;
const
isAbove
=
top
<
boundingRect
.
top
;
subItems
.
classList
.
add
(
'
fly-out-list
'
);
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
)
{
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
'
);
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_CLASS
);
el
.
classList
.
remove
(
'
is-over
'
);
};
subItems
.
style
.
display
=
''
;
subItems
.
style
.
transform
=
''
;
export
const
documentMouseMove
=
(
e
)
=>
{
subItems
.
classList
.
remove
(
'
is-above
'
);
mousePos
.
push
({
x
:
e
.
clientX
,
y
:
e
.
clientY
});
if
(
mousePos
.
length
>
6
)
mousePos
.
shift
();
};
};
export
default
()
=>
{
export
default
()
=>
{
const
items
=
[...
document
.
querySelectorAll
(
'
.sidebar-top-level-items > li
'
)]
const
sidebar
=
document
.
querySelector
(
'
.sidebar-top-level-items
'
);
.
filter
(
el
=>
el
.
querySelector
(
'
.sidebar-sub-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
)
=>
{
items
.
forEach
((
el
)
=>
{
el
.
addEventListener
(
'
mouseenter
'
,
e
=>
showSubLevelItems
(
e
.
currentTarget
));
const
subItems
=
el
.
querySelector
(
'
.sidebar-sub-level-items
'
);
el
.
addEventListener
(
'
mouseleave
'
,
e
=>
hideSubLevelItems
(
e
.
currentTarget
));
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 @
640cbb00
...
@@ -250,32 +250,13 @@ $new-sidebar-collapsed-width: 50px;
...
@@ -250,32 +250,13 @@ $new-sidebar-collapsed-width: 50px;
position
:
absolute
;
position
:
absolute
;
top
:
-30px
;
top
:
-30px
;
bottom
:
-30px
;
bottom
:
-30px
;
left
:
0
;
left
:
-10px
;
right
:
-30px
;
right
:
-30px
;
z-index
:
-1
;
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
{
&
.is-above
{
margin-top
:
1px
;
margin-top
:
1px
;
&
:
:
after
{
top
:
auto
;
bottom
:
44px
;
transform
:
skew
(
-30deg
);
}
}
}
>
.active
{
>
.active
{
...
@@ -322,8 +303,7 @@ $new-sidebar-collapsed-width: 50px;
...
@@ -322,8 +303,7 @@ $new-sidebar-collapsed-width: 50px;
}
}
}
}
&
:not
(
.active
)
:hover
>
a
,
&
.active
>
a
:hover
,
>
a
:hover
,
&
.is-over
>
a
{
&
.is-over
>
a
{
background-color
:
$white-light
;
background-color
:
$white-light
;
}
}
...
...
spec/javascripts/fly_out_nav_spec.js
View file @
640cbb00
import
Cookies
from
'
js-cookie
'
;
import
Cookies
from
'
js-cookie
'
;
import
{
import
{
calculateTop
,
calculateTop
,
hideSubLevelItems
,
showSubLevelItems
,
showSubLevelItems
,
canShowSubItems
,
canShowSubItems
,
canShowActiveSubItems
,
canShowActiveSubItems
,
mouseEnterTopItems
,
mouseLeaveTopItem
,
setOpenMenu
,
mousePos
,
getHideSubItemsInterval
,
documentMouseMove
,
}
from
'
~/fly_out_nav
'
;
}
from
'
~/fly_out_nav
'
;
import
bp
from
'
~/breakpoints
'
;
import
bp
from
'
~/breakpoints
'
;
describe
(
'
Fly out sidebar navigation
'
,
()
=>
{
f
describe
(
'
Fly out sidebar navigation
'
,
()
=>
{
let
el
;
let
el
;
let
breakpointSize
=
'
lg
'
;
let
breakpointSize
=
'
lg
'
;
...
@@ -18,11 +23,14 @@ describe('Fly out sidebar navigation', () => {
...
@@ -18,11 +23,14 @@ describe('Fly out sidebar navigation', () => {
document
.
body
.
appendChild
(
el
);
document
.
body
.
appendChild
(
el
);
spyOn
(
bp
,
'
getBreakpointSize
'
).
and
.
callFake
(()
=>
breakpointSize
);
spyOn
(
bp
,
'
getBreakpointSize
'
).
and
.
callFake
(()
=>
breakpointSize
);
setOpenMenu
(
null
);
});
});
afterEach
(()
=>
{
afterEach
(()
=>
{
el
.
remove
();
el
.
remove
();
breakpointSize
=
'
lg
'
;
breakpointSize
=
'
lg
'
;
mousePos
.
length
=
0
;
});
});
describe
(
'
calculateTop
'
,
()
=>
{
describe
(
'
calculateTop
'
,
()
=>
{
...
@@ -49,61 +57,152 @@ describe('Fly out sidebar navigation', () => {
...
@@ -49,61 +57,152 @@ describe('Fly out sidebar navigation', () => {
});
});
});
});
describe
(
'
hideSubLevelItems
'
,
()
=>
{
describe
(
'
getHideSubItemsInterval
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
el
.
innerHTML
=
'
<div class="sidebar-sub-level-items"></div>
'
;
el
.
innerHTML
=
'
<div class="sidebar-sub-level-items"
style="position: absolute; top: 0; left: 100px; height: 200px;"
></div>
'
;
});
});
it
(
'
hides subitems
'
,
()
=>
{
it
(
'
returns 0 if currentOpenMenu is nil
'
,
()
=>
{
hideSubLevelItems
(
el
);
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
(
expect
(
el
.
querySelector
(
'
.sidebar-sub-level-items
'
).
style
.
display
,
getHideSubItemsInterval
()
,
).
toBe
(
''
);
).
toBe
(
0
);
});
});
it
(
'
does not hude subitems on mobile
'
,
()
=>
{
it
(
'
returns 0 when mouse is below sub-items
'
,
()
=>
{
breakpointSize
=
'
xs
'
;
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
(
expect
(
el
.
querySelector
(
'
.sidebar-sub-level-items
'
).
style
.
display
,
getHideSubItemsInterval
()
,
).
not
.
toBe
(
'
none
'
);
).
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
'
);
spyOn
(
el
.
classList
,
'
remove
'
);
});
hideSubLevelItems
(
el
);
it
(
'
removes is-over class if currentOpenMenu is null
'
,
()
=>
{
mouseLeaveTopItem
(
el
);
expect
(
expect
(
el
.
classList
.
remove
,
el
.
classList
.
remove
,
).
toHaveBeenCalledWith
(
'
is-over
'
);
).
toHaveBeenCalledWith
(
'
is-over
'
);
});
});
it
(
'
removes is-above class from sub-items
'
,
()
=>
{
it
(
'
removes is-over class if currentOpenMenu is null & there are sub-items
'
,
()
=>
{
const
subItems
=
el
.
querySelector
(
'
.sidebar-sub-level-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
(
expect
(
subItems
.
classList
.
remove
,
getHideSubItemsInterval
(),
).
toHaveBeenCalledWith
(
'
is-above
'
);
).
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
'
,
()
=>
{
it
(
'
shows sub-items after 300ms if a menu is currently open
'
,
()
=>
{
el
.
innerHTML
=
''
;
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
(
expect
(
el
.
classList
.
remove
,
getHideSubItemsInterval
(),
).
not
.
toHaveBeenCalledWith
();
).
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', () => {
...
@@ -132,7 +231,7 @@ describe('Fly out sidebar navigation', () => {
).
not
.
toBe
(
'
block
'
);
).
not
.
toBe
(
'
block
'
);
});
});
it
(
'
does not
shows sub-items
'
,
()
=>
{
it
(
'
shows sub-items
'
,
()
=>
{
showSubLevelItems
(
el
);
showSubLevelItems
(
el
);
expect
(
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