lucid-ui
Version:
A UI component library from AppNexus.
183 lines (182 loc) • 7.71 kB
JavaScript
import React from 'react';
import PropTypes from 'react-peek/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: {
className: string `
Appended to the component-specific class names set on the root elements.
`,
style: object `
Styles that are passed through to root element.
`,
isDisabled: bool `
Disables the Autocomplete from being clicked or focused.
`,
suggestions: arrayOf(string) `
Array of suggested text input values shown in drop menu.
`,
value: string `
Text value of the input.
`,
DropMenu: shape(DropMenu.propTypes) `
Object of DropMenu props which are passed thru to the underlying DropMenu
component.
`,
onChange: func `
Called when the input value changes. Has the signature
\`(value, {props, event}) => {}\` where value is a string.
`,
onSelect: func `
Called when a suggstion is selected from the menu. Has the signature
\`(optionIndex, {props, event}) => {}\` where optionIndex is a number.
`,
onExpand: func `
Called when menu is expected to expand. Has the signature
\`({props, event}) => {}\`.
`,
},
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) {
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, Object.assign({}, dropMenuProps, { isDisabled: isDisabled, selectedIndices: [], className: cx('&', className), onSelect: this.handleSelect, style: style }),
React.createElement(DropMenu.Control, Object.assign({}, { 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", Object.assign({}, _.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 };