Skip to content

Commit e1689da

Browse files
authored
Merge pull request #2312 from broadinstitute/jb-search-option-ux
Search options button bugfixes (SCP-6056, SCP-6057)
2 parents db97241 + ac49d23 commit e1689da

File tree

4 files changed

+100
-64
lines changed

4 files changed

+100
-64
lines changed
Lines changed: 66 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,87 @@
11
import React, { useState, useContext } from 'react'
22
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3-
import { faCogs } from '@fortawesome/free-solid-svg-icons'
4-
import { Popover, OverlayTrigger } from 'react-bootstrap'
3+
import { faCogs, faTimesCircle } from '@fortawesome/free-solid-svg-icons'
54
import { StudySearchContext } from '~/providers/StudySearchProvider'
65

76
import OptionsControl from '~/components/search/controls/OptionsControl'
7+
import useCloseableModal from '~/hooks/closeableModal'
88

9+
/** list of configured search option entries */
10+
export const configuredOptions = [
11+
{ searchProp: 'external', value: 'hca', label: 'Include HCA results' },
12+
{ searchProp: 'data_types', value: 'raw_counts', label: 'Has raw counts', multiple: true },
13+
{ searchProp: 'data_types', value: 'diff_exp', label: 'Has differential expression', multiple: true },
14+
{ searchProp: 'data_types', value: 'spatial', label: 'Has spatial data', multiple: true }
15+
]
16+
17+
/** Search options button for filtering results by data types/sources */
918
export default function OptionsButton() {
1019
const searchContext = useContext(StudySearchContext)
1120
const [showOptions, setShowOptions] = useState(false)
12-
const configuredOptions = [
13-
{ searchProp: 'external', value: 'hca', label: 'Include HCA results' },
14-
{ searchProp: 'data_types', value: 'raw_counts', label: 'Has raw counts', multiple: true },
15-
{ searchProp: 'data_types', value: 'diff_exp', label: 'Has differential expression', multiple: true },
16-
{ searchProp: 'data_types', value: 'spatial', label: 'Has spatial data', multiple: true }
17-
]
18-
19-
const optionsPopover = <Popover data-analytics-name='search-options-menu' id='search-options-menu'>
20-
<ul className="facet-filter-list">
21+
22+
/** determine if any options have been selected */
23+
function searchOptionSelected() {
24+
const opts = []
25+
configuredOptions.map(option => {
26+
const opt = option.searchProp
27+
if (searchContext.params[opt] && searchContext.params[opt].length > 0) {
28+
opts.push(opt)
29+
}
30+
})
31+
return opts.length > 0
32+
}
33+
34+
/** clear all selected options */
35+
function clearAllOptions() {
36+
const existingParams = searchContext.params
37+
configuredOptions.map(option => {delete existingParams[option.searchProp]})
38+
searchContext.updateSearch(existingParams)
39+
setShowOptions(false)
40+
}
41+
42+
const { node, clearNode, handleButtonClick } = useCloseableModal(showOptions, setShowOptions)
43+
44+
const optionsMenu = <div data-analytics-name='search-options-menu' id='search-options-menu'>
45+
<ul>
2146
{
2247
configuredOptions.map((option, index) => {
23-
return <OptionsControl
24-
key={`${option.searchProp}-${index}`}
25-
searchContext={searchContext}
26-
searchProp={option.searchProp}
27-
value={option.value}
28-
label={option.label}
29-
multiple={option.multiple}
30-
/>
48+
return <OptionsControl
49+
key={`${option.searchProp}-${index}`}
50+
searchContext={searchContext}
51+
searchProp={option.searchProp}
52+
value={option.value}
53+
label={option.label}
54+
multiple={option.multiple}
55+
/>
3156
})
3257
}
3358
</ul>
34-
</Popover>
59+
{ showOptions && searchOptionSelected() &&
60+
<a className='pull-right' onClick={clearAllOptions}>
61+
Clear&nbsp;
62+
<span
63+
ref={clearNode}
64+
data-testid='clear-search-options'
65+
onClick={clearAllOptions}
66+
aria-label='Clear options'
67+
>
68+
<FontAwesomeIcon icon={faTimesCircle}/>
69+
</span>
70+
</a>
71+
}
72+
</div>
3573

3674
return (
37-
<OverlayTrigger trigger={['click']} placement='bottom' animation={false} overlay={optionsPopover}>
38-
<span id="search-options-button" data-testid="search-options-button"
39-
className={`facet ${showOptions ? 'active' : ''}`}>
40-
<a onClick={() => setShowOptions(!showOptions)}>
75+
<span
76+
ref={node}
77+
id="search-options-button"
78+
data-testid="search-options-button"
79+
className={`facet ${showOptions ? 'active' : ''}`}
80+
>
81+
<a onClick={handleButtonClick}>
4182
<FontAwesomeIcon className="icon-left" icon={faCogs}/>Options
4283
</a>
84+
{ showOptions && optionsMenu }
4385
</span>
44-
</OverlayTrigger>
4586
)
4687
}

app/javascript/components/search/controls/OptionsControl.jsx

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React, { useState } from 'react'
44
export default function OptionsControl({ searchContext, searchProp, value, label, multiple = false }) {
55
const defaultChecked = isDefaultChecked()
66
const [isChecked, setIsChecked] = useState(defaultChecked)
7+
const optionId = `options-control-${searchProp}-${value}`
78

89
/** return existing url query params for this option */
910
function getExistingOpts() {
@@ -12,11 +13,7 @@ export default function OptionsControl({ searchContext, searchProp, value, label
1213

1314
/** set the default state for this option checkbox */
1415
function isDefaultChecked() {
15-
if (multiple) {
16-
return getExistingOpts().includes(value)
17-
} else {
18-
return searchContext.params[searchProp] === value
19-
}
16+
return multiple ? getExistingOpts().includes(value) : searchContext.params[searchProp] === value
2017
}
2118

2219
/** toggle state of checkbox */
@@ -32,14 +29,9 @@ export default function OptionsControl({ searchContext, searchProp, value, label
3229
}
3330

3431
return (
35-
<li id={`options-control-${searchProp}`} key={`options-control-${searchProp}`}>
36-
<label>
37-
<input data-testid={`options-checkbox-${searchProp}-${value}`}
38-
type="checkbox"
39-
checked={isChecked}
40-
onChange={() => {toggleCheckbox(!isChecked)}}/>
41-
<span onClick={() => {toggleCheckbox(!isChecked)}} >{ label }</span>
42-
</label>
32+
<li id={optionId} key={optionId} onClick={() => {toggleCheckbox(!isChecked)}}>
33+
<input data-testid={`options-checkbox-${searchProp}-${value}`} type="checkbox" checked={isChecked} readOnly />
34+
{ label }
4335
</li>
4436
)
4537
}

app/javascript/styles/_search.scss

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -396,33 +396,33 @@ span.text-search {
396396
}
397397

398398
#search-options-menu {
399+
border: 1px solid #aaa;
400+
background: #fff;
401+
position: absolute;
402+
z-index: 9000;
403+
border-radius: 5px;
404+
padding-top: 10px;
405+
padding-right: 5px;
406+
margin-top: 2px;
407+
399408
ul {
400409
list-style-type: none;
401-
padding-left: 0px;
402-
}
403-
404-
ul.facet-filter-list {
405-
max-height: 300px;
406-
overflow-y: auto;
407410
margin-bottom: 0;
408-
label {
409-
display: inline-block;
410-
white-space: nowrap;
411-
}
412-
input {
413-
font-size: 2em;
414-
vertical-align: middle;
415-
margin-top: 0px;
416-
margin-right: 12px;
417-
}
418-
label span {
411+
412+
li {
413+
padding: 0 0.8em;
414+
margin-bottom: 0.8em;
415+
cursor: pointer;
419416
vertical-align: middle;
420417
font-weight: normal;
421-
}
422-
}
423418

424-
li {
425-
width: 100%;
426-
margin-bottom: 0.5em;
419+
input {
420+
font-size: 2em;
421+
vertical-align: top;
422+
margin-top: 4px;
423+
margin-right: 12px;
424+
cursor: pointer;
425+
}
426+
}
427427
}
428428
}

test/js/search/options-button.test.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React from 'react'
2-
import { render, fireEvent } from '@testing-library/react'
2+
import { render, fireEvent, screen } from '@testing-library/react'
33
import '@testing-library/jest-dom/extend-expect'
44

5-
import OptionsButton from '~/components/search/controls/OptionsButton'
5+
import OptionsButton, { configuredOptions } from '~/components/search/controls/OptionsButton'
66

77
describe('OptionsButton component', () => {
88
it('renders the options button with correct icon and text', () => {
@@ -17,9 +17,12 @@ describe('OptionsButton component', () => {
1717
// Initially, options should not be visible
1818
expect(queryByText('Include HCA results')).not.toBeInTheDocument()
1919

20-
// Click to show options
20+
// Click to show options and confirm button is active
2121
fireEvent.click(getByText('Options'))
22-
expect(getByText('Include HCA results')).toBeInTheDocument()
22+
configuredOptions.map(option => {
23+
expect(getByText(option.label)).toBeInTheDocument()
24+
})
25+
expect(screen.getByTestId('search-options-button')).toHaveClass('active')
2326

2427
// Click again to hide options
2528
fireEvent.click(getByText('Options'))

0 commit comments

Comments
 (0)