ndla-ui
Version:
UI component library for NDLA.
306 lines (284 loc) • 9.89 kB
JSX
/*
* Copyright (c) 2016-present, NDLA.
*
* This source code is licensed under the GPLv3 license found in the
* LICENSE file in the root directory of this source tree.
* FRI OG BEGRENSET
*/
import React, { Component, createElement, Fragment } from 'react';
import PropTypes from 'prop-types';
import BEMHelper from 'react-bem-helper';
import { ChevronDown, ChevronUp } from 'ndla-icons/common';
import { Cross } from 'ndla-icons/action';
import { ActiveFilters } from 'ndla-ui';
import Modal, { ModalHeader, ModalBody, ModalCloseButton } from 'ndla-modal';
import Button from 'ndla-button';
import debounce from 'lodash/debounce';
const filterClasses = new BEMHelper({
name: 'filter',
prefix: 'c-',
});
class FilterListPhone extends Component {
constructor(props) {
super(props);
this.state = {
isNarrowScreen: false,
visibleCount: props.defaultVisibleCount,
};
this.setScreenSizeDebounced = debounce(() => this.setScreenSize(false), 50);
}
componentDidMount() {
this.setScreenSize(true);
window.addEventListener('resize', this.setScreenSizeDebounced);
}
componentWillUnmount() {
this.setScreenSizeDebounced.cancel();
window.removeEventListener('resize', this.setScreenSizeDebounced);
}
setScreenSize(initial = false) {
const isNarrowScreen =
(window.innerWidth || document.documentElement.clientWidth) < 768;
/* eslint react/no-did-mount-set-state: 0 */
if ((initial && isNarrowScreen) || !initial) {
this.setState({
isNarrowScreen,
});
}
/* eslint react/no-did-mount-set-state: 1 */
}
render() {
const {
modifiers,
label,
labelNotVisible,
options,
values,
onChange,
defaultVisibleCount,
showLabel,
hideLabel,
messages,
alignedGroup,
collapseMobile,
activeFiltersNarrow,
} = this.props;
const showAll =
defaultVisibleCount === null || options.length <= defaultVisibleCount;
const labelModifiers = [];
if (labelNotVisible) {
labelModifiers.push('hidden');
}
if (this.state.isNarrowScreen) {
const currentlyActiveFilters = options.filter(option =>
values.some(value => value === option.value),
);
return (
<div
className={
activeFiltersNarrow &&
filterClasses('narrow-active-filters').className
}>
{currentlyActiveFilters.length > 0 && (
<ActiveFilters
filters={currentlyActiveFilters}
onFilterRemove={value => {
onChange(values.filter(option => option !== value), value);
}}
/>
)}
<Modal
size="fullscreen"
animation="slide-up"
backgroundColor="grey"
activateButton={
<Button outline {...filterClasses('modal-button')}>
{messages.openFilter}
</Button>
}>
{onClose => (
<Fragment>
<ModalHeader modifier={['grey-dark', 'left-align']}>
<ModalCloseButton
title={
<Fragment>
<Cross /> {messages.closeFilter}
</Fragment>
}
onClick={onClose}
/>
</ModalHeader>
<ModalBody modifier="no-side-padding-mobile">
<h1 {...filterClasses('label')}>{label}</h1>
<ul
{...filterClasses('item-wrapper', {
'aligned-grouping': alignedGroup,
'collapse-mobile': collapseMobile,
})}>
{options.map(option => (
<li {...filterClasses('item')} key={option.value}>
<input
{...filterClasses('input')}
type="checkbox"
id={option.value}
value={option.value}
checked={values.some(value => value === option.value)}
onChange={event => {
let newValues = null;
if (event.currentTarget.checked) {
newValues = [...values, option.value];
} else {
newValues = values.filter(
value => value !== option.value,
);
}
if (onChange) {
onChange(newValues, option.value);
}
}}
/>
<label htmlFor={option.value}>
<span {...filterClasses('item-checkbox')} />
<span {...filterClasses('text')}>{option.title}</span>
{option.icon
? createElement(option.icon, {
className: `c-icon--22 u-margin-left-small ${
filterClasses('icon').className
}`,
})
: null}
</label>
</li>
))}
</ul>
<div {...filterClasses('usefilter-wrapper')}>
<Button outline onClick={onClose}>
{messages.useFilter}
</Button>
</div>
</ModalBody>
</Fragment>
)}
</Modal>
</div>
);
}
return (
<section {...filterClasses('list', modifiers)}>
<h1 {...filterClasses('label', labelModifiers)}>{label}</h1>
<ul {...filterClasses('item-wrapper')}>
{options.map((option, index) => {
const itemModifiers = [];
const checked = values.some(value => value === option.value);
if (!showAll && !checked && index + 1 > this.state.visibleCount) {
itemModifiers.push('hidden');
}
if (option.noResults) {
itemModifiers.push('no-results');
}
return (
<li {...filterClasses('item', itemModifiers)} key={option.value}>
<input
{...filterClasses('input')}
type="checkbox"
id={option.value}
value={option.value}
tabIndex={option.noResults ? -1 : 0}
checked={checked}
onChange={event => {
let newValues = null;
if (event.currentTarget.checked) {
newValues = [...values, option.value];
} else {
newValues = values.filter(
value => value !== option.value,
);
}
if (onChange) {
onChange(newValues, option.value);
}
}}
/>
<label htmlFor={option.value}>
<span {...filterClasses('item-checkbox')} />
<span {...filterClasses('text')}>{option.title}</span>
{option.icon
? createElement(option.icon, {
className: `c-icon--22 u-margin-left-small ${
filterClasses('icon').className
}`,
})
: null}
</label>
</li>
);
})}
</ul>
{!showAll && (
<button
{...filterClasses('expand')}
type="button"
onClick={() => {
this.setState(prevState => {
if (prevState.visibleCount === defaultVisibleCount) {
return {
visibleCount: options.length,
};
}
return {
visibleCount: defaultVisibleCount,
};
});
}}>
{this.state.visibleCount === defaultVisibleCount ? (
<Fragment>
<span>{showLabel}</span> <ChevronDown />
</Fragment>
) : (
<Fragment>
<span>{hideLabel}</span> <ChevronUp />
</Fragment>
)}
</button>
)}
</section>
);
}
}
const valueShape = PropTypes.oneOfType([PropTypes.string, PropTypes.number]);
FilterListPhone.propTypes = {
children: PropTypes.node,
label: PropTypes.string.isRequired,
labelNotVisible: PropTypes.bool,
modifiers: PropTypes.string,
onChange: PropTypes.func, // isRequired
options: PropTypes.arrayOf(
PropTypes.shape({
title: PropTypes.string.isRequired,
value: valueShape.isRequired,
icon: PropTypes.func,
noResults: PropTypes.bool,
}),
).isRequired,
values: PropTypes.arrayOf(valueShape),
defaultVisibleCount: PropTypes.number,
showLabel: PropTypes.string,
hideLabel: PropTypes.string,
onToggle: PropTypes.func,
alignedGroup: PropTypes.bool,
collapseMobile: PropTypes.bool,
activeFiltersNarrow: PropTypes.bool,
messages: PropTypes.shape({
useFilter: PropTypes.string.isRequired,
openFilter: PropTypes.string.isRequired,
closeFilter: PropTypes.string.isRequired,
}).isRequired,
};
FilterListPhone.defaultProps = {
modifiers: '',
values: [],
defaultVisibleCount: null,
onToggle: null,
alignedGroup: false,
collapseMobile: true,
};
export default FilterListPhone;