components/Input/SelectInput/index.js
import React from 'react';import PropTypes from 'prop-types';import memoize from 'memoize-one';import { caseInsensitiveSubmatch, compareStringSearch, _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 propTypeKey = PropTypes.oneOfType([ PropTypes.string, PropTypes.number, PropTypes.bool,]); const RawKeyInput = handleKeyboard(RawInput);const emptyList = []; // NOTE: labelSelector must return string// NOTE: optionLabelSelector may return renderable nodeconst propTypes = { autoFocus: PropTypes.bool, disabled: PropTypes.bool, hideClearButton: PropTypes.bool, readOnly: PropTypes.bool, showHintAndError: PropTypes.bool, showLabel: PropTypes.bool, className: PropTypes.string, error: PropTypes.string, hint: PropTypes.string, label: PropTypes.string, optionsClassName: PropTypes.string, placeholder: PropTypes.string, title: PropTypes.string, options: PropTypes.arrayOf(PropTypes.object), value: propTypeKey, onChange: PropTypes.func.isRequired, keySelector: PropTypes.func, labelSelector: PropTypes.func, optionLabelSelector: PropTypes.func, renderEmpty: PropTypes.func, persistentHintAndError: PropTypes.bool,}; const defaultProps = { autoFocus: undefined, className: '', disabled: false, error: undefined, hideClearButton: false, hint: undefined, keySelector: d => d.key, label: '', labelSelector: d => d.label, optionLabelSelector: undefined, options: emptyList, optionsClassName: '', placeholder: 'Select an option', readOnly: false, renderEmpty: undefined, showHintAndError: true, showLabel: true, title: undefined, value: undefined, persistentHintAndError: true,}; export class NormalSelectInput 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(); } } 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; }); findDefaultSearchValue = memoize(( options, labelSelector, keySelector, value, ) => { const activeOption = options.find( d => keySelector(d) === value, ); if (activeOption === undefined) { return ''; } return labelSelector(activeOption); }); // 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 = (optionKey) => { const { value, onChange, } = this.props; this.setState({ showOptionsPopup: false, searchValue: undefined, }); if (optionKey !== value) { onChange(optionKey); } } handleClearButtonClick = () => { const { onChange, value, } = this.props; if (value !== undefined) { onChange(undefined); } } handleFocusChange = (focusedKey) => { this.setState({ focusedKey }); } Function `render` has 151 lines of code (exceeds 100 allowed). Consider refactoring. render() { const { error, hint, keySelector, label, labelSelector, optionLabelSelector, optionsClassName, renderEmpty, showHintAndError, showLabel, title, value, disabled, readOnly, placeholder, autoFocus, hideClearButton, className: classNameFromProps, persistentHintAndError, options, } = this.props; const { showOptionsPopup, focusedKey, inputInFocus, searchValue, } = this.state; // NOTE: finalSearchValue shouldn't be undefined const finalSearchValue = searchValue === undefined ? this.findDefaultSearchValue(options, labelSelector, keySelector, value) : searchValue; const isFilled = finalSearchValue && finalSearchValue.length !== 0; const showClearButton = isFilled && !(hideClearButton || disabled || readOnly); const { current: container } = this.containerRef; const filteredOptions = this.filterOptions( options, labelSelector, // NOTE: Using value instead of finalSearchValue // this will display all options at first searchValue, ); const className = _cs( classNameFromProps, 'select-input', styles.selectInput, showOptionsPopup && styles.showOptions, showOptionsPopup && 'show-options', disabled && styles.disabled, disabled && 'disabled', error && styles.error, error && 'error', inputInFocus && styles.inputInFocus, inputInFocus && 'input-in-focus', hideClearButton && 'hide-clear-button', isFilled && styles.filled, isFilled && 'filled', ); const inputClassName = _cs( styles.inputAndActions, 'input-and-actions', ); const actionsClassName = ` actions ${styles.actions} `; const dropdownButtonClassName = ` dropdown-button ${styles.dropdownButton} `; const clearClassName = ` clear-button ${styles.clearButton} `; return ( <div className={className} ref={this.containerRef} title={title} > { showLabel && ( <Label text={label} error={!!error} disabled={disabled} active={inputInFocus || showOptionsPopup} /> )} <div className={inputClassName}> <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} placeholder={placeholder} 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}> { showClearButton && ( <DangerButton tabIndex="-1" iconName="close" className={clearClassName} onClick={this.handleClearButtonClick} transparent /> )} <Button tabIndex="-1" iconName="arrowDropdown" className={dropdownButtonClassName} onClick={this.handleShowOptionsPopup} disabled={disabled || readOnly} transparent /> </div> </div> <HintAndError show={showHintAndError} error={error} hint={hint} persistent={persistentHintAndError} /> <Options className={optionsClassName} labelSelector={labelSelector} onBlur={this.handleHideOptionsPopup} onInvalidate={this.handleOptionsInvalidate} onOptionClick={this.handleOptionSelect} optionLabelSelector={optionLabelSelector} parentContainer={container} renderEmpty={renderEmpty} value={value} show={showOptionsPopup} keySelector={keySelector} data={filteredOptions} onOptionFocus={this.handleFocusChange} focusedKey={focusedKey} /> </div> ); }} export default FaramInputElement(NormalSelectInput);