@spaced-out/ui-design-system 0.3.37 → 0.3.38-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cspell/custom-words.txt +1 -0
- package/CHANGELOG.md +7 -0
- package/lib/components/Chip/Chip.js +2 -1
- package/lib/components/Chip/Chip.js.flow +2 -0
- package/lib/components/TokenListInput/TokenListInput.js +237 -0
- package/lib/components/TokenListInput/TokenListInput.js.flow +344 -0
- package/lib/components/TokenListInput/TokenListInput.module.css +147 -0
- package/lib/components/TokenListInput/TokenValueChips.js +59 -0
- package/lib/components/TokenListInput/TokenValueChips.js.flow +71 -0
- package/lib/components/TokenListInput/index.js +16 -0
- package/lib/components/TokenListInput/index.js.flow +3 -0
- package/lib/components/index.js +11 -0
- package/lib/components/index.js.flow +1 -0
- package/lib/hooks/index.js +22 -0
- package/lib/hooks/index.js.flow +2 -0
- package/lib/hooks/useArbitraryOptionAddition/index.js +16 -0
- package/lib/hooks/useArbitraryOptionAddition/index.js.flow +3 -0
- package/lib/hooks/useArbitraryOptionAddition/useArbitraryOptionAddition.js +98 -0
- package/lib/hooks/useArbitraryOptionAddition/useArbitraryOptionAddition.js.flow +117 -0
- package/lib/hooks/useFilteredOptions/index.js +16 -0
- package/lib/hooks/useFilteredOptions/index.js.flow +3 -0
- package/lib/hooks/useFilteredOptions/useFilteredOptions.js +62 -0
- package/lib/hooks/useFilteredOptions/useFilteredOptions.js.flow +81 -0
- package/lib/hooks/useInputState/useInputState.js +5 -3
- package/lib/hooks/useInputState/useInputState.js.flow +3 -3
- package/lib/utils/token-list-input/token-list-input.js +28 -0
- package/lib/utils/token-list-input/token-list-input.js.flow +37 -0
- package/package.json +1 -1
package/.cspell/custom-words.txt
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [0.3.38-beta.0](https://github.com/spaced-out/ui-design-system/compare/v0.3.37...v0.3.38-beta.0) (2025-05-09)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **token-list-input:** [GDS-508] token list input and hooks to support it ([#338](https://github.com/spaced-out/ui-design-system/issues/338)) ([4177a80](https://github.com/spaced-out/ui-design-system/commit/4177a806cfe44ab777ba16da661e092fa1a0c345))
|
|
11
|
+
|
|
5
12
|
### [0.3.37](https://github.com/spaced-out/ui-design-system/compare/v0.3.36...v0.3.37) (2025-04-28)
|
|
6
13
|
|
|
7
14
|
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.DEFAULT_LIMIT_VALUE = void 0;
|
|
7
|
+
exports.TokenListInput = TokenListInput;
|
|
8
|
+
var React = _interopRequireWildcard(require("react"));
|
|
9
|
+
var _react2 = require("@floating-ui/react");
|
|
10
|
+
var _size = require("../../styles/variables/_size");
|
|
11
|
+
var _space = require("../../styles/variables/_space");
|
|
12
|
+
var _typography = require("../../types/typography");
|
|
13
|
+
var _classify = _interopRequireDefault(require("../../utils/classify"));
|
|
14
|
+
var _clickAway = require("../../utils/click-away");
|
|
15
|
+
var _mergeRefs = require("../../utils/merge-refs");
|
|
16
|
+
var _tokenListInput = require("../../utils/token-list-input/token-list-input");
|
|
17
|
+
var _ButtonDropdown = require("../ButtonDropdown");
|
|
18
|
+
var _Icon = require("../Icon");
|
|
19
|
+
var _Menu = require("../Menu");
|
|
20
|
+
var _Text = require("../Text");
|
|
21
|
+
var _TokenValueChips = require("./TokenValueChips");
|
|
22
|
+
var _TokenListInputModule = _interopRequireDefault(require("./TokenListInput.module.css"));
|
|
23
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
24
|
+
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
|
25
|
+
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
|
26
|
+
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
27
|
+
const DEFAULT_LIMIT_VALUE = 100;
|
|
28
|
+
exports.DEFAULT_LIMIT_VALUE = DEFAULT_LIMIT_VALUE;
|
|
29
|
+
function TokenListInput(props) {
|
|
30
|
+
const {
|
|
31
|
+
classNames,
|
|
32
|
+
clickAwayRef,
|
|
33
|
+
disabled = false,
|
|
34
|
+
errorText,
|
|
35
|
+
focusOnMount,
|
|
36
|
+
helperText,
|
|
37
|
+
inputValue = '',
|
|
38
|
+
inputPlaceholder = '',
|
|
39
|
+
limit = DEFAULT_LIMIT_VALUE,
|
|
40
|
+
locked,
|
|
41
|
+
onChange,
|
|
42
|
+
menu,
|
|
43
|
+
onMenuOpen,
|
|
44
|
+
onMenuClose,
|
|
45
|
+
onInputBlur,
|
|
46
|
+
onInputChange,
|
|
47
|
+
onInputFocus,
|
|
48
|
+
placeholder,
|
|
49
|
+
size = 'medium',
|
|
50
|
+
tabIndex,
|
|
51
|
+
values,
|
|
52
|
+
resolveTokenValue,
|
|
53
|
+
inputProps
|
|
54
|
+
} = props;
|
|
55
|
+
const menuRef = React.useRef(null);
|
|
56
|
+
const {
|
|
57
|
+
x,
|
|
58
|
+
y,
|
|
59
|
+
refs,
|
|
60
|
+
strategy
|
|
61
|
+
} = (0, _react2.useFloating)({
|
|
62
|
+
open: true,
|
|
63
|
+
strategy: _ButtonDropdown.STRATEGY_TYPE.absolute,
|
|
64
|
+
placement: _ButtonDropdown.ANCHOR_POSITION_TYPE.bottomStart,
|
|
65
|
+
whileElementsMounted: _react2.autoUpdate,
|
|
66
|
+
middleware: [(0, _react2.flip)(), (0, _react2.offset)(parseInt(_space.spaceXXSmall))]
|
|
67
|
+
});
|
|
68
|
+
const inputRef = React.useRef();
|
|
69
|
+
const onOpenToggle = isOpen => {
|
|
70
|
+
if (isOpen) {
|
|
71
|
+
onMenuOpen?.();
|
|
72
|
+
inputRef.current?.focus();
|
|
73
|
+
} else {
|
|
74
|
+
onMenuClose?.();
|
|
75
|
+
inputRef.current?.blur();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
React.useEffect(() => {
|
|
79
|
+
if (focusOnMount) {
|
|
80
|
+
inputRef.current?.focus();
|
|
81
|
+
}
|
|
82
|
+
}, [focusOnMount]);
|
|
83
|
+
const addValue = value => {
|
|
84
|
+
if (locked || !value || value.disabled) {
|
|
85
|
+
return; // Prevent adding values when disabled or locked
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
onInputChange?.('');
|
|
89
|
+
// $FlowFixMe[incompatible-use] - token has key property
|
|
90
|
+
const existingToken = values.find(token => token.key === value.key);
|
|
91
|
+
if (!existingToken) {
|
|
92
|
+
onChange([...values, value]);
|
|
93
|
+
}
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
inputRef.current && inputRef.current.focus();
|
|
96
|
+
}, 0);
|
|
97
|
+
};
|
|
98
|
+
const hideInput = values.length >= limit || disabled || locked;
|
|
99
|
+
const handleInputKeyDown = event => {
|
|
100
|
+
const value = event.currentTarget.value;
|
|
101
|
+
const key = event.key;
|
|
102
|
+
|
|
103
|
+
// Note: adding this Enter key handler to handle the case where the user is typing a new value
|
|
104
|
+
// and presses Enter to add the value to the list (maintain parity with the old TokenListInput)
|
|
105
|
+
if (key === 'Enter') {
|
|
106
|
+
if (value.trim()) {
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
let firstOption = null;
|
|
109
|
+
if (menu?.options && menu.options.length > 0) {
|
|
110
|
+
//$FlowFixMe
|
|
111
|
+
firstOption = (0, _tokenListInput.getFirstOption)(menu.options);
|
|
112
|
+
} else if (menu?.groupTitleOptions && menu.groupTitleOptions.length > 0) {
|
|
113
|
+
//$FlowFixMe
|
|
114
|
+
firstOption = (0, _tokenListInput.getFirstOptionFromGroup)(menu.groupTitleOptions);
|
|
115
|
+
}
|
|
116
|
+
if (firstOption) {
|
|
117
|
+
//$FlowFixMe
|
|
118
|
+
addValue(firstOption);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} else if (key === 'Backspace') {
|
|
122
|
+
if (inputValue === '' && values.length > 0) {
|
|
123
|
+
onChange(values.slice(0, -1));
|
|
124
|
+
}
|
|
125
|
+
} else if (key === 'Escape') {
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
inputRef.current && inputRef.current.blur();
|
|
129
|
+
}, 0);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
return /*#__PURE__*/React.createElement(_clickAway.ClickAway, {
|
|
133
|
+
closeOnEscapeKeypress: true,
|
|
134
|
+
onChange: onOpenToggle,
|
|
135
|
+
clickAwayRef: clickAwayRef
|
|
136
|
+
}, _ref => {
|
|
137
|
+
let {
|
|
138
|
+
isOpen,
|
|
139
|
+
onOpen,
|
|
140
|
+
clickAway,
|
|
141
|
+
boundaryRef,
|
|
142
|
+
triggerRef
|
|
143
|
+
} = _ref;
|
|
144
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
|
145
|
+
className: (0, _classify.default)(_TokenListInputModule.default.tokenListContainer, classNames?.wrapper),
|
|
146
|
+
ref: menuRef
|
|
147
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
148
|
+
onClick: () => {
|
|
149
|
+
if (disabled || locked || values.length >= limit) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!isOpen) {
|
|
153
|
+
onOpen();
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
onFocus: e => {
|
|
157
|
+
if (disabled || locked || values.length >= limit) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!isOpen) {
|
|
161
|
+
onOpen();
|
|
162
|
+
}
|
|
163
|
+
onInputFocus?.(e);
|
|
164
|
+
},
|
|
165
|
+
onBlur: e => {
|
|
166
|
+
onInputBlur?.(e);
|
|
167
|
+
},
|
|
168
|
+
className: (0, _classify.default)(_TokenListInputModule.default.box, {
|
|
169
|
+
[_TokenListInputModule.default.inputDisabled]: disabled,
|
|
170
|
+
[_TokenListInputModule.default.medium]: size === 'medium',
|
|
171
|
+
[_TokenListInputModule.default.small]: size === 'small',
|
|
172
|
+
[_TokenListInputModule.default.withError]: errorText,
|
|
173
|
+
[_TokenListInputModule.default.inputLocked]: locked
|
|
174
|
+
}, classNames?.box),
|
|
175
|
+
ref: (0, _mergeRefs.mergeRefs)([refs.setReference, triggerRef])
|
|
176
|
+
}, /*#__PURE__*/React.createElement(_TokenValueChips.TokenValueChips, {
|
|
177
|
+
values: values,
|
|
178
|
+
resolveTokenValue: resolveTokenValue,
|
|
179
|
+
disabled: disabled,
|
|
180
|
+
locked: locked,
|
|
181
|
+
onChange: onChange
|
|
182
|
+
}), !hideInput && /*#__PURE__*/React.createElement("input", _extends({}, inputProps, {
|
|
183
|
+
ref: inputRef,
|
|
184
|
+
type: "text",
|
|
185
|
+
readOnly: locked,
|
|
186
|
+
value: inputValue,
|
|
187
|
+
placeholder: values.length === 0 ? placeholder || inputPlaceholder : inputPlaceholder,
|
|
188
|
+
onChange: event => {
|
|
189
|
+
onInputChange?.(event.target.value);
|
|
190
|
+
!isOpen && onOpen();
|
|
191
|
+
},
|
|
192
|
+
disabled: disabled || locked,
|
|
193
|
+
tabIndex: tabIndex,
|
|
194
|
+
"data-qa-id": "token-list-input",
|
|
195
|
+
onKeyDown: handleInputKeyDown,
|
|
196
|
+
className: (0, _classify.default)({
|
|
197
|
+
[_TokenListInputModule.default.inputMedium]: size === 'medium',
|
|
198
|
+
[_TokenListInputModule.default.inputSmall]: size === 'small'
|
|
199
|
+
}, classNames?.input),
|
|
200
|
+
autoComplete: "off"
|
|
201
|
+
})), locked && /*#__PURE__*/React.createElement(_Icon.Icon, {
|
|
202
|
+
name: "lock",
|
|
203
|
+
color: disabled ? 'disabled' : 'secondary',
|
|
204
|
+
size: "small",
|
|
205
|
+
className: _TokenListInputModule.default.lockIcon
|
|
206
|
+
})), !isOpen && (Boolean(helperText) || errorText) && /*#__PURE__*/React.createElement("div", {
|
|
207
|
+
className: _TokenListInputModule.default.footerTextContainer
|
|
208
|
+
}, errorText ? /*#__PURE__*/React.createElement(_Text.BodySmall, {
|
|
209
|
+
color: _typography.TEXT_COLORS.danger
|
|
210
|
+
}, errorText) : typeof helperText === 'string' ? /*#__PURE__*/React.createElement(_Text.BodySmall, {
|
|
211
|
+
color: _typography.TEXT_COLORS.secondary
|
|
212
|
+
}, helperText) : helperText), !locked && isOpen && menu && /*#__PURE__*/React.createElement("div", {
|
|
213
|
+
ref: (0, _mergeRefs.mergeRefs)([refs.setFloating, boundaryRef]),
|
|
214
|
+
style: {
|
|
215
|
+
position: strategy,
|
|
216
|
+
top: y ?? _space.spaceNone,
|
|
217
|
+
left: x ?? _space.spaceNone,
|
|
218
|
+
width: _size.sizeFluid
|
|
219
|
+
}
|
|
220
|
+
}, /*#__PURE__*/React.createElement(_Menu.Menu, _extends({}, menu, {
|
|
221
|
+
onSelect: option => {
|
|
222
|
+
if (values.length >= limit) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
// $FlowFixMe[incompatible-call] option from Menu is MenuOption but addValue expects T
|
|
226
|
+
// $FlowFixMe[prop-missing] MenuOption properties are missing in T
|
|
227
|
+
addValue(option);
|
|
228
|
+
clickAway();
|
|
229
|
+
inputRef.current?.focus();
|
|
230
|
+
},
|
|
231
|
+
size: menu.size || size,
|
|
232
|
+
onTabOut: clickAway,
|
|
233
|
+
ref: menuRef
|
|
234
|
+
})))));
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
TokenListInput.displayName = 'TokenListInput';
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// @flow strict
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import {
|
|
5
|
+
// $FlowFixMe[untyped-import]
|
|
6
|
+
autoUpdate,
|
|
7
|
+
// $FlowFixMe[untyped-import]
|
|
8
|
+
flip,
|
|
9
|
+
// $FlowFixMe[untyped-import]
|
|
10
|
+
offset,
|
|
11
|
+
// $FlowFixMe[untyped-import]
|
|
12
|
+
useFloating,
|
|
13
|
+
} from '@floating-ui/react';
|
|
14
|
+
|
|
15
|
+
import {sizeFluid} from '../../styles/variables/_size';
|
|
16
|
+
import {spaceNone, spaceXXSmall} from '../../styles/variables/_space';
|
|
17
|
+
import {TEXT_COLORS} from '../../types/typography';
|
|
18
|
+
import classify from '../../utils/classify';
|
|
19
|
+
import {ClickAway} from '../../utils/click-away';
|
|
20
|
+
import type {ClickAwayRefType} from '../../utils/click-away/click-away';
|
|
21
|
+
import {mergeRefs} from '../../utils/merge-refs';
|
|
22
|
+
import {
|
|
23
|
+
getFirstOption,
|
|
24
|
+
getFirstOptionFromGroup,
|
|
25
|
+
} from '../../utils/token-list-input/token-list-input';
|
|
26
|
+
import {ANCHOR_POSITION_TYPE, STRATEGY_TYPE} from '../ButtonDropdown';
|
|
27
|
+
import {Icon} from '../Icon';
|
|
28
|
+
import type {InputProps} from '../Input';
|
|
29
|
+
import type {BaseMenuProps} from '../Menu';
|
|
30
|
+
import {Menu} from '../Menu';
|
|
31
|
+
import {BodySmall} from '../Text';
|
|
32
|
+
|
|
33
|
+
import type {ResolveTokenValueProps} from './TokenValueChips';
|
|
34
|
+
import {TokenValueChips} from './TokenValueChips';
|
|
35
|
+
|
|
36
|
+
import css from './TokenListInput.module.css';
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
export const DEFAULT_LIMIT_VALUE = 100;
|
|
40
|
+
|
|
41
|
+
type ClassNames = $ReadOnly<{
|
|
42
|
+
wrapper?: string,
|
|
43
|
+
box?: string,
|
|
44
|
+
input?: string,
|
|
45
|
+
}>;
|
|
46
|
+
|
|
47
|
+
export type TokenListMenuOptionTypes<T> = {
|
|
48
|
+
options?: Array<T>,
|
|
49
|
+
groupTitleOptions?: Array<TokenGroupTitleOption<T>>,
|
|
50
|
+
resolveLabel?: (option: T) => string,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type TokenGroupTitleOption<T> = {
|
|
54
|
+
groupTitle?: React.Node,
|
|
55
|
+
options?: Array<T>,
|
|
56
|
+
showLineDivider?: boolean,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type TokenListMenuProps<T> = {
|
|
60
|
+
...BaseMenuProps,
|
|
61
|
+
...TokenListMenuOptionTypes<T>,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// TODO: use Generics with Constraints when we have typescript.
|
|
65
|
+
|
|
66
|
+
export type Props<T> = {
|
|
67
|
+
classNames?: ClassNames,
|
|
68
|
+
clickAwayRef?: ClickAwayRefType,
|
|
69
|
+
disabled?: boolean, // disables user interaction with the input
|
|
70
|
+
errorText?: string,
|
|
71
|
+
focusOnMount?: boolean,
|
|
72
|
+
helperText?: string,
|
|
73
|
+
inputValue?: string,
|
|
74
|
+
inputPlaceholder?: string,
|
|
75
|
+
limit?: number, // maximum number of values
|
|
76
|
+
locked?: boolean,
|
|
77
|
+
onChange: (values: Array<T>) => mixed, // an onChange handler
|
|
78
|
+
onInputBlur?: (e: SyntheticEvent<HTMLInputElement>) => mixed,
|
|
79
|
+
onInputChange?: (value: string) => mixed,
|
|
80
|
+
onInputFocus?: (e: SyntheticEvent<HTMLInputElement>) => mixed,
|
|
81
|
+
placeholder?: string,
|
|
82
|
+
size?: 'medium' | 'small',
|
|
83
|
+
tabIndex?: number,
|
|
84
|
+
values: Array<T>, // a list of options representing the current value of the input
|
|
85
|
+
menu?: TokenListMenuProps<T>,
|
|
86
|
+
onMenuOpen?: () => mixed,
|
|
87
|
+
onMenuClose?: () => mixed,
|
|
88
|
+
resolveTokenValue?: (ResolveTokenValueProps<T>) => React.Node,
|
|
89
|
+
inputProps?: InputProps,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export function TokenListInput<T>(props: Props<T>): React.Node {
|
|
93
|
+
const {
|
|
94
|
+
classNames,
|
|
95
|
+
clickAwayRef,
|
|
96
|
+
disabled = false,
|
|
97
|
+
errorText,
|
|
98
|
+
focusOnMount,
|
|
99
|
+
helperText,
|
|
100
|
+
inputValue = '',
|
|
101
|
+
inputPlaceholder = '',
|
|
102
|
+
limit = DEFAULT_LIMIT_VALUE,
|
|
103
|
+
locked,
|
|
104
|
+
onChange,
|
|
105
|
+
menu,
|
|
106
|
+
onMenuOpen,
|
|
107
|
+
onMenuClose,
|
|
108
|
+
onInputBlur,
|
|
109
|
+
onInputChange,
|
|
110
|
+
onInputFocus,
|
|
111
|
+
placeholder,
|
|
112
|
+
size = 'medium',
|
|
113
|
+
tabIndex,
|
|
114
|
+
values,
|
|
115
|
+
resolveTokenValue,
|
|
116
|
+
inputProps,
|
|
117
|
+
} = props;
|
|
118
|
+
|
|
119
|
+
const menuRef = React.useRef<HTMLDivElement | null>(null);
|
|
120
|
+
|
|
121
|
+
const {x, y, refs, strategy} = useFloating({
|
|
122
|
+
open: true,
|
|
123
|
+
strategy: STRATEGY_TYPE.absolute,
|
|
124
|
+
placement: ANCHOR_POSITION_TYPE.bottomStart,
|
|
125
|
+
whileElementsMounted: autoUpdate,
|
|
126
|
+
middleware: [flip(), offset(parseInt(spaceXXSmall))],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const inputRef = React.useRef<?HTMLInputElement>();
|
|
130
|
+
|
|
131
|
+
const onOpenToggle = (isOpen) => {
|
|
132
|
+
if (isOpen) {
|
|
133
|
+
onMenuOpen?.();
|
|
134
|
+
inputRef.current?.focus();
|
|
135
|
+
} else {
|
|
136
|
+
onMenuClose?.();
|
|
137
|
+
inputRef.current?.blur();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
React.useEffect(() => {
|
|
142
|
+
if (focusOnMount) {
|
|
143
|
+
inputRef.current?.focus();
|
|
144
|
+
}
|
|
145
|
+
}, [focusOnMount]);
|
|
146
|
+
|
|
147
|
+
const addValue = (value: T) => {
|
|
148
|
+
if (locked || !value || value.disabled) {
|
|
149
|
+
return; // Prevent adding values when disabled or locked
|
|
150
|
+
}
|
|
151
|
+
onInputChange?.('');
|
|
152
|
+
// $FlowFixMe[incompatible-use] - token has key property
|
|
153
|
+
const existingToken = values.find((token) => token.key === value.key);
|
|
154
|
+
if (!existingToken) {
|
|
155
|
+
onChange([...values, value]);
|
|
156
|
+
}
|
|
157
|
+
setTimeout(() => {
|
|
158
|
+
inputRef.current && inputRef.current.focus();
|
|
159
|
+
}, 0);
|
|
160
|
+
};
|
|
161
|
+
const hideInput = values.length >= limit || disabled || locked;
|
|
162
|
+
|
|
163
|
+
const handleInputKeyDown = (
|
|
164
|
+
event: SyntheticKeyboardEvent<HTMLInputElement>,
|
|
165
|
+
) => {
|
|
166
|
+
const value = event.currentTarget.value;
|
|
167
|
+
const key = event.key;
|
|
168
|
+
|
|
169
|
+
// Note: adding this Enter key handler to handle the case where the user is typing a new value
|
|
170
|
+
// and presses Enter to add the value to the list (maintain parity with the old TokenListInput)
|
|
171
|
+
if (key === 'Enter') {
|
|
172
|
+
if (value.trim()) {
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
let firstOption = null;
|
|
175
|
+
if (menu?.options && menu.options.length > 0) {
|
|
176
|
+
//$FlowFixMe
|
|
177
|
+
firstOption = getFirstOption(menu.options);
|
|
178
|
+
} else if (
|
|
179
|
+
menu?.groupTitleOptions &&
|
|
180
|
+
menu.groupTitleOptions.length > 0
|
|
181
|
+
) {
|
|
182
|
+
//$FlowFixMe
|
|
183
|
+
firstOption = getFirstOptionFromGroup(menu.groupTitleOptions);
|
|
184
|
+
}
|
|
185
|
+
if (firstOption) {
|
|
186
|
+
//$FlowFixMe
|
|
187
|
+
addValue(firstOption);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} else if (key === 'Backspace') {
|
|
191
|
+
if (inputValue === '' && values.length > 0) {
|
|
192
|
+
onChange(values.slice(0, -1));
|
|
193
|
+
}
|
|
194
|
+
} else if (key === 'Escape') {
|
|
195
|
+
event.preventDefault();
|
|
196
|
+
setTimeout(() => {
|
|
197
|
+
inputRef.current && inputRef.current.blur();
|
|
198
|
+
}, 0);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<ClickAway
|
|
204
|
+
closeOnEscapeKeypress={true}
|
|
205
|
+
onChange={onOpenToggle}
|
|
206
|
+
clickAwayRef={clickAwayRef}
|
|
207
|
+
>
|
|
208
|
+
{({isOpen, onOpen, clickAway, boundaryRef, triggerRef}) => (
|
|
209
|
+
<>
|
|
210
|
+
<div
|
|
211
|
+
className={classify(css.tokenListContainer, classNames?.wrapper)}
|
|
212
|
+
ref={menuRef}
|
|
213
|
+
>
|
|
214
|
+
<div
|
|
215
|
+
onClick={() => {
|
|
216
|
+
if (disabled || locked || values.length >= limit) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (!isOpen) {
|
|
220
|
+
onOpen();
|
|
221
|
+
}
|
|
222
|
+
}}
|
|
223
|
+
onFocus={(e) => {
|
|
224
|
+
if (disabled || locked || values.length >= limit) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (!isOpen) {
|
|
228
|
+
onOpen();
|
|
229
|
+
}
|
|
230
|
+
onInputFocus?.(e);
|
|
231
|
+
}}
|
|
232
|
+
onBlur={(e) => {
|
|
233
|
+
onInputBlur?.(e);
|
|
234
|
+
}}
|
|
235
|
+
className={classify(
|
|
236
|
+
css.box,
|
|
237
|
+
{
|
|
238
|
+
[css.inputDisabled]: disabled,
|
|
239
|
+
[css.medium]: size === 'medium',
|
|
240
|
+
[css.small]: size === 'small',
|
|
241
|
+
[css.withError]: errorText,
|
|
242
|
+
[css.inputLocked]: locked,
|
|
243
|
+
},
|
|
244
|
+
classNames?.box,
|
|
245
|
+
)}
|
|
246
|
+
ref={mergeRefs([refs.setReference, triggerRef])}
|
|
247
|
+
>
|
|
248
|
+
<TokenValueChips
|
|
249
|
+
values={values}
|
|
250
|
+
resolveTokenValue={resolveTokenValue}
|
|
251
|
+
disabled={disabled}
|
|
252
|
+
locked={locked}
|
|
253
|
+
onChange={onChange}
|
|
254
|
+
/>
|
|
255
|
+
{!hideInput && (
|
|
256
|
+
<input
|
|
257
|
+
{...inputProps}
|
|
258
|
+
ref={inputRef}
|
|
259
|
+
type="text"
|
|
260
|
+
readOnly={locked}
|
|
261
|
+
value={inputValue}
|
|
262
|
+
placeholder={
|
|
263
|
+
values.length === 0
|
|
264
|
+
? placeholder || inputPlaceholder
|
|
265
|
+
: inputPlaceholder
|
|
266
|
+
}
|
|
267
|
+
onChange={(event) => {
|
|
268
|
+
onInputChange?.(event.target.value);
|
|
269
|
+
!isOpen && onOpen();
|
|
270
|
+
}}
|
|
271
|
+
disabled={disabled || locked}
|
|
272
|
+
tabIndex={tabIndex}
|
|
273
|
+
data-qa-id="token-list-input"
|
|
274
|
+
onKeyDown={handleInputKeyDown}
|
|
275
|
+
className={classify(
|
|
276
|
+
{
|
|
277
|
+
[css.inputMedium]: size === 'medium',
|
|
278
|
+
[css.inputSmall]: size === 'small',
|
|
279
|
+
},
|
|
280
|
+
classNames?.input,
|
|
281
|
+
)}
|
|
282
|
+
autoComplete="off"
|
|
283
|
+
/>
|
|
284
|
+
)}
|
|
285
|
+
{locked && (
|
|
286
|
+
<Icon
|
|
287
|
+
name="lock"
|
|
288
|
+
color={disabled ? 'disabled' : 'secondary'}
|
|
289
|
+
size="small"
|
|
290
|
+
className={css.lockIcon}
|
|
291
|
+
/>
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
{!isOpen && (Boolean(helperText) || errorText) && (
|
|
295
|
+
<div className={css.footerTextContainer}>
|
|
296
|
+
{errorText ? (
|
|
297
|
+
<BodySmall color={TEXT_COLORS.danger}>{errorText}</BodySmall>
|
|
298
|
+
) : typeof helperText === 'string' ? (
|
|
299
|
+
<BodySmall color={TEXT_COLORS.secondary}>
|
|
300
|
+
{helperText}
|
|
301
|
+
</BodySmall>
|
|
302
|
+
) : (
|
|
303
|
+
helperText
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
)}
|
|
307
|
+
{!locked && isOpen && menu && (
|
|
308
|
+
<div
|
|
309
|
+
ref={mergeRefs([refs.setFloating, boundaryRef])}
|
|
310
|
+
style={{
|
|
311
|
+
position: strategy,
|
|
312
|
+
top: y ?? spaceNone,
|
|
313
|
+
left: x ?? spaceNone,
|
|
314
|
+
width: sizeFluid,
|
|
315
|
+
}}
|
|
316
|
+
>
|
|
317
|
+
{/* $FlowFixMe[incompatible-type] Menu expects MenuOption but receives T */}
|
|
318
|
+
{/* $FlowFixMe[prop-missing] MenuOption properties are missing in T */}
|
|
319
|
+
<Menu
|
|
320
|
+
{...menu}
|
|
321
|
+
onSelect={(option) => {
|
|
322
|
+
if (values.length >= limit) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// $FlowFixMe[incompatible-call] option from Menu is MenuOption but addValue expects T
|
|
326
|
+
// $FlowFixMe[prop-missing] MenuOption properties are missing in T
|
|
327
|
+
addValue(option);
|
|
328
|
+
clickAway();
|
|
329
|
+
inputRef.current?.focus();
|
|
330
|
+
}}
|
|
331
|
+
size={menu.size || size}
|
|
332
|
+
onTabOut={clickAway}
|
|
333
|
+
ref={menuRef}
|
|
334
|
+
/>
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
</>
|
|
339
|
+
)}
|
|
340
|
+
</ClickAway>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
TokenListInput.displayName = 'TokenListInput';
|