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
fed7c1ed
Commit
fed7c1ed
authored
Sep 05, 2017
by
Clement Ho
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'winh-search-dropdowns' into 'master'
Make search dropdowns consistent See merge request !13615
parents
597b0517
87699616
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
190 additions
and
150 deletions
+190
-150
gl_dropdown.js
app/assets/javascripts/gl_dropdown.js
+9
-5
dropdowns.scss
app/assets/stylesheets/framework/dropdowns.scss
+1
-0
search.scss
app/assets/stylesheets/pages/search.scss
+2
-0
gl_dropdown_spec.js
spec/javascripts/gl_dropdown_spec.js
+178
-145
No files found.
app/assets/javascripts/gl_dropdown.js
View file @
fed7c1ed
...
...
@@ -637,11 +637,15 @@ GitLabDropdown = (function() {
value
=
this
.
options
.
id
?
this
.
options
.
id
(
data
)
:
data
.
id
;
fieldName
=
this
.
options
.
fieldName
;
if
(
value
)
{
value
=
value
.
toString
().
replace
(
/'/g
,
'
\\
\'
'
);
}
field
=
this
.
dropdown
.
parent
().
find
(
"input[name='"
+
fieldName
+
"'][value='"
+
value
+
"']"
);
if
(
field
.
length
)
{
selected
=
true
;
if
(
value
)
{
value
=
value
.
toString
().
replace
(
/'/g
,
'
\\
\'
'
);
field
=
this
.
dropdown
.
parent
().
find
(
`input[name='
${
fieldName
}
'][value='
${
value
}
']`
);
if
(
field
.
length
)
{
selected
=
true
;
}
}
else
{
field
=
this
.
dropdown
.
parent
().
find
(
`input[name='
${
fieldName
}
']`
);
selected
=
!
field
.
length
;
}
}
// Set URL
...
...
app/assets/stylesheets/framework/dropdowns.scss
View file @
fed7c1ed
...
...
@@ -766,6 +766,7 @@
box-shadow
:
none
;
padding
:
8px
16px
;
text-align
:
left
;
white-space
:
normal
;
width
:
100%
;
// make sure the text color is not overriden
...
...
app/assets/stylesheets/pages/search.scss
View file @
fed7c1ed
...
...
@@ -190,6 +190,8 @@ input[type="checkbox"]:hover {
}
.search-holder
{
@include
new-style-dropdown
;
@media
(
min-width
:
$screen-sm-min
)
{
display
:
-
webkit-flex
;
display
:
flex
;
...
...
spec/javascripts/gl_dropdown_spec.js
View file @
fed7c1ed
...
...
@@ -4,7 +4,10 @@ import '~/gl_dropdown';
import
'~/lib/utils/common_utils'
;
import
'~/lib/utils/url_utility'
;
(()
=>
{
describe
(
'glDropdown'
,
function
describeDropdown
()
{
preloadFixtures
(
'static/gl_dropdown.html.raw'
);
loadJSONFixtures
(
'projects.json'
);
const
NON_SELECTABLE_CLASSES
=
'.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'
;
const
SEARCH_INPUT_SELECTOR
=
'.dropdown-input-field'
;
const
ITEM_SELECTOR
=
`.dropdown-content li:not(
${
NON_SELECTABLE_CLASSES
}
)`
;
...
...
@@ -39,187 +42,217 @@ import '~/lib/utils/url_utility';
remoteCallback
=
callback
.
bind
({},
data
);
};
describe
(
'Dropdown'
,
function
describeDropdown
()
{
preloadFixtures
(
'static/gl_dropdown.html.raw'
);
loadJSONFixtures
(
'projects.json'
);
function
initDropDown
(
hasRemote
,
isFilterable
,
extraOpts
=
{})
{
const
options
=
Object
.
assign
({
selectable
:
true
,
filterable
:
isFilterable
,
data
:
hasRemote
?
remoteMock
.
bind
({},
this
.
projectsData
)
:
this
.
projectsData
,
search
:
{
fields
:
[
'name'
]
},
text
:
project
=>
(
project
.
name_with_namespace
||
project
.
name
),
id
:
project
=>
project
.
id
,
},
extraOpts
);
this
.
dropdownButtonElement
=
$
(
'#js-project-dropdown'
,
this
.
dropdownContainerElement
).
glDropdown
(
options
);
}
function
initDropDown
(
hasRemote
,
isFilterable
,
extraOpts
=
{})
{
const
options
=
Object
.
assign
({
selectable
:
true
,
filterable
:
isFilterable
,
data
:
hasRemote
?
remoteMock
.
bind
({},
this
.
projectsData
)
:
this
.
projectsData
,
search
:
{
fields
:
[
'name'
]
},
text
:
project
=>
(
project
.
name_with_namespace
||
project
.
name
),
id
:
project
=>
project
.
id
,
},
extraOpts
);
this
.
dropdownButtonElement
=
$
(
'#js-project-dropdown'
,
this
.
dropdownContainerElement
).
glDropdown
(
options
);
}
beforeEach
(()
=>
{
loadFixtures
(
'static/gl_dropdown.html.raw'
);
this
.
dropdownContainerElement
=
$
(
'.dropdown.inline'
);
this
.
$dropdownMenuElement
=
$
(
'.dropdown-menu'
,
this
.
dropdownContainerElement
);
this
.
projectsData
=
getJSONFixture
(
'projects.json'
);
});
beforeEach
(()
=>
{
loadFixtures
(
'static/gl_dropdown.html.raw'
);
this
.
dropdownContainerElement
=
$
(
'.dropdown.inline'
);
this
.
$dropdownMenuElement
=
$
(
'.dropdown-menu'
,
this
.
dropdownContainerElement
);
this
.
projectsData
=
getJSONFixture
(
'projects.json'
);
});
afterEach
(()
=>
{
$
(
'body'
).
unbind
(
'keydown'
);
this
.
dropdownContainerElement
.
unbind
(
'keyup'
);
});
afterEach
(()
=>
{
$
(
'body'
).
unbind
(
'keydown'
);
this
.
dropdownContainerElement
.
unbind
(
'keyup'
);
});
it
(
'should open on click'
,
()
=>
{
initDropDown
.
call
(
this
,
false
);
expect
(
this
.
dropdownContainerElement
).
not
.
toHaveClass
(
'open'
);
this
.
dropdownButtonElement
.
click
();
expect
(
this
.
dropdownContainerElement
).
toHaveClass
(
'open'
);
});
it
(
'should open on click'
,
()
=>
{
initDropDown
.
call
(
this
,
false
);
expect
(
this
.
dropdownContainerElement
).
not
.
toHaveClass
(
'open'
);
this
.
dropdownButtonElement
.
click
();
expect
(
this
.
dropdownContainerElement
).
toHaveClass
(
'open'
);
});
it
(
'escapes HTML as text'
,
()
=>
{
this
.
projectsData
[
0
].
name_with_namespace
=
'<script>alert("testing");</script>'
;
it
(
'escapes HTML as text'
,
()
=>
{
this
.
projectsData
[
0
].
name_with_namespace
=
'<script>alert("testing");</script>'
;
initDropDown
.
call
(
this
,
false
);
initDropDown
.
call
(
this
,
false
);
this
.
dropdownButtonElement
.
click
(
);
this
.
dropdownButtonElement
.
click
();
expect
(
$
(
'.dropdown-content li:first-child'
).
text
(),
).
toBe
(
'<script>alert("testing");</script>'
);
});
expect
(
$
(
'.dropdown-content li:first-child'
).
text
(),
).
toBe
(
'<script>alert("testing");</script>'
);
});
it
(
'should output HTML when highlighting'
,
()
=>
{
this
.
projectsData
[
0
].
name_with_namespace
=
'testing'
;
$
(
'.dropdown-input .dropdown-input-field'
).
val
(
'test'
);
i
t
(
'should output HTML when highlighting'
,
()
=>
{
this
.
projectsData
[
0
].
name_with_namespace
=
'testing'
;
$
(
'.dropdown-input .dropdown-input-field'
).
val
(
'test'
);
i
nitDropDown
.
call
(
this
,
false
,
true
,
{
highlight
:
true
,
}
);
initDropDown
.
call
(
this
,
false
,
true
,
{
highlight
:
true
,
});
this
.
dropdownButtonElement
.
click
();
this
.
dropdownButtonElement
.
click
();
expect
(
$
(
'.dropdown-content li:first-child'
).
text
(),
).
toBe
(
'testing'
);
expect
(
$
(
'.dropdown-content li:first-child'
).
text
(),
).
toBe
(
'testing'
);
expect
(
$
(
'.dropdown-content li:first-child a'
).
html
(),
).
toBe
(
'<b>t</b><b>e</b><b>s</b><b>t</b>ing'
);
});
expect
(
$
(
'.dropdown-content li:first-child a'
).
html
(),
).
toBe
(
'<b>t</b><b>e</b><b>s</b><b>t</b>ing'
);
describe
(
'that is open'
,
()
=>
{
beforeEach
(()
=>
{
initDropDown
.
call
(
this
,
false
,
false
);
this
.
dropdownButtonElement
.
click
();
});
describe
(
'that is open'
,
()
=>
{
beforeEach
(()
=>
{
initDropDown
.
call
(
this
,
false
,
false
);
this
.
dropdownButtonElement
.
click
();
it
(
'should select a following item on DOWN keypress'
,
()
=>
{
expect
(
$
(
FOCUSED_ITEM_SELECTOR
,
this
.
$dropdownMenuElement
).
length
).
toBe
(
0
);
const
randomIndex
=
(
Math
.
floor
(
Math
.
random
()
*
(
this
.
projectsData
.
length
-
1
))
+
0
);
navigateWithKeys
(
'down'
,
randomIndex
,
()
=>
{
expect
(
$
(
FOCUSED_ITEM_SELECTOR
,
this
.
$dropdownMenuElement
).
length
).
toBe
(
1
);
expect
(
$
(
`
${
ITEM_SELECTOR
}
:eq(
${
randomIndex
}
) a`
,
this
.
$dropdownMenuElement
)).
toHaveClass
(
'is-focused'
);
});
});
it
(
'should select a following item on DOWN keypress'
,
()
=>
{
expect
(
$
(
FOCUSED_ITEM_SELECTOR
,
this
.
$dropdownMenuElement
).
length
).
toBe
(
0
);
const
randomIndex
=
(
Math
.
floor
(
Math
.
random
()
*
(
this
.
projectsData
.
length
-
1
))
+
0
);
navigateWithKeys
(
'down'
,
randomIndex
,
()
=>
{
it
(
'should select a previous item on UP keypress'
,
()
=>
{
expect
(
$
(
FOCUSED_ITEM_SELECTOR
,
this
.
$dropdownMenuElement
).
length
).
toBe
(
0
);
navigateWithKeys
(
'down'
,
(
this
.
projectsData
.
length
-
1
),
()
=>
{
expect
(
$
(
FOCUSED_ITEM_SELECTOR
,
this
.
$dropdownMenuElement
).
length
).
toBe
(
1
);
const
randomIndex
=
(
Math
.
floor
(
Math
.
random
()
*
(
this
.
projectsData
.
length
-
2
))
+
0
);
navigateWithKeys
(
'up'
,
randomIndex
,
()
=>
{
expect
(
$
(
FOCUSED_ITEM_SELECTOR
,
this
.
$dropdownMenuElement
).
length
).
toBe
(
1
);
expect
(
$
(
`
${
ITEM_SELECTOR
}
:eq(
${
randomIndex
}
) a`
,
this
.
$dropdownMenuElement
)).
toHaveClass
(
'is-focused'
);
expect
(
$
(
`
${
ITEM_SELECTOR
}
:eq(
${
((
this
.
projectsData
.
length
-
2
)
-
randomIndex
)
}
) a`
,
this
.
$dropdownMenuElement
)).
toHaveClass
(
'is-focused'
);
});
});
});
it
(
'should select a previous item on UP keypress'
,
()
=>
{
expect
(
$
(
FOCUSED_ITEM_SELECTOR
,
this
.
$dropdownMenuElement
).
length
).
toBe
(
0
);
navigateWithKeys
(
'down'
,
(
this
.
projectsData
.
length
-
1
),
()
=>
{
expect
(
$
(
FOCUSED_ITEM_SELECTOR
,
this
.
$dropdownMenuElement
).
length
).
toBe
(
1
);
const
randomIndex
=
(
Math
.
floor
(
Math
.
random
()
*
(
this
.
projectsData
.
length
-
2
))
+
0
);
navigateWithKeys
(
'up'
,
randomIndex
,
()
=>
{
expect
(
$
(
FOCUSED_ITEM_SELECTOR
,
this
.
$dropdownMenuElement
).
length
).
toBe
(
1
);
expect
(
$
(
`
${
ITEM_SELECTOR
}
:eq(
${((
this
.
projectsData
.
length
-
2
)
-
randomIndex
)}
) a`
,
this
.
$dropdownMenuElement
)).
toHaveClass
(
'is-focused'
);
});
it
(
'should click the selected item on ENTER keypress'
,
()
=>
{
expect
(
this
.
dropdownContainerElement
).
toHaveClass
(
'open'
);
const
randomIndex
=
Math
.
floor
(
Math
.
random
()
*
(
this
.
projectsData
.
length
-
1
))
+
0
;
navigateWithKeys
(
'down'
,
randomIndex
,
()
=>
{
spyOn
(
gl
.
utils
,
'visitUrl'
).
and
.
stub
();
navigateWithKeys
(
'enter'
,
null
,
()
=>
{
expect
(
this
.
dropdownContainerElement
).
not
.
toHaveClass
(
'open'
);
const
link
=
$
(
`
${
ITEM_SELECTOR
}
:eq(
${
randomIndex
}
) a`
,
this
.
$dropdownMenuElement
);
expect
(
link
).
toHaveClass
(
'is-active'
);
const
linkedLocation
=
link
.
attr
(
'href'
);
if
(
linkedLocation
&&
linkedLocation
!==
'#'
)
expect
(
gl
.
utils
.
visitUrl
).
toHaveBeenCalledWith
(
linkedLocation
);
});
});
});
it
(
'should click the selected item on ENTER keypress'
,
()
=>
{
expect
(
this
.
dropdownContainerElement
).
toHaveClass
(
'open'
);
const
randomIndex
=
Math
.
floor
(
Math
.
random
()
*
(
this
.
projectsData
.
length
-
1
))
+
0
;
navigateWithKeys
(
'down'
,
randomIndex
,
()
=>
{
spyOn
(
gl
.
utils
,
'visitUrl'
).
and
.
stub
();
navigateWithKeys
(
'enter'
,
null
,
()
=>
{
expect
(
this
.
dropdownContainerElement
).
not
.
toHaveClass
(
'open'
);
const
link
=
$
(
`
${
ITEM_SELECTOR
}
:eq(
${
randomIndex
}
) a`
,
this
.
$dropdownMenuElement
);
expect
(
link
).
toHaveClass
(
'is-active'
);
const
linkedLocation
=
link
.
attr
(
'href'
);
if
(
linkedLocation
&&
linkedLocation
!==
'#'
)
expect
(
gl
.
utils
.
visitUrl
).
toHaveBeenCalledWith
(
linkedLocation
);
});
});
it
(
'should close on ESC keypress'
,
()
=>
{
expect
(
this
.
dropdownContainerElement
).
toHaveClass
(
'open'
);
this
.
dropdownContainerElement
.
trigger
({
type
:
'keyup'
,
which
:
ARROW_KEYS
.
ESC
,
keyCode
:
ARROW_KEYS
.
ESC
});
expect
(
this
.
dropdownContainerElement
).
not
.
toHaveClass
(
'open'
);
});
});
it
(
'should close on ESC keypress'
,
()
=>
{
expect
(
this
.
dropdownContainerElement
).
toHaveClass
(
'open'
);
this
.
dropdownContainerElement
.
trigger
({
type
:
'keyup'
,
which
:
ARROW_KEYS
.
ESC
,
keyCode
:
ARROW_KEYS
.
ESC
});
expect
(
this
.
dropdownContainerElement
).
not
.
toHaveClass
(
'open'
);
describe
(
'opened and waiting for a remote callback'
,
()
=>
{
beforeEach
(()
=>
{
initDropDown
.
call
(
this
,
true
,
true
);
this
.
dropdownButtonElement
.
click
();
});
it
(
'should show loading indicator while search results are being fetched by backend'
,
()
=>
{
const
dropdownMenu
=
document
.
querySelector
(
'.dropdown-menu'
);
expect
(
dropdownMenu
.
className
.
indexOf
(
'is-loading'
)
!==
-
1
).
toEqual
(
true
);
remoteCallback
();
expect
(
dropdownMenu
.
className
.
indexOf
(
'is-loading'
)
!==
-
1
).
toEqual
(
false
);
});
it
(
'should not focus search input while remote task is not complete'
,
()
=>
{
expect
(
$
(
document
.
activeElement
)).
not
.
toEqual
(
$
(
SEARCH_INPUT_SELECTOR
));
remoteCallback
();
expect
(
$
(
document
.
activeElement
)).
toEqual
(
$
(
SEARCH_INPUT_SELECTOR
));
});
it
(
'should focus search input after remote task is complete'
,
()
=>
{
remoteCallback
();
expect
(
$
(
document
.
activeElement
)).
toEqual
(
$
(
SEARCH_INPUT_SELECTOR
));
});
it
(
'should focus on input when opening for the second time after transition'
,
()
=>
{
remoteCallback
();
this
.
dropdownContainerElement
.
trigger
({
type
:
'keyup'
,
which
:
ARROW_KEYS
.
ESC
,
keyCode
:
ARROW_KEYS
.
ESC
});
this
.
dropdownButtonElement
.
click
();
this
.
dropdownContainerElement
.
trigger
(
'transitionend'
);
expect
(
$
(
document
.
activeElement
)).
toEqual
(
$
(
SEARCH_INPUT_SELECTOR
));
});
});
describe
(
'input focus with array data'
,
()
=>
{
it
(
'should focus input when passing array data to drop down'
,
()
=>
{
initDropDown
.
call
(
this
,
false
,
true
);
this
.
dropdownButtonElement
.
click
();
this
.
dropdownContainerElement
.
trigger
(
'transitionend'
);
expect
(
$
(
document
.
activeElement
)).
toEqual
(
$
(
SEARCH_INPUT_SELECTOR
));
});
});
it
(
'should still have input value on close and restore'
,
()
=>
{
const
$searchInput
=
$
(
SEARCH_INPUT_SELECTOR
);
initDropDown
.
call
(
this
,
false
,
true
);
$searchInput
.
trigger
(
'focus'
)
.
val
(
'g'
)
.
trigger
(
'input'
);
expect
(
$searchInput
.
val
()).
toEqual
(
'g'
);
this
.
dropdownButtonElement
.
trigger
(
'hidden.bs.dropdown'
);
$searchInput
.
trigger
(
'blur'
)
.
trigger
(
'focus'
);
expect
(
$searchInput
.
val
()).
toEqual
(
'g'
);
});
describe
(
'renderItem'
,
()
=>
{
describe
(
'without selected value'
,
()
=>
{
let
dropdown
;
describe
(
'opened and waiting for a remote callback'
,
()
=>
{
beforeEach
(()
=>
{
initDropDown
.
call
(
this
,
true
,
true
);
this
.
dropdownButtonElement
.
click
();
const
dropdownOptions
=
{
};
const
$dropdownDiv
=
$
(
'<div />'
);
$dropdownDiv
.
glDropdown
(
dropdownOptions
);
dropdown
=
$dropdownDiv
.
data
(
'glDropdown'
);
});
it
(
'
should show loading indicator while search results are being fetched by backend
'
,
()
=>
{
const
d
ropdownMenu
=
document
.
querySelector
(
'.dropdown-menu'
)
;
it
(
'
marks items without ID as active
'
,
()
=>
{
const
d
ummyData
=
{
}
;
expect
(
dropdownMenu
.
className
.
indexOf
(
'is-loading'
)
!==
-
1
).
toEqual
(
true
);
remoteCallback
();
expect
(
dropdownMenu
.
className
.
indexOf
(
'is-loading'
)
!==
-
1
).
toEqual
(
false
);
});
const
html
=
dropdown
.
renderItem
(
dummyData
,
null
,
null
);
it
(
'should not focus search input while remote task is not complete'
,
()
=>
{
expect
(
$
(
document
.
activeElement
)).
not
.
toEqual
(
$
(
SEARCH_INPUT_SELECTOR
));
remoteCallback
();
expect
(
$
(
document
.
activeElement
)).
toEqual
(
$
(
SEARCH_INPUT_SELECTOR
));
const
link
=
html
.
querySelector
(
'a'
);
expect
(
link
).
toHaveClass
(
'is-active'
);
});
it
(
'
should focus search input after remote task is complet
e'
,
()
=>
{
remoteCallback
();
expect
(
$
(
document
.
activeElement
)).
toEqual
(
$
(
SEARCH_INPUT_SELECTOR
));
})
;
it
(
'
does not mark items with ID as activ
e'
,
()
=>
{
const
dummyData
=
{
id
:
'ea'
}
;
it
(
'should focus on input when opening for the second time after transition'
,
()
=>
{
remoteCallback
();
this
.
dropdownContainerElement
.
trigger
({
type
:
'keyup'
,
which
:
ARROW_KEYS
.
ESC
,
keyCode
:
ARROW_KEYS
.
ESC
});
this
.
dropdownButtonElement
.
click
();
this
.
dropdownContainerElement
.
trigger
(
'transitionend'
);
expect
(
$
(
document
.
activeElement
)).
toEqual
(
$
(
SEARCH_INPUT_SELECTOR
));
});
});
const
html
=
dropdown
.
renderItem
(
dummyData
,
null
,
null
);
describe
(
'input focus with array data'
,
()
=>
{
it
(
'should focus input when passing array data to drop down'
,
()
=>
{
initDropDown
.
call
(
this
,
false
,
true
);
this
.
dropdownButtonElement
.
click
();
this
.
dropdownContainerElement
.
trigger
(
'transitionend'
);
expect
(
$
(
document
.
activeElement
)).
toEqual
(
$
(
SEARCH_INPUT_SELECTOR
));
const
link
=
html
.
querySelector
(
'a'
);
expect
(
link
).
not
.
toHaveClass
(
'is-active'
);
});
});
it
(
'should still have input value on close and restore'
,
()
=>
{
const
$searchInput
=
$
(
SEARCH_INPUT_SELECTOR
);
initDropDown
.
call
(
this
,
false
,
true
);
$searchInput
.
trigger
(
'focus'
)
.
val
(
'g'
)
.
trigger
(
'input'
);
expect
(
$searchInput
.
val
()).
toEqual
(
'g'
);
this
.
dropdownButtonElement
.
trigger
(
'hidden.bs.dropdown'
);
$searchInput
.
trigger
(
'blur'
)
.
trigger
(
'focus'
);
expect
(
$searchInput
.
val
()).
toEqual
(
'g'
);
});
});
})
()
;
});
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