Skip to content

Commit

Permalink
feat: add searchable dropdown for skills filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
muhammad-ammar committed Mar 23, 2021
1 parent 9a5e351 commit f57f57d
Show file tree
Hide file tree
Showing 12 changed files with 243 additions and 11 deletions.
52 changes: 49 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
"classnames": "^2.2.5",
"query-string": "^5.1.1",
"react-instantsearch-dom": "^6.8.3",
"react-loading-skeleton": "^2.1.1"
"react-loading-skeleton": "^2.1.1",
"lodash.debounce": "^4.0.8"
},
"jest": {
"collectCoverageFrom": [
Expand All @@ -93,7 +94,8 @@
"classnames": "^2.2.5",
"query-string": "^5.1.1",
"react-instantsearch-dom": "^6.8.3",
"react-loading-skeleton": "^2.1.1"
"react-loading-skeleton": "^2.1.1",
"lodash.debounce": "^4.0.8"
},
"dependencies": {}
}
14 changes: 12 additions & 2 deletions src/course-search/FacetDropdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ import PropTypes from 'prop-types';
import { Dropdown } from '@edx/paragon';
import classNames from 'classnames';

const FacetDropdown = ({ title, items, isBold }) => (
const FacetDropdown = ({
title,
items,
isBold,
type,
}) => (
<div className="facet-list">
<Dropdown className="mb-0 mr-md-3">
<Dropdown className={classNames('mb-0 mr-md-3', type)}>
<Dropdown.Toggle
className={
classNames(
Expand All @@ -24,10 +29,15 @@ const FacetDropdown = ({ title, items, isBold }) => (
</div>
);

FacetDropdown.defaultProps = {
type: undefined,
};

FacetDropdown.propTypes = {
title: PropTypes.string.isRequired,
items: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
isBold: PropTypes.bool.isRequired,
type: PropTypes.string,
};

export default FacetDropdown;
25 changes: 24 additions & 1 deletion src/course-search/FacetListBase.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';

import { NO_OPTIONS_FOUND } from './data/constants';
import FacetDropdown from './FacetDropdown';
import TypeaheadFacetDropdown from './TypeaheadFacetDropdown';
import FacetItem from './FacetItem';
import { SearchContext } from './SearchContext';
import {
Expand All @@ -16,6 +17,8 @@ const FacetListBase = ({
isCheckedField,
items,
title,
typeaheadOptions,
searchForItems,
}) => {
/**
* Handles when a facet option is toggled by either updating the appropriate
Expand Down Expand Up @@ -45,7 +48,7 @@ const FacetListBase = ({
const renderItems = useCallback(
() => {
if (!items || !items.length) {
return <span className="py-2 px-2">{NO_OPTIONS_FOUND}</span>;
return <span className="py-2 px-2 no-options-found">{NO_OPTIONS_FOUND}</span>;
}

return items.map(item => {
Expand All @@ -64,6 +67,18 @@ const FacetListBase = ({
[items],
);

if (typeaheadOptions) {
return (
<TypeaheadFacetDropdown
items={renderItems()}
title={title}
isBold={isBold}
options={typeaheadOptions}
searchForItems={searchForItems}
/>
);
}

return (
<FacetDropdown
items={renderItems()}
Expand All @@ -75,6 +90,8 @@ const FacetListBase = ({

FacetListBase.defaultProps = {
isCheckedField: null,
typeaheadOptions: null,
searchForItems: null,
};

FacetListBase.propTypes = {
Expand All @@ -84,6 +101,12 @@ FacetListBase.propTypes = {
isCheckedField: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.shape()).isRequired,
title: PropTypes.string.isRequired,
typeaheadOptions: PropTypes.shape({
placeholder: PropTypes.string.isRequired,
ariaLabel: PropTypes.string.isRequired,
minLength: PropTypes.number.isRequired,
}),
searchForItems: PropTypes.func,
};

export default FacetListBase;
4 changes: 3 additions & 1 deletion src/course-search/SearchFilters.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const SearchFilters = () => {
const searchFacets = useMemo(
() => {
const filtersFromRefinements = searchFacetFilters.map(({
title, attribute, isSortedAlphabetical,
title, attribute, isSortedAlphabetical, typeaheadOptions,
}) => (
<FacetListRefinement
key={attribute}
Expand All @@ -57,6 +57,8 @@ const SearchFilters = () => {
refinementsFromQueryParams={refinementsFromQueryParams}
defaultRefinement={refinementsFromQueryParams[attribute]}
facetValueType="array"
typeaheadOptions={typeaheadOptions}
searchable={!!typeaheadOptions}
/>
));
return (
Expand Down
59 changes: 59 additions & 0 deletions src/course-search/TypeaheadFacetDropdown.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Input } from '@edx/paragon';
import debounce from 'lodash.debounce';
import FacetDropdown from './FacetDropdown';

const TypeaheadFacetDropdown = ({
title,
items,
isBold,
options,
searchForItems,
}) => {
const handleSearch = debounce((value) => {
// when user is erasing the input and input is empty we need to reset the filtering
if (value.length >= options.minLength || value.length === 0) {
searchForItems(value);
}
}, 200);

const transformMenuOptions = menuOptions => (
<>
<Input
autoFocus
type="search"
className="typeahead-dropdown-input"
placeholder={options.placeholder}
aria-label={options.ariaLabel}
onChange={(event) => handleSearch(event.currentTarget.value)}
/>
<div className="typeahead-dropdown-menu-scrollable-items">
{menuOptions}
</div>
</>
);

return (
<FacetDropdown
items={transformMenuOptions(items)}
title={title}
isBold={isBold}
type="typeahead"
/>
);
};

TypeaheadFacetDropdown.propTypes = {
title: PropTypes.string.isRequired,
items: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
isBold: PropTypes.bool.isRequired,
options: PropTypes.shape({
placeholder: PropTypes.string.isRequired,
ariaLabel: PropTypes.string.isRequired,
minLength: PropTypes.number.isRequired,
}).isRequired,
searchForItems: PropTypes.func.isRequired,
};

export default TypeaheadFacetDropdown;
9 changes: 9 additions & 0 deletions src/course-search/data/constants.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
export const SHOW_ALL_NAME = 'showAll';

export const SEARCH_FACET_FILTERS = [
{
attribute: 'skill_names',
title: 'Skills',
typeaheadOptions: {
placeholder: 'Find a skill',
ariaLabel: 'Type to find a skill',
minLength: 3,
},
},
{
attribute: 'subjects',
title: 'Subject',
Expand Down
31 changes: 31 additions & 0 deletions src/course-search/styles/_FacetList.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,35 @@
}
}
}

.typeahead {
.dropdown-menu {
.facet-list & {
padding: 50px 0 0;
max-height: none;
overflow: visible;
width: 420px;
}

.typeahead-dropdown-menu-scrollable-items {
position: relative;
max-height: 200px;
overflow: auto;

.no-options-found {
display: block;
}
}

.typeahead-dropdown-input {
position: absolute;
top: 0;
left: 0;
width: 98%;
background: white;
padding: 5px;
margin: 4px;
}
}
}
}
2 changes: 1 addition & 1 deletion src/course-search/styles/_MobileSearchFilters.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
border: none;
}

.dropdown-menu {
.dropdown-menu, .typeahead-dropdown-menu-scrollable-items {
position: relative;
width: 100%;
box-shadow: none;
Expand Down
2 changes: 1 addition & 1 deletion src/course-search/tests/ClearCurrentRefinements.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('<ClearCurrentRefinements />', () => {

test('dispatches the clear refinements action on click', async () => {
const spy = jest.spyOn(actions, 'clearRefinementsAction');
renderWithRouter(<SearchData><ClearCurrentRefinements /></SearchData>);
renderWithRouter(<SearchData><ClearCurrentRefinements variant="primary" /></SearchData>);

// click a specific refinement to remove it
fireEvent.click(screen.queryByText(CLEAR_ALL_TEXT));
Expand Down
Loading

0 comments on commit f57f57d

Please sign in to comment.