toggle-corp/react-store

View on GitHub
components/Input/SelectInput/index.js

Summary

Maintainability
A
3 hrs
Test Coverage
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 node
const 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);