UNPKG

ndla-ui

Version:

UI component library for NDLA.

306 lines (284 loc) 9.89 kB
/* * 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;