Skip to content

Instantly share code, notes, and snippets.

@sirdarthvader
Created April 25, 2020 16:52
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save sirdarthvader/b021e9b987882ad670cb76591a3a5640 to your computer and use it in GitHub Desktop.
filter-bar-index.js
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