lucid-ui
Version:
A UI component library from Xandr.
193 lines • 8.08 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { createClass } from '../../util/component-types';
import { lucidClassNames } from '../../util/style-helpers';
import { buildHybridComponent } from '../../util/state-management';
import { partitionText } from '../../util/text-manipulation';
import * as reducers from './Autocomplete.reducers';
import * as KEYCODE from '../../constants/key-code';
import { DropMenuDumb as DropMenu } from '../DropMenu/DropMenu';
const cx = lucidClassNames.bind('&-Autocomplete');
const { arrayOf, bool, func, object, shape, string } = PropTypes;
const Autocomplete = createClass({
statics: {
peek: {
description: `A text input with suggested values displayed in an attached menu.`,
categories: ['controls', 'text'],
madeFrom: ['DropMenu'],
},
},
displayName: 'Autocomplete',
reducers: reducers,
propTypes: {
/**
Appended to the component-specific class names set on the root elements.
*/
className: string,
/**
Styles that are passed through to root element.
*/
style: object,
/**
Disables the Autocomplete from being clicked or focused.
*/
isDisabled: bool,
/**
Array of suggested text input values shown in drop menu.
*/
suggestions: arrayOf(string),
/**
Text value of the input.
*/
value: string,
/**
Object of DropMenu props which are passed thru to the underlying DropMenu
component.
*/
DropMenu: shape(DropMenu.propTypes),
/**
Called when the input value changes. Has the signature
\`(value, {props, event}) => {}\` where value is a string.
*/
onChange: func,
/**
Called when a suggstion is selected from the menu. Has the signature
\`(optionIndex, {props, event}) => {}\` where optionIndex is a number.
*/
onSelect: func,
/**
Called when menu is expected to expand. Has the signature
\`({props, event}) => {}\`.
*/
onExpand: func,
},
getDefaultProps() {
return {
isDisabled: false,
suggestions: [],
value: '',
onChange: _.noop,
onSelect: _.noop,
onExpand: _.noop,
DropMenu: DropMenu.defaultProps,
}; // TODO: typescript hack that should be removed
},
handleSelect(optionIndex, { event }) {
const { suggestions, onChange, onSelect } = this.props;
onChange(suggestions[optionIndex], { event, props: this.props });
onSelect(optionIndex, { event, props: this.props });
},
handleInput(event) {
const { onChange, onExpand, DropMenu: { onCollapse }, } = this.props;
onChange(event.target.value, { event, props: this.props });
if (!_.isEmpty(event.target.value)) {
onExpand({ event, props: this.props });
}
else {
onCollapse();
}
},
getInputValue() {
return _.get(this, 'inputRef.value', this.props.value);
},
setInputValue(value) {
if (this.inputRef) {
this.inputRef.value = value;
}
},
handleInputKeydown(event) {
const { onExpand, DropMenu: { isExpanded, focusedIndex, onCollapse }, } = this.props;
const value = this.getInputValue();
if (event.keyCode === KEYCODE.Tab && isExpanded && focusedIndex !== null) {
this.handleSelect(focusedIndex, { event, props: this.props });
event.preventDefault();
}
if (event.keyCode === KEYCODE.ArrowDown && !isExpanded) {
event.stopPropagation();
if (_.isEmpty(value)) {
onExpand({ event, props: this.props });
}
}
if (event.keyCode === KEYCODE.Escape) {
event.stopPropagation();
onCollapse(event);
}
if (event.keyCode === KEYCODE.Enter && focusedIndex === null) {
event.stopPropagation();
onCollapse(event);
}
},
handleControlClick(event) {
const { onExpand, DropMenu: { isExpanded, onCollapse }, } = this.props;
if (event.target === this.inputRef) {
onExpand({ event, props: this.props });
}
else {
if (isExpanded) {
onCollapse(event);
}
else {
onExpand({ event, props: this.props });
}
this.inputRef.focus();
}
},
componentDidMount() {
const { value } = this.props;
this.inputRef.addEventListener('input', this.handleInput);
this.setInputValue(value);
},
UNSAFE_componentWillReceiveProps(nextProps) {
// TODO: typescript hack that should be removed
const { value } = nextProps;
if (value !== this.getInputValue()) {
this.setInputValue(value);
}
},
componentWillUnmount() {
if (this.inputRef) {
this.inputRef.removeEventListener('input', this.handleInput);
}
},
render() {
const { style, className, isDisabled, DropMenu: dropMenuProps, suggestions, ...passThroughs } = this.props; // TODO: typescript hack that should be removed
const { isExpanded } = dropMenuProps;
const value = this.getInputValue();
const valuePattern = new RegExp(_.escapeRegExp(value), 'i');
return (React.createElement(DropMenu, { ...dropMenuProps, isDisabled: isDisabled, selectedIndices: [], className: cx('&', className), onSelect: this.handleSelect, style: style },
React.createElement(DropMenu.Control, { ...{
onClick: this.handleControlClick,
} /* TODO: typescript hack that should be removed */ },
React.createElement("div", { className: cx('&-Control', {
'&-Control-is-expanded': isExpanded,
'&-Control-is-disabled': isDisabled,
}) },
React.createElement("input", { ..._.omit(passThroughs, [
'onChange',
'onSelect',
'onExpand',
'value',
'children',
]), type: 'text', className: cx('&-Control-input'), ref: (ref) => (this.inputRef = ref), onKeyDown: this.handleInputKeydown, disabled: isDisabled }))),
value
? _.map(suggestions, (suggestion) => (React.createElement(DropMenu.Option, { key: 'AutocompleteOption' + suggestion }, (() => {
const [pre, match, post] = partitionText(suggestion, valuePattern, value.length);
const formattedSuggestion = [];
if (pre) {
formattedSuggestion.push(React.createElement("span", { key: `AutocompleteOption-suggestion-pre-${suggestion}`, className: cx('&-Option-suggestion-pre') }, pre));
}
if (match) {
formattedSuggestion.push(React.createElement("span", { key: `AutocompleteOption-suggestion-match-${suggestion}`, className: cx('&-Option-suggestion-match') }, match));
}
if (post) {
formattedSuggestion.push(React.createElement("span", { key: `AutocompleteOption-suggestion-post-${suggestion}`, className: cx('&-Option-suggestion-post') }, post));
}
return formattedSuggestion;
})())))
: _.map(suggestions, (suggestion) => (React.createElement(DropMenu.Option, { key: 'AutocompleteOption' + suggestion }, suggestion)))));
},
});
export default buildHybridComponent(Autocomplete);
export { Autocomplete as AutocompleteDumb };
//# sourceMappingURL=Autocomplete.js.map