@spaced-out/ui-design-system
Version:
Sense UI components library
271 lines (267 loc) • 10.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.DEFAULT_LIMIT_VALUE = void 0;
exports.TokenListInput = TokenListInput;
var React = _interopRequireWildcard(require("react"));
var _react2 = require("@floating-ui/react");
var _without = _interopRequireDefault(require("lodash/without"));
var _hooks = require("../../hooks");
var _space = require("../../styles/variables/_space");
var _typography = require("../../types/typography");
var _classify = _interopRequireDefault(require("../../utils/classify"));
var _clickAway = require("../../utils/click-away");
var _mergeRefs = require("../../utils/merge-refs");
var _tokenListInput = require("../../utils/token-list-input/token-list-input");
var _ButtonDropdown = require("../ButtonDropdown");
var _CircularLoader = require("../CircularLoader");
var _Icon = require("../Icon");
var _Menu = require("../Menu");
var _Text = require("../Text");
var _Tooltip = require("../Tooltip");
var _TokenValueChips = require("./TokenValueChips");
var _TokenListInputModule = _interopRequireDefault(require("./TokenListInput.module.css"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
const DEFAULT_LIMIT_VALUE = exports.DEFAULT_LIMIT_VALUE = 100;
// TODO: use Generics with Constraints when we have typescript.
function TokenListInput(props) {
const {
classNames,
clickAwayRef,
disabled = false,
error,
errorText,
focusOnMount,
helperText,
inputValue = '',
inputPlaceholder = '',
limit = DEFAULT_LIMIT_VALUE,
isLoading,
locked,
onChange,
menu,
onMenuOpen,
onMenuClose,
onInputBlur,
onInputChange,
onInputFocus,
placeholder,
size = 'medium',
tabIndex,
values,
resolveTokenValue,
inputProps,
elevation = 'modal'
} = props;
const menuRef = React.useRef(null);
const {
x,
y,
refs,
strategy,
context
} = (0, _react2.useFloating)({
open: true,
strategy: _ButtonDropdown.STRATEGY_TYPE.absolute,
placement: _ButtonDropdown.ANCHOR_POSITION_TYPE.bottomStart,
whileElementsMounted: _react2.autoUpdate,
middleware: [(0, _react2.flip)(), (0, _react2.offset)(parseInt(_space.spaceXXSmall))]
});
const inputRef = React.useRef();
const dropdownWidth = (0, _hooks.useReferenceElementWidth)(refs.reference?.current);
const onOpenToggle = isOpen => {
if (isOpen) {
onMenuOpen?.();
inputRef.current?.focus();
} else {
onMenuClose?.();
inputRef.current?.blur();
}
};
React.useEffect(() => {
if (focusOnMount) {
inputRef.current?.focus();
}
}, [focusOnMount]);
const addValue = value => {
if (locked || !value || value.disabled) {
return; // Prevent adding values when disabled or locked
}
onInputChange?.('');
// $FlowFixMe[incompatible-use] - token has key property
const existingToken = values.find(token => token.key === value.key);
if (!existingToken) {
onChange([...values, value]);
}
setTimeout(() => {
inputRef.current && inputRef.current.focus();
}, 0);
};
const removeValue = value => {
!disabled && !locked && onChange((0, _without.default)(values, value));
setTimeout(() => {
inputRef.current && inputRef.current.focus();
}, 0);
};
const hideInput = values.length >= limit || disabled || locked;
const handleInputKeyDown = event => {
const value = event.currentTarget.value;
const key = event.key;
// Note: adding this Enter key handler to handle the case where the user is typing a new value
// and presses Enter to add the value to the list (maintain parity with the old TokenListInput)
if (key === 'Enter') {
if (value.trim()) {
event.preventDefault();
let firstOption = null;
if (menu?.options && menu.options.length > 0) {
//$FlowFixMe
firstOption = (0, _tokenListInput.getFirstOption)(menu.options);
} else if (menu?.groupTitleOptions && menu.groupTitleOptions.length > 0) {
//$FlowFixMe
firstOption = (0, _tokenListInput.getFirstOptionFromGroup)(menu.groupTitleOptions);
}
if (firstOption) {
//$FlowFixMe
addValue(firstOption);
}
}
} else if (key === 'Backspace') {
if (inputValue === '' && values.length > 0) {
onChange(values.slice(0, -1));
}
} else if (key === 'Escape') {
event.preventDefault();
setTimeout(() => {
inputRef.current && inputRef.current.blur();
}, 0);
}
};
return /*#__PURE__*/React.createElement(_clickAway.ClickAway, {
closeOnEscapeKeypress: true,
onChange: onOpenToggle,
clickAwayRef: clickAwayRef
}, _ref => {
let {
isOpen,
onOpen,
clickAway,
boundaryRef,
triggerRef
} = _ref;
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
ref: menuRef,
className: (0, _classify.default)(_TokenListInputModule.default.tokenListContainer, classNames?.wrapper),
"data-testid": "TokenListInput"
}, /*#__PURE__*/React.createElement("div", {
onClick: () => {
if (disabled || locked || values.length >= limit) {
return;
}
if (!isOpen) {
onOpen();
}
},
onFocus: e => {
if (disabled || locked || values.length >= limit) {
return;
}
if (!isOpen) {
onOpen();
}
onInputFocus?.(e);
},
onBlur: e => {
onInputBlur?.(e);
},
className: (0, _classify.default)(_TokenListInputModule.default.box, {
[_TokenListInputModule.default.inputDisabled]: disabled,
[_TokenListInputModule.default.medium]: size === 'medium',
[_TokenListInputModule.default.small]: size === 'small',
[_TokenListInputModule.default.withError]: error,
[_TokenListInputModule.default.inputLocked]: locked
}, classNames?.box),
ref: (0, _mergeRefs.mergeRefs)([refs.setReference, triggerRef])
}, /*#__PURE__*/React.createElement(_TokenValueChips.TokenValueChips, {
values: values,
resolveTokenValue: resolveTokenValue,
disabled: disabled,
locked: locked,
onDismiss: removeValue
}), !hideInput && /*#__PURE__*/React.createElement("input", _extends({}, inputProps, {
ref: inputRef,
type: "text",
readOnly: locked,
value: inputValue,
placeholder: values.length === 0 ? placeholder || inputPlaceholder : inputPlaceholder,
onChange: event => {
onInputChange?.(event.target.value);
!isOpen && onOpen();
},
disabled: disabled || locked,
tabIndex: tabIndex,
"data-qa-id": "token-list-input",
onKeyDown: handleInputKeyDown,
className: (0, _classify.default)({
[_TokenListInputModule.default.inputMedium]: size === 'medium',
[_TokenListInputModule.default.inputSmall]: size === 'small'
}, classNames?.input),
autoComplete: "off"
})), isLoading && /*#__PURE__*/React.createElement("div", {
className: _TokenListInputModule.default.loaderContainer
}, /*#__PURE__*/React.createElement(_CircularLoader.CircularLoader, {
size: "small",
colorToken: "colorFillPrimary"
})), locked && /*#__PURE__*/React.createElement(_Icon.Icon, {
name: "lock",
color: disabled ? 'disabled' : 'secondary',
size: "small",
className: _TokenListInputModule.default.lockIcon
})), !isOpen && (Boolean(helperText) || errorText) && /*#__PURE__*/React.createElement("div", {
className: _TokenListInputModule.default.footerTextContainer
}, error && errorText ? /*#__PURE__*/React.createElement(_Text.BodySmall, {
color: _typography.TEXT_COLORS.danger
}, errorText) : typeof helperText === 'string' ? /*#__PURE__*/React.createElement(_Text.BodySmall, {
color: _typography.TEXT_COLORS.secondary
}, helperText) : helperText), !locked && isOpen && menu && /*#__PURE__*/React.createElement(_react2.FloatingPortal, null, /*#__PURE__*/React.createElement(_react2.FloatingFocusManager, {
modal: false,
context: context,
returnFocus: false,
initialFocus: refs.reference
}, /*#__PURE__*/React.createElement("div", {
className: _TokenListInputModule.default.menuWrapper,
ref: (0, _mergeRefs.mergeRefs)([refs.setFloating, boundaryRef]),
style: {
position: strategy,
top: y ?? _space.spaceNone,
left: x ?? _space.spaceNone,
/* NOTE(Sharad): The FloatingPortal renders the menu outside the normal DOM structure,
so its parent is effectively the <body> element. This means the menu
would otherwise default to the body's width. To support fluid width,
we must manually set the dropdown width here; otherwise, it uses a fixed width.
Also, Only treat menu as non-fluid if isFluid is strictly false, since default is true in menu and undefined means fluid. */
...(menu.isFluid !== false && {
'--dropdown-width': dropdownWidth
}),
'--menu-elevation': (0, _Tooltip.getElevationValue)(elevation)
}
}, /*#__PURE__*/React.createElement(_Menu.Menu, _extends({}, menu, {
onSelect: option => {
if (values.length >= limit) {
return;
}
// $FlowFixMe[incompatible-call] option from Menu is MenuOption but addValue expects T
// $FlowFixMe[prop-missing] MenuOption properties are missing in T
addValue(option);
clickAway();
inputRef.current?.focus();
},
size: menu.size || size,
onTabOut: clickAway,
ref: menuRef
})))))));
});
}
TokenListInput.displayName = 'TokenListInput';