optimizely-oui
Version:
Optimizely's Component Library.
267 lines (235 loc) • 7.42 kB
JavaScript
import React from "react";
import PropTypes from "prop-types";
import ReactTagsInput from "react-tagsinput";
import Icon from "react-oui-icons";
import classNames from "classnames";
import Token from "../Token";
import { brandBlueDark } from "../../tokens/forimport/index.es";
/**
* @typedef {Object} TokenWrapper
* @property {String} name - The text display for the token.
* @property {String=} style - The style to use for displaying via <Token>
* Indicates how to style the token.
*/
/**
* Key codes which should be interpreted as an
* indication to add the current text as a new token.
*/
const ADD_KEYS = [
/** TAB */
{
keyCode: 9,
match: "\t",
},
/** ENTER */
{
keyCode: 13,
match: "\n",
},
/** COMMA */
{
keyCode: 188,
match: ",",
},
];
/**
* Tokens Input Component
* @param {Object} props - Properties passed to component
* @returns {ReactElement}
*/
export const TokensInput = ({
addOnBlur,
addOnPaste,
className,
extraAddKeys,
hasSearchIcon,
isDisabled,
maxTags,
onChange,
onInputBlur,
onInputChange,
onInputFocus,
placeholder,
tokens,
...props
}) => {
/**
* Wrap the layout in a flexed <div>
* https://github.com/olahol/react-tagsinput#renderlayout
* @param {Array<ReactElement>} tokenComponents - Token components
* @param {ReactElement} inputComponent - Input component
* @returns {ReactElement}
*/
function renderLayout(tokenComponents, inputComponent) {
return (
<div className="flex flex-1 flex-wrap flex-align--center" style={{ resize: "both" }}>
{hasSearchIcon && (
<span className="flex flex-align--center push-half--sides">
<Icon name="search" fill={brandBlueDark} />
</span>
)}
{tokenComponents}
{inputComponent}
</div>
);
}
/**
* Render an OUI Token to display each token.
* https://github.com/olahol/react-tagsinput#rendertag
*
* @param {Object} renderOptions - Values to render token
* @returns {ReactElement}
*/
// eslint-disable-next-line react/prop-types
function renderToken({ tag, key, onRemove }) {
function onDismiss() {
onRemove(key);
}
const { name, style, backgroundColor } = tag;
return (
<Token
key={key}
hasSnugWrap={true}
isDismissible={!isDisabled}
onDismiss={onDismiss}
name={name}
style={style}
backgroundColor={backgroundColor}
showWell={false}
testSection={`token-${name}`}
/>
);
}
/**
* Handler for when a string is pasted into the input.
* Splits the string on all the addKeys and extraAddKeys.
* https://github.com/olahol/react-tagsinput#pastesplit
*
* @param {string} str - The pasted string.
* @returns {Array<string>}
*/
function pasteSplit(str) {
return (
ADD_KEYS.map(k => k.match)
.concat(extraAddKeys)
// Split the string by all our addKeys by joining with a
// \n, which we will use as the final split operator.
.reduce((acc, value) => acc.split(value).join("\n"), str)
.split("\n")
.filter(k => !!k)
);
}
/**
* When the list of tokens changes, convert any string tokens
* to object form and ensure there are no duplicates.
* @param {Array.<TokenWrapper|String>} allTokens - All tokens
*/
function __onChange(allTokens) {
const updatedTokens = allTokens
.reduce((acc, token) => {
// A newly typed token will be in string form
if (typeof token === "string") {
token = { name: token };
}
if (!acc.find(item => item.name === token.name)) {
acc.push(token);
}
return acc;
}, [])
.filter(token => !!token.name);
onChange(updatedTokens);
}
const addKeys = ADD_KEYS.map(k => k.keyCode).concat(extraAddKeys);
const isNumberOfTokensMoreThanOrEqualToMaxTags = tokens.length >= maxTags && maxTags !== -1;
const minWidth = isNumberOfTokensMoreThanOrEqualToMaxTags ? "" : "min-width--150";
return (
<div className={classNames("oui-text-input oui-pill-input text--left flush height--auto", className)} {...props}>
<ReactTagsInput
addKeys={addKeys}
addOnBlur={addOnBlur}
addOnPaste={addOnPaste}
inputProps={{
className: `flex flex--1 ${minWidth} no-border soft-half--ends soft--sides border-radius push-half--left`,
onBlur: onInputBlur,
onChange: onInputChange,
onFocus: onInputFocus,
placeholder: isNumberOfTokensMoreThanOrEqualToMaxTags ? "" : placeholder,
readOnly: isNumberOfTokensMoreThanOrEqualToMaxTags,
disabled: isDisabled,
}}
maxTags={maxTags}
onChange={__onChange}
onlyUnique={true}
pasteSplit={pasteSplit}
renderTag={renderToken}
renderLayout={renderLayout}
value={tokens}
/>
</div>
);
};
TokensInput.propTypes = {
/**
* Adds a new tag on input blur
*/
addOnBlur: PropTypes.bool,
/**
* Adds a new tag on input paste
*/
addOnPaste: PropTypes.bool,
/** CSS class names. */
className: PropTypes.string,
/**
* Additional keycodes which should be considered
* an intent to enter the current string as a new Token.
* See ADD_KEYS above.
*/
extraAddKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])),
/**
* Whether a search icon should be present
*/
hasSearchIcon: PropTypes.bool,
/**
* Whether the input is disabled or not
*/
isDisabled: PropTypes.bool,
/**
* Maximum number of allowed tokens (pass-through to <ReactTagsInput>)
*/
maxTags: PropTypes.number,
/**
* Handler to invoke when the token list changes.
*/
onChange: PropTypes.func.isRequired,
/**
* Handler to invoke when the token input is blurred
*/
onInputBlur: PropTypes.func,
/**
* Handler to invoke when the token input changes
*/
onInputChange: PropTypes.func,
/**
* Handler to invoke when the token input is focused
*/
onInputFocus: PropTypes.func,
/**
* Placeholder text for the input box.
*/
placeholder: PropTypes.string,
/**
* @type {Array.<TokenWrapper>}
*/
tokens: PropTypes.arrayOf(PropTypes.object).isRequired,
};
TokensInput.defaultProps = {
addOnBlur: true,
addOnPaste: true,
extraAddKeys: [],
maxTags: -1,
onInputBlur: () => null,
onInputChange: () => null,
onInputFocus: () => null,
placeholder: "enter tokens",
};
export default TokensInput;