Created
April 25, 2020 16:52
-
-
Save sirdarthvader/b021e9b987882ad670cb76591a3a5640 to your computer and use it in GitHub Desktop.
filter-bar-index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useMemo, useState, useEffect, useCallback } from 'react' | |
import { useDispatch, useSelector } from 'react-redux' | |
import PropTypes from 'prop-types' | |
import uuid from 'uuidv4' | |
import FocusTrap from 'react-focus-trap' | |
import DownArrow from 'components/svg/down-arrow' | |
import Checkbox from 'components/checkbox' | |
import { setFilter, clearFilter } from 'modules/filters/actions' | |
import filterSelector from 'modules/filters/selector' | |
import { getTreeData } from 'lib/utils/category' | |
import { | |
ALGOLIA_LABEL_MAP, | |
ALGOLIA_FIELDS_MAP, | |
SYSTEM_FILTERS | |
} from 'modules/search-results/algolia-search' | |
import { StyledFilter, StyledArrow, StyledNodeParent } from './styles' | |
export default function SearchFilter({ | |
// All possible values for this particular Filter | |
facetValues, | |
// Filtered values for this Filter, based on current search | |
filteredFacetValues, | |
attribute, | |
searchable, | |
toggled, | |
onToggle, | |
treeData | |
}) { | |
const [id, setId] = useState(null) | |
const queryParams = useSelector(filterSelector.getFilters) | |
const key = ALGOLIA_LABEL_MAP[attribute] | |
const filterValues = filteredFacetValues[key] || facetValues[key] | |
const filters = filterValues ? Object.keys(filterValues).map(key => key) : [] | |
const defaultRefinement = queryParams[ALGOLIA_FIELDS_MAP[key]] || '' | |
const [selectedFilters, setSelectedFilters] = useState( | |
defaultRefinement || [] | |
) | |
useEffect(() => setId(uuid()), []) | |
/** Listen for all filter changes and de-select filters on Clear */ | |
useEffect(() => { | |
const emptyFilters = | |
Object.keys(queryParams).filter(key => SYSTEM_FILTERS.indexOf(key) === -1) | |
.length === 0 | |
if (emptyFilters) { | |
setSelectedFilters([]) | |
} | |
}, [queryParams]) | |
/** Listen for window clicks and close the Filter */ | |
const onWindowClickListener = useCallback(() => { | |
if (toggled) { | |
onToggle() | |
} | |
}, [toggled]) | |
/** | |
* Listen for clicks inside the current Filter elem | |
* If the click happened inside the current Filter stop the propagation so the event doesn't | |
* reach the onWindowClickListener method | |
* @param e {Object} event | |
*/ | |
const elemClickListener = e => { | |
let insideCrtElem = false | |
let elem = e.target | |
while (elem.parentElement) { | |
if (elem.id === id) { | |
insideCrtElem = true | |
} | |
elem = elem.parentElement | |
} | |
if (insideCrtElem) { | |
e.stopPropagation() | |
} | |
} | |
/** Listen for click events to detect if the user clicked outside or not */ | |
useEffect(() => { | |
window.addEventListener('click', onWindowClickListener) | |
// on unmount, remove the listener | |
return () => { | |
window.removeEventListener('click', onWindowClickListener) | |
} | |
}, [toggled]) | |
return ( | |
<StyledFilter | |
up={toggled} | |
disabled={filters.length === 0} | |
onClick={elemClickListener} | |
id={id} | |
> | |
<div | |
className="box" | |
onClick={onToggle} | |
tabIndex="0" | |
onKeyDown={e => { | |
if (e.keyCode === 32 || e.keyCode === 40) { | |
e.preventDefault() | |
onToggle() | |
} | |
}} | |
aria-label={`Filter by ${attribute} - filter ${ | |
toggled ? 'opened' : 'not opened' | |
}`} | |
> | |
<span> | |
{attribute}{' '} | |
{selectedFilters.length > 0 ? ( | |
<span className="box__refinement">({selectedFilters.length})</span> | |
) : null} | |
</span> | |
<StyledArrow up={toggled} className="tablet-hide"> | |
<DownArrow /> | |
</StyledArrow> | |
<div className="box__handle tablet-show">+</div> | |
</div> | |
<SearchDropdownValues | |
filters={filters} | |
attribute={ALGOLIA_FIELDS_MAP[key]} | |
searchable={searchable} | |
toggled={toggled} | |
selectedFilters={selectedFilters} | |
setSelectedFilters={setSelectedFilters} | |
treeData={treeData} | |
/> | |
</StyledFilter> | |
) | |
} | |
SearchFilter.propTypes = { | |
facetValues: PropTypes.object.isRequired, | |
filteredFacetValues: PropTypes.object.isRequired, | |
attribute: PropTypes.string.isRequired, | |
searchable: PropTypes.bool, | |
toggled: PropTypes.bool, | |
onToggle: PropTypes.func.isRequired, | |
treeData: PropTypes.bool | |
} | |
SearchFilter.defaultProps = { | |
searchable: true, | |
toggled: false, | |
treeData: false | |
} | |
const SearchDropdownValues = ({ | |
filters, | |
attribute, | |
toggled, | |
selectedFilters, | |
setSelectedFilters, | |
treeData | |
}) => { | |
const dispatch = useDispatch() | |
const uid = useMemo(() => uuid(), []) | |
const [filtersToShow, setFiltersToShow] = useState([]) | |
const [searchValue, setSearchValue] = useState('') | |
useEffect(() => { | |
searchForItems(searchValue) | |
}, [filters]) | |
/* On Filter toggled */ | |
const onToggle = filter => { | |
const arr = [...selectedFilters] | |
const indexOfItem = arr.indexOf(filter) | |
if (indexOfItem === -1) { | |
arr.push(filter) | |
} else { | |
arr.splice(indexOfItem, 1) | |
} | |
setSelectedFilters(arr) | |
dispatch(setFilter(attribute, arr)) | |
} | |
/* Search for Filters */ | |
const searchForItems = (value = '') => { | |
setFiltersToShow( | |
[...filters].filter(item => | |
item.toLowerCase().includes(value.toLowerCase()) | |
) | |
) | |
} | |
const onSearchKey = event => { | |
const value = event.currentTarget.value | |
setSearchValue(value) | |
searchForItems(value) | |
} | |
return ( | |
<> | |
{toggled && filters.length > 0 && ( | |
<div className="items"> | |
<FocusTrap active={toggled && filters.length > 0}> | |
<label htmlFor={uid} style={{ visibility: 'hidden' }}> | |
<span>Search</span> | |
</label> | |
<input | |
value={searchValue} | |
type="search" | |
placeholder="Search..." | |
onChange={onSearchKey} | |
className="items__search tablet-hide" | |
id={uid} | |
tabIndex="0" | |
/> | |
<div className="items__container"> | |
<div className="items__clear tablet-hide" tabIndex="0"> | |
<button | |
onClick={() => { | |
setSearchValue('') | |
setSelectedFilters([]) | |
dispatch(clearFilter(attribute)) | |
}} | |
disabled={!filters.length} | |
> | |
Clear all | |
</button> | |
</div> | |
<div className="drop-down-options"> | |
{treeData ? ( | |
<NodeTree | |
data={filtersToShow} | |
onToggle={onToggle} | |
selectedFilters={selectedFilters} | |
/> | |
) : ( | |
filtersToShow.map((filter, index) => ( | |
<Checkbox | |
label={filter} | |
checked={selectedFilters.includes(filter)} | |
onToggle={() => onToggle(filter)} | |
squared={true} | |
key={index} | |
/> | |
)) | |
)} | |
</div> | |
</div> | |
</FocusTrap> | |
</div> | |
)} | |
</> | |
) | |
} | |
SearchDropdownValues.propTypes = { | |
filters: PropTypes.arrayOf(PropTypes.string).isRequired, | |
attribute: PropTypes.string.isRequired, | |
toggled: PropTypes.bool, | |
defaultRefinement: PropTypes.string, | |
selectedFilters: PropTypes.array, | |
setSelectedFilters: PropTypes.func.isRequired, | |
treeData: PropTypes.bool | |
} | |
const NodeTree = ({ data, onToggle, selectedFilters }) => { | |
const [localTreeData, setLocalTreeData] = useState([]) | |
useEffect(() => { | |
setLocalTreeData(getTreeData(data)) | |
}, [data]) | |
return ( | |
<TreeOption | |
data={localTreeData} | |
onToggle={onToggle} | |
selectedFilters={selectedFilters} | |
/> | |
) | |
} | |
NodeTree.propTypes = {} | |
NodeTree.defaultProps = {} | |
const TreeOption = ({ data, onToggle, selectedFilters }) => { | |
return data.map((parent, index) => { | |
if (parent.children && parent.children.length) { | |
return ( | |
<ParentNode | |
key={index} | |
data={parent} | |
onToggle={onToggle} | |
selectedFilters={selectedFilters} | |
/> | |
) | |
} else { | |
return ( | |
<ChildNode | |
data={parent} | |
onToggle={onToggle} | |
id={index} | |
key={index} | |
selectedFilters={selectedFilters} | |
/> | |
) | |
} | |
}) | |
} | |
const ParentNode = ({ data, onToggle, selectedFilters }) => { | |
const [treeOpen, setTreeOpen] = useState(false) | |
const children = data.children | |
const parentClick = e => { | |
e.stopPropagation() | |
setTreeOpen(!treeOpen) | |
} | |
return ( | |
<StyledNodeParent> | |
<div className="parent-option" onClick={e => parentClick(e)}> | |
<div className="option"> | |
<div className="sign">{treeOpen ? '-' : '+'}</div> | |
<div className="label">{data.label}</div> | |
</div> | |
<div className="child"> | |
<div className={`child-node ${treeOpen ? 'open' : 'close'}`}> | |
<TreeOption | |
data={data.children} | |
onToggle={onToggle} | |
selectedFilters={selectedFilters} | |
/> | |
</div> | |
</div> | |
</div> | |
</StyledNodeParent> | |
) | |
} | |
const ChildNode = ({ data, onToggle, id, selectedFilters }) => { | |
return ( | |
<Checkbox | |
label={data.label} | |
checked={selectedFilters.includes(data.value)} | |
onToggle={() => onToggle(data.value)} | |
squared={true} | |
key={id} | |
/> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment