components/Input/MultiSelectInput/index.js
import React from 'react';import PropTypes from 'prop-types';import memoize from 'memoize-one';import { listToMap, caseInsensitiveSubmatch, compareStringSearch, isDefined, _cs,} from '@togglecorp/fujs';import { FaramInputElement } from '@togglecorp/faram'; import Button from '../../Action/Button';import DangerButton from '../../Action/Button/DangerButton';import handleKeyboard from '../../General/HandleKeyboard';import HintAndError from '../HintAndError';import Label from '../Label';import RawInput from '../RawInput'; import { calcFloatPositionInMainWindow, defaultOffset, defaultLimit,} from '../../../utils/bounds'; import Options from './Options';import styles from './styles.scss'; const RawKeyInput = handleKeyboard(RawInput);const emptyList = []; // NOTE: labelSelector must return string// NOTE: optionLabelSelector may return renderable nodeexport const propTypes = { autoFocus: PropTypes.bool, disabled: PropTypes.bool, hideClearButton: PropTypes.bool, hideSelectAllButton: PropTypes.bool, readOnly: PropTypes.bool, showHintAndError: PropTypes.bool, showLabel: PropTypes.bool, className: PropTypes.string, error: PropTypes.string, hint: PropTypes.string, label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), optionsClassName: PropTypes.string, placeholder: PropTypes.string, title: PropTypes.string, options: PropTypes.arrayOf(PropTypes.object), // eslint-disable-next-line react/forbid-prop-types value: PropTypes.array, onChange: PropTypes.func.isRequired, keySelector: PropTypes.func, labelSelector: PropTypes.func, optionLabelSelector: PropTypes.func, renderEmpty: PropTypes.func, }; export const defaultProps = { autoFocus: undefined, className: '', disabled: false, error: undefined, hideClearButton: false, hideSelectAllButton: false, hint: undefined, keySelector: d => d.key, label: '', labelSelector: d => d.label, optionLabelSelector: undefined, options: emptyList, optionsClassName: '', placeholder: 'Select option(s)', readOnly: false, renderEmpty: undefined, showHintAndError: true, showLabel: true, title: undefined, value: emptyList,}; export class NormalMultiSelectInput extends React.PureComponent { static propTypes = propTypes; static defaultProps = defaultProps; constructor(props) { super(props); this.state = {FIXME found // FIXME: this may break inputInFocus: props.autoFocus, focusedKey: undefined, showOptionsPopup: false, searchValue: undefined, }; this.containerRef = React.createRef(); this.inputRef = React.createRef(); } componentDidMount() { const { current: container } = this.containerRef; if (container) { this.boundingClientRect = container.getBoundingClientRect(); } } findPlaceholderValue = memoize(( options, labelSelector, keySelector, value = [], ) => { const optionsMap = listToMap( options, keySelector, element => element, ); const selectedOptions = value .map(k => optionsMap[k]) .filter(isDefined) .map(v => labelSelector(v)); return selectedOptions.join(', '); }) filterOptions = memoize(( options, labelSelector, value, ) => { const newOptions = options .filter( option => ( value === undefined || caseInsensitiveSubmatch(labelSelector(option), value) ), ) .sort((a, b) => compareStringSearch( labelSelector(a), labelSelector(b), value, )); return newOptions; }) // Helper handleShowOptionsPopup = () => { const { current: input } = this.inputRef; if (input) { input.select(); } // NOTE: this may not be required const { current: container } = this.containerRef; if (container) { this.boundingClientRect = container.getBoundingClientRect(); } this.setState({ showOptionsPopup: true, searchValue: undefined, }); } handleHideOptionsPopup = () => { this.setState({ showOptionsPopup: false, searchValue: undefined, }); } // Input handleInputFocus = () => { this.setState({ inputInFocus: true }); } handleInputBlur = () => { this.setState({ inputInFocus: false }); } handleInputChange = (e) => { const { value } = e.target; // NOTE: this may not be required const { current: container } = this.containerRef; if (container) { this.boundingClientRect = container.getBoundingClientRect(); } this.setState({ showOptionsPopup: true, searchValue: value, }); } // Options handleOptionsInvalidate = (optionsContainer) => { const contentRect = optionsContainer.getBoundingClientRect(); let parentRect = this.boundingClientRect; const { current: container } = this.containerRef; if (container) { parentRect = container.getBoundingClientRect(); } const { showHintAndError } = this.props; const offset = { ...defaultOffset }; if (showHintAndError) { offset.top = 12; } const limit = { ...defaultLimit, minW: parentRect.width, maxW: parentRect.width, }; const optionsContainerPosition = ( calcFloatPositionInMainWindow({ parentRect, contentRect, offset, limit, }) ); return optionsContainerPosition; }; handleOptionSelect = (key) => { const { value, onChange, } = this.props; const newValue = [...value]; const optionIndex = newValue.findIndex(d => d === key); if (optionIndex === -1) { newValue.push(key); } else { newValue.splice(optionIndex, 1); } // No need to close modal or reset sort // No need to check if same option was clicked onChange(newValue); } handleClearButtonClick = () => { const { onChange } = this.props; onChange(emptyList); } handleSelectAllButtonClick = () => { const { options, keySelector, onChange, } = this.props; const newValue = options.map(d => keySelector(d)); onChange(newValue); } handleFocusChange = (focusedKey) => { this.setState({ focusedKey }); } Function `render` has 177 lines of code (exceeds 100 allowed). Consider refactoring.
Function `render` has a Cognitive Complexity of 11 (exceeds 10 allowed). Consider refactoring. render() { const { error, hint, keySelector, label, labelSelector, optionsClassName, renderEmpty, showHintAndError, showLabel, title, value, disabled, className: classNameFromProps, options, hideClearButton, hideSelectAllButton, readOnly, autoFocus, placeholder, optionLabelSelector, } = this.props; const { showOptionsPopup, focusedKey, inputInFocus, searchValue, } = this.state; const isFilled = value.length !== 0; const isAllFilled = value.length === options.length; const showClearButton = isFilled && !(hideClearButton || disabled || readOnly); const showSelectAllButton = !isAllFilled && !(hideSelectAllButton || disabled || readOnly); const { current: container } = this.containerRef; const inputTitle = this.findPlaceholderValue(options, labelSelector, keySelector, value); const finalPlaceholder = ( inputTitle || placeholder ); const finalSearchValue = searchValue || ''; const filteredOptions = this.filterOptions( options, labelSelector, searchValue, ); const className = _cs( classNameFromProps, 'multi-select-input', styles.multiSelectInput, showOptionsPopup && styles.showOptions, showOptionsPopup && 'show-options', disabled && 'disabled', disabled && styles.disabled, inputInFocus && styles.inputInFocus, inputInFocus && 'input-in-focus', error && styles.error, error && 'error', hideClearButton && 'hide-clear-button', hideSelectAllButton && 'hide-select-all-button', value.length !== 0 && styles.filled, value.length !== 0 && 'filled', value.length === options.length && 'completely-filled', ); const inputAndActionClassName = ` input-and-actions ${styles.inputAndActions} `; const actionsClassName = ` actions ${styles.actions} `; const dropdownButtonClassName = ` dropdown-button ${styles.dropdownButton} `; const clearButtonClassName = ` clear-button ${styles.clearButton} `; const selectAllButtonClassName = ` select-all-button ${styles.selectAllButton} `; return ( <div className={className} ref={this.containerRef} title={title} > { showLabel && ( <Label text={label} error={!!error} disabled={disabled} active={inputInFocus || showOptionsPopup} /> )} <div className={inputAndActionClassName}> <RawKeyInput className={styles.input} type="text" elementRef={this.inputRef} onBlur={this.handleInputBlur} onFocus={this.handleInputFocus} onClick={this.handleShowOptionsPopup} onChange={this.handleInputChange} value={finalSearchValue} autoFocus={autoFocus} title={inputTitle} placeholder={finalPlaceholder} disabled={disabled || readOnly} focusedKey={focusedKey} options={filteredOptions} keySelector={keySelector} isOptionsShown={showOptionsPopup} onFocusChange={this.handleFocusChange} onHideOptions={this.handleHideOptionsPopup} onShowOptions={this.handleShowOptionsPopup} onOptionSelect={this.handleOptionSelect} /> <div className={actionsClassName}> { showSelectAllButton && ( <Button transparent tabIndex="-1" className={selectAllButtonClassName} onClick={this.handleSelectAllButtonClick} title="Select all options" disabled={disabled || readOnly} type="button" iconName="checkAll" /> )} { showClearButton && ( <DangerButton transparent tabIndex="-1" className={clearButtonClassName} onClick={this.handleClearButtonClick} title="Clear selected option(s)" disabled={disabled || readOnly} iconName="close" /> )} <Button tabIndex="-1" iconName="arrowDropdown" className={dropdownButtonClassName} onClick={this.handleShowOptionsPopup} transparent disabled={disabled || readOnly} /> </div> </div> { showHintAndError && ( <HintAndError error={error} hint={hint} /> )} <Options activeKeys={value} data={filteredOptions} keySelector={keySelector} labelSelector={labelSelector} optionLabelSelector={optionLabelSelector} onBlur={this.handleHideOptionsPopup} onInvalidate={this.handleOptionsInvalidate} onOptionClick={this.handleOptionSelect} onOptionFocus={this.handleFocusChange} className={optionsClassName} parentContainer={container} renderEmpty={renderEmpty} show={showOptionsPopup} focusedKey={focusedKey} /> </div> ); }} export default FaramInputElement(NormalMultiSelectInput);