UNPKG

lucid-ui

Version:

A UI component library from AppNexus.

183 lines (182 loc) 7.71 kB
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 };