@sproutsocial/seeds-react-token-input 1.0.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/.eslintignore +6 -0
- package/.eslintrc.js +4 -0
- package/.turbo/turbo-build.log +21 -0
- package/CHANGELOG.md +13 -0
- package/dist/esm/index.js +441 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/index.d.mts +104 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +478 -0
- package/dist/index.js.map +1 -0
- package/jest.config.js +9 -0
- package/package.json +49 -0
- package/src/TokenInput.stories.tsx +235 -0
- package/src/TokenInput.tsx +302 -0
- package/src/TokenInputTypes.ts +119 -0
- package/src/TokenScreenReaderStatus.tsx +46 -0
- package/src/__tests__/TokenInput.test.tsx +672 -0
- package/src/__tests__/TokenInput.typetest.tsx +137 -0
- package/src/index.ts +5 -0
- package/src/styled.d.ts +7 -0
- package/src/styles.ts +136 -0
- package/src/util.ts +22 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +12 -0
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sproutsocial/seeds-react-token-input",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Seeds React TokenInput",
|
|
5
|
+
"author": "Sprout Social, Inc.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"module": "dist/esm/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup --dts",
|
|
12
|
+
"build:debug": "tsup --dts --metafile",
|
|
13
|
+
"dev": "tsup --watch --dts",
|
|
14
|
+
"clean": "rm -rf .turbo dist",
|
|
15
|
+
"clean:modules": "rm -rf node_modules",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "jest",
|
|
18
|
+
"test:watch": "jest --watch --coverage=false"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@sproutsocial/seeds-react-theme": "*",
|
|
22
|
+
"@sproutsocial/seeds-react-system-props": "*",
|
|
23
|
+
"@sproutsocial/seeds-react-mixins": "*",
|
|
24
|
+
"@sproutsocial/seeds-react-input": "*",
|
|
25
|
+
"@sproutsocial/seeds-react-box": "*",
|
|
26
|
+
"@sproutsocial/seeds-react-icon": "*",
|
|
27
|
+
"@sproutsocial/seeds-react-token": "*",
|
|
28
|
+
"lodash.uniqueid": "^4.0.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/react": "^18.0.0",
|
|
32
|
+
"@types/styled-components": "^5.1.26",
|
|
33
|
+
"@types/lodash.uniqueid": "^4.0.1",
|
|
34
|
+
"@sproutsocial/eslint-config-seeds": "*",
|
|
35
|
+
"react": "^18.0.0",
|
|
36
|
+
"styled-components": "^5.2.3",
|
|
37
|
+
"tsup": "^8.0.2",
|
|
38
|
+
"typescript": "^5.6.2",
|
|
39
|
+
"@sproutsocial/seeds-tsconfig": "*",
|
|
40
|
+
"@sproutsocial/seeds-testing": "*",
|
|
41
|
+
"@sproutsocial/seeds-react-testing-library": "*"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"styled-components": "^5.2.3"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import React, { Fragment, useState } from "react";
|
|
2
|
+
import { Icon } from "@sproutsocial/seeds-react-icon";
|
|
3
|
+
import { TokenInput, type TypeTokenInputProps, type TypeTokenSpec } from "./";
|
|
4
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
5
|
+
|
|
6
|
+
const MinimalStatefulTokenInput = (props: TypeTokenInputProps) => {
|
|
7
|
+
const { tokens, ...rest } = props;
|
|
8
|
+
const [tokenSpecs, setTokenSpecs] = useState(tokens || []);
|
|
9
|
+
return (
|
|
10
|
+
<Fragment>
|
|
11
|
+
<TokenInput
|
|
12
|
+
{...rest}
|
|
13
|
+
id="example"
|
|
14
|
+
name="example"
|
|
15
|
+
tokens={tokenSpecs}
|
|
16
|
+
onChangeTokens={setTokenSpecs}
|
|
17
|
+
autocomplete="off"
|
|
18
|
+
/>
|
|
19
|
+
</Fragment>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const MeddlingStatefulTokenInput = (props: TypeTokenInputProps) => {
|
|
24
|
+
const { delimiters, value, tokens, ...rest } = props;
|
|
25
|
+
const [tokenSpecs, setTokenSpecs] = useState(tokens || []);
|
|
26
|
+
const [text, setText] = useState(value);
|
|
27
|
+
|
|
28
|
+
const handleAddToken = (newToken: TypeTokenSpec) => {
|
|
29
|
+
setTokenSpecs((tokens) => tokens.concat(newToken));
|
|
30
|
+
setText("");
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handleRemoveToken = (id: TypeTokenSpec["id"]) => {
|
|
34
|
+
setTokenSpecs((tokens) => tokens.filter((token) => token.id !== id));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// @ts-expect-error: event isn't used but it's required by the type
|
|
38
|
+
const handleChange = (e, newValue: TypeTokenSpec["value"]) => {
|
|
39
|
+
setText(newValue);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// @ts-expect-error: event isn't used but it's required by the type
|
|
43
|
+
const handleClickToken = (e, id: TypeTokenSpec["id"]) => {
|
|
44
|
+
handleRemoveToken(id);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Fragment>
|
|
49
|
+
<TokenInput
|
|
50
|
+
{...rest}
|
|
51
|
+
id="fine-grain"
|
|
52
|
+
name="fine-grain"
|
|
53
|
+
value={text}
|
|
54
|
+
tokens={tokenSpecs}
|
|
55
|
+
delimiters={delimiters}
|
|
56
|
+
onChange={handleChange}
|
|
57
|
+
onAddToken={handleAddToken}
|
|
58
|
+
onRemoveToken={handleRemoveToken}
|
|
59
|
+
onClickToken={handleClickToken}
|
|
60
|
+
/>
|
|
61
|
+
</Fragment>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const meta: Meta<typeof TokenInput> = {
|
|
66
|
+
title: "Components/Form Elements/TokenInput",
|
|
67
|
+
component: TokenInput,
|
|
68
|
+
args: {
|
|
69
|
+
ariaLabel: "Enter names of fruits",
|
|
70
|
+
value: "",
|
|
71
|
+
disabled: undefined,
|
|
72
|
+
isInvalid: undefined,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default meta;
|
|
77
|
+
|
|
78
|
+
type Story = StoryObj<typeof TokenInput>;
|
|
79
|
+
|
|
80
|
+
export const Default: Story = {
|
|
81
|
+
render: (args) => (
|
|
82
|
+
<Fragment>
|
|
83
|
+
<MinimalStatefulTokenInput {...args} />
|
|
84
|
+
</Fragment>
|
|
85
|
+
),
|
|
86
|
+
name: "Default",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const HighControl: Story = {
|
|
90
|
+
args: {
|
|
91
|
+
tokens: [
|
|
92
|
+
{
|
|
93
|
+
id: "0",
|
|
94
|
+
value: "Cherries",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "1",
|
|
98
|
+
value: "Mangos",
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
render: (args) => (
|
|
103
|
+
<Fragment>
|
|
104
|
+
<MeddlingStatefulTokenInput {...args} />
|
|
105
|
+
</Fragment>
|
|
106
|
+
),
|
|
107
|
+
name: "Fine-grained state control",
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const TrimmedTokens: Story = {
|
|
111
|
+
args: {
|
|
112
|
+
tokenMaxLength: 4,
|
|
113
|
+
},
|
|
114
|
+
render: (args) => (
|
|
115
|
+
<Fragment>
|
|
116
|
+
<MinimalStatefulTokenInput {...args} />
|
|
117
|
+
</Fragment>
|
|
118
|
+
),
|
|
119
|
+
name: "Trimming tokens",
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const WithIcon: Story = {
|
|
123
|
+
args: {
|
|
124
|
+
iconName: "lock-outline",
|
|
125
|
+
},
|
|
126
|
+
render: (args) => (
|
|
127
|
+
<Fragment>
|
|
128
|
+
<MinimalStatefulTokenInput {...args} />
|
|
129
|
+
</Fragment>
|
|
130
|
+
),
|
|
131
|
+
name: "Tokens With Icon",
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const WithIndividualIcons: Story = {
|
|
135
|
+
args: {
|
|
136
|
+
iconName: "triangle-outline",
|
|
137
|
+
tokens: [
|
|
138
|
+
{
|
|
139
|
+
id: "0",
|
|
140
|
+
iconName: "face-smile-solid",
|
|
141
|
+
iconProps: {
|
|
142
|
+
"aria-label": "informative emoji icon",
|
|
143
|
+
},
|
|
144
|
+
value: "Cherries",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "1",
|
|
148
|
+
value: "Mangos",
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
render: (args) => (
|
|
153
|
+
<Fragment>
|
|
154
|
+
<MeddlingStatefulTokenInput {...args} />
|
|
155
|
+
</Fragment>
|
|
156
|
+
),
|
|
157
|
+
name: "Tokens With Individual Icons",
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const Disabled: Story = {
|
|
161
|
+
args: {
|
|
162
|
+
tokens: [
|
|
163
|
+
{
|
|
164
|
+
id: "0",
|
|
165
|
+
value: "One",
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "1",
|
|
169
|
+
value: "Two",
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
disabled: true,
|
|
173
|
+
},
|
|
174
|
+
render: (args) => <MinimalStatefulTokenInput {...args} />,
|
|
175
|
+
name: "Disabled",
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const Error: Story = {
|
|
179
|
+
args: {
|
|
180
|
+
isInvalid: true,
|
|
181
|
+
tokens: [
|
|
182
|
+
{
|
|
183
|
+
id: "0",
|
|
184
|
+
value: "One",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: "1",
|
|
188
|
+
value: "Two",
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
render: (args) => <MinimalStatefulTokenInput {...args} />,
|
|
193
|
+
name: "Error",
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const LeftIcon: Story = {
|
|
197
|
+
args: {
|
|
198
|
+
elemBefore: <Icon fixedWidth name="lock-outline" aria-hidden />,
|
|
199
|
+
},
|
|
200
|
+
render: (args) => <MinimalStatefulTokenInput {...args} />,
|
|
201
|
+
name: "Left Icon",
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export const RightIcon: Story = {
|
|
205
|
+
args: {
|
|
206
|
+
elemAfter: <Icon fixedWidth name="lock-outline" aria-hidden />,
|
|
207
|
+
},
|
|
208
|
+
render: (args) => <MinimalStatefulTokenInput {...args} />,
|
|
209
|
+
name: "Right Icon",
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export const LeftAndRightIcons: Story = {
|
|
213
|
+
args: {
|
|
214
|
+
elemBefore: <Icon fixedWidth name="magnifying-glass-outline" aria-hidden />,
|
|
215
|
+
elemAfter: <Icon fixedWidth name="lock-outline" aria-hidden />,
|
|
216
|
+
},
|
|
217
|
+
render: (args) => <MinimalStatefulTokenInput {...args} />,
|
|
218
|
+
name: "Left and right icons",
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export const Autofocus: Story = {
|
|
222
|
+
args: {
|
|
223
|
+
autoFocus: true,
|
|
224
|
+
},
|
|
225
|
+
render: (args) => <MinimalStatefulTokenInput {...args} />,
|
|
226
|
+
name: "Autofocus",
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export const WithPlaceholder: Story = {
|
|
230
|
+
args: {
|
|
231
|
+
placeholder: "Enter a token...",
|
|
232
|
+
},
|
|
233
|
+
render: (args) => <MinimalStatefulTokenInput {...args} />,
|
|
234
|
+
name: "With Placeholder",
|
|
235
|
+
};
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Accessory } from "@sproutsocial/seeds-react-input";
|
|
3
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
4
|
+
import Icon from "@sproutsocial/seeds-react-icon";
|
|
5
|
+
import Token from "@sproutsocial/seeds-react-token";
|
|
6
|
+
import Container from "./styles";
|
|
7
|
+
import { asTokenSpec, delimitersAsRegExp } from "./util";
|
|
8
|
+
import type { TypeTokenInputProps, TypeTokenSpec } from "./TokenInputTypes";
|
|
9
|
+
import { TokenScreenReaderStatus } from "./TokenScreenReaderStatus";
|
|
10
|
+
|
|
11
|
+
type TypeState = {
|
|
12
|
+
prevProps: TypeTokenInputProps;
|
|
13
|
+
hasFocus: boolean;
|
|
14
|
+
activeToken: string | null | undefined;
|
|
15
|
+
value: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const DefaultDelimiters = [",", "Enter"];
|
|
19
|
+
const ControlledPropNames: (keyof TypeState)[] = [
|
|
20
|
+
"value",
|
|
21
|
+
"hasFocus",
|
|
22
|
+
"activeToken",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export default class TokenInput extends React.Component<
|
|
26
|
+
TypeTokenInputProps,
|
|
27
|
+
TypeState
|
|
28
|
+
> {
|
|
29
|
+
delimiterMatcher: RegExp;
|
|
30
|
+
|
|
31
|
+
constructor(props: TypeTokenInputProps) {
|
|
32
|
+
super(props);
|
|
33
|
+
const { hasFocus, activeToken, value, delimiters } = props;
|
|
34
|
+
this.delimiterMatcher = delimitersAsRegExp(delimiters || DefaultDelimiters);
|
|
35
|
+
this.state = {
|
|
36
|
+
prevProps: props,
|
|
37
|
+
hasFocus: hasFocus || false,
|
|
38
|
+
activeToken: activeToken,
|
|
39
|
+
value: value || "",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static getDerivedStateFromProps(
|
|
44
|
+
props: Readonly<TypeTokenInputProps>,
|
|
45
|
+
state: TypeState
|
|
46
|
+
) {
|
|
47
|
+
const { prevProps } = state;
|
|
48
|
+
const modifiedState: Partial<TypeState> = { prevProps: props };
|
|
49
|
+
ControlledPropNames.forEach((propName) => {
|
|
50
|
+
const currentProp = props[propName as keyof TypeTokenInputProps];
|
|
51
|
+
|
|
52
|
+
// @ts-ignore: TODO - fix state types for prevProps
|
|
53
|
+
if (currentProp !== prevProps[propName]) {
|
|
54
|
+
modifiedState[propName] = currentProp;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
modifiedState.prevProps = props;
|
|
58
|
+
return modifiedState;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
isDelimiter(keyName: string) {
|
|
62
|
+
const { delimiters = DefaultDelimiters } = this.props;
|
|
63
|
+
return delimiters.includes(keyName);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
spawnNewTokens(texts: string[]) {
|
|
67
|
+
const {
|
|
68
|
+
onAddToken,
|
|
69
|
+
onChangeTokens,
|
|
70
|
+
tokenMaxLength: max = Infinity,
|
|
71
|
+
tokens = [],
|
|
72
|
+
} = this.props;
|
|
73
|
+
const tokenSpecs = texts.map((text) => asTokenSpec(text.slice(0, max)));
|
|
74
|
+
|
|
75
|
+
if (onAddToken) {
|
|
76
|
+
tokenSpecs.forEach(onAddToken);
|
|
77
|
+
} else if (onChangeTokens) {
|
|
78
|
+
onChangeTokens(tokens.concat(tokenSpecs));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.setState({
|
|
82
|
+
value: "",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
deleteToken(tokenId?: string) {
|
|
87
|
+
const { onRemoveToken, onChangeTokens, tokens = [] } = this.props;
|
|
88
|
+
const count = tokens.length;
|
|
89
|
+
if (count === 0) return;
|
|
90
|
+
const id = tokenId ?? tokens[count - 1]?.id;
|
|
91
|
+
|
|
92
|
+
if (onRemoveToken) {
|
|
93
|
+
onRemoveToken(id ? id : "");
|
|
94
|
+
} else if (onChangeTokens) {
|
|
95
|
+
onChangeTokens(tokens.filter((tokenSpec) => tokenSpec.id !== id));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.setState({
|
|
99
|
+
value: "",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
handleChangeText = (e: React.SyntheticEvent<HTMLInputElement>) => {
|
|
104
|
+
const { value } = e.currentTarget;
|
|
105
|
+
const { onChange } = this.props;
|
|
106
|
+
this.setState({
|
|
107
|
+
value,
|
|
108
|
+
});
|
|
109
|
+
onChange?.(e, value);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
113
|
+
const { onFocus } = this.props;
|
|
114
|
+
this.setState({
|
|
115
|
+
hasFocus: true,
|
|
116
|
+
});
|
|
117
|
+
onFocus?.(e);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
121
|
+
const { onBlur } = this.props;
|
|
122
|
+
if (onBlur) onBlur(e);
|
|
123
|
+
this.setState({
|
|
124
|
+
hasFocus: false,
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
129
|
+
this.props.onKeyUp?.(e, e.currentTarget.value);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
133
|
+
const { onKeyDown } = this.props;
|
|
134
|
+
const { key, currentTarget } = e;
|
|
135
|
+
const text = currentTarget.value;
|
|
136
|
+
if (onKeyDown) onKeyDown(e, text);
|
|
137
|
+
|
|
138
|
+
// keyPress event runs before change
|
|
139
|
+
// Prevent event from bubbling up and calling change, which can lead to comma in value
|
|
140
|
+
if (this.isDelimiter(key)) {
|
|
141
|
+
if (text) {
|
|
142
|
+
this.spawnNewTokens([text]);
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
}
|
|
145
|
+
} else if (key === "Backspace") {
|
|
146
|
+
if (text === "") {
|
|
147
|
+
this.deleteToken();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
|
153
|
+
const text = e.clipboardData.getData("text");
|
|
154
|
+
const { onPaste } = this.props;
|
|
155
|
+
if (onPaste) onPaste(e, text);
|
|
156
|
+
const subtexts = text.split(this.delimiterMatcher);
|
|
157
|
+
const texts = subtexts.filter((subtext) => subtext.length);
|
|
158
|
+
|
|
159
|
+
if (texts.length > 1) {
|
|
160
|
+
this.spawnNewTokens(texts);
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
handleClickToken = (
|
|
166
|
+
e: React.SyntheticEvent<HTMLButtonElement>,
|
|
167
|
+
token: TypeTokenSpec
|
|
168
|
+
) => {
|
|
169
|
+
const { onClickToken, disabled } = this.props;
|
|
170
|
+
if (onClickToken) onClickToken(e, token.id);
|
|
171
|
+
|
|
172
|
+
if (!disabled) {
|
|
173
|
+
this.deleteToken(token.id);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
renderToken(token: TypeTokenSpec): React.ReactNode {
|
|
178
|
+
const { iconName: defaultIconName, disabled } = this.props;
|
|
179
|
+
const activeId = this.state.activeToken;
|
|
180
|
+
const {
|
|
181
|
+
id,
|
|
182
|
+
iconName: tokenIconName,
|
|
183
|
+
iconProps = { "aria-hidden": true },
|
|
184
|
+
value,
|
|
185
|
+
valid,
|
|
186
|
+
} = token;
|
|
187
|
+
const iconName = tokenIconName || defaultIconName;
|
|
188
|
+
const isActive = activeId === id;
|
|
189
|
+
return (
|
|
190
|
+
<Token
|
|
191
|
+
id={id}
|
|
192
|
+
onClick={(e) => this.handleClickToken(e, token)}
|
|
193
|
+
valid={valid}
|
|
194
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
195
|
+
// @ts-ignore
|
|
196
|
+
active={isActive}
|
|
197
|
+
disabled={disabled}
|
|
198
|
+
>
|
|
199
|
+
<Box display="flex" alignItems="center">
|
|
200
|
+
{iconName && (
|
|
201
|
+
<Icon name={iconName} size="mini" pr={300} {...iconProps} />
|
|
202
|
+
)}
|
|
203
|
+
{value}
|
|
204
|
+
</Box>
|
|
205
|
+
</Token>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
renderTokens(tokens: TypeTokenSpec[]): React.ReactNode {
|
|
210
|
+
return tokens.map<React.ReactNode>((token) => (
|
|
211
|
+
<div key={token.id} className="TokenInput-token">
|
|
212
|
+
{this.renderToken(token)}
|
|
213
|
+
</div>
|
|
214
|
+
));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
override render() {
|
|
218
|
+
const {
|
|
219
|
+
autoFocus,
|
|
220
|
+
autocomplete,
|
|
221
|
+
disabled,
|
|
222
|
+
isInvalid,
|
|
223
|
+
hasWarning,
|
|
224
|
+
id,
|
|
225
|
+
name,
|
|
226
|
+
placeholder,
|
|
227
|
+
required,
|
|
228
|
+
elemBefore,
|
|
229
|
+
elemAfter,
|
|
230
|
+
maxLength,
|
|
231
|
+
ariaDescribedby,
|
|
232
|
+
ariaLabel,
|
|
233
|
+
innerRef,
|
|
234
|
+
// These functions are used in the class functions above, but need to be extracted in order for `rest` to be correct
|
|
235
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
236
|
+
value,
|
|
237
|
+
onAddToken,
|
|
238
|
+
onRemoveToken,
|
|
239
|
+
onChangeTokens,
|
|
240
|
+
onClickToken,
|
|
241
|
+
onBlur,
|
|
242
|
+
onChange,
|
|
243
|
+
onFocus,
|
|
244
|
+
onKeyDown,
|
|
245
|
+
onKeyUp,
|
|
246
|
+
onPaste,
|
|
247
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
248
|
+
inputProps = {},
|
|
249
|
+
qa = {},
|
|
250
|
+
tokens,
|
|
251
|
+
...rest
|
|
252
|
+
} = this.props;
|
|
253
|
+
const { state } = this;
|
|
254
|
+
return (
|
|
255
|
+
<Container
|
|
256
|
+
hasBeforeElement={!!elemBefore}
|
|
257
|
+
hasAfterElement={!!elemAfter}
|
|
258
|
+
disabled={disabled}
|
|
259
|
+
invalid={!!isInvalid}
|
|
260
|
+
warning={hasWarning}
|
|
261
|
+
focused={state.hasFocus}
|
|
262
|
+
{...rest}
|
|
263
|
+
>
|
|
264
|
+
{elemBefore && <Accessory before>{elemBefore}</Accessory>}
|
|
265
|
+
|
|
266
|
+
{tokens && this.renderTokens(tokens)}
|
|
267
|
+
|
|
268
|
+
<TokenScreenReaderStatus tokens={tokens} />
|
|
269
|
+
|
|
270
|
+
<input
|
|
271
|
+
aria-describedby={ariaDescribedby}
|
|
272
|
+
aria-invalid={!!isInvalid}
|
|
273
|
+
aria-label={ariaLabel}
|
|
274
|
+
autoFocus={autoFocus}
|
|
275
|
+
autoComplete={autocomplete}
|
|
276
|
+
disabled={disabled}
|
|
277
|
+
id={id}
|
|
278
|
+
name={name}
|
|
279
|
+
placeholder={placeholder}
|
|
280
|
+
type="text"
|
|
281
|
+
required={required}
|
|
282
|
+
value={state.value}
|
|
283
|
+
maxLength={maxLength}
|
|
284
|
+
onBlur={this.handleBlur}
|
|
285
|
+
onChange={this.handleChangeText}
|
|
286
|
+
onFocus={this.handleFocus}
|
|
287
|
+
onKeyDown={this.handleKeyDown}
|
|
288
|
+
onKeyUp={this.handleKeyUp}
|
|
289
|
+
onPaste={this.handlePaste}
|
|
290
|
+
ref={innerRef}
|
|
291
|
+
data-qa-input={name || ""}
|
|
292
|
+
data-qa-input-isdisabled={!!disabled}
|
|
293
|
+
data-qa-input-isrequired={!!required}
|
|
294
|
+
{...qa}
|
|
295
|
+
{...inputProps}
|
|
296
|
+
/>
|
|
297
|
+
|
|
298
|
+
{elemAfter && <Accessory after>{elemAfter}</Accessory>}
|
|
299
|
+
</Container>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type {
|
|
3
|
+
TypeStyledComponentsCommonProps,
|
|
4
|
+
TypeSystemCommonProps,
|
|
5
|
+
} from "@sproutsocial/seeds-react-system-props";
|
|
6
|
+
import type {
|
|
7
|
+
TypeIconProps,
|
|
8
|
+
TypeIconName,
|
|
9
|
+
} from "@sproutsocial/seeds-react-icon";
|
|
10
|
+
|
|
11
|
+
export interface TypeTokenSpec {
|
|
12
|
+
id: string;
|
|
13
|
+
iconName?: TypeIconName;
|
|
14
|
+
iconProps?: Omit<TypeIconProps, "name">;
|
|
15
|
+
value: string;
|
|
16
|
+
valid?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TypeBaseTokenInputProps {
|
|
20
|
+
/** ID of the form element, should match the "for" value of the associated label */
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
iconName?: TypeIconName;
|
|
24
|
+
|
|
25
|
+
/** Array of delimiter key names */
|
|
26
|
+
delimiters?: string[];
|
|
27
|
+
|
|
28
|
+
/** Current input text. Required when controlling the input text */
|
|
29
|
+
value?: string;
|
|
30
|
+
|
|
31
|
+
/** Current focus state. Required when controlling the focus via onFocus and onBlur */
|
|
32
|
+
hasFocus?: boolean;
|
|
33
|
+
|
|
34
|
+
/** Id of the currently selected token */
|
|
35
|
+
activeToken?: string;
|
|
36
|
+
|
|
37
|
+
/** Array of current tokens */
|
|
38
|
+
tokens?: TypeTokenSpec[];
|
|
39
|
+
|
|
40
|
+
/** Standard control of changing tokens. For fine-grain control use onAddToken and onRemoveToken, instead */
|
|
41
|
+
onChangeTokens?: (tokens: TypeTokenSpec[]) => void;
|
|
42
|
+
|
|
43
|
+
/** Fine-grained control of adding tokens. Use with onRemoveToken instead of onChangeTokens */
|
|
44
|
+
onAddToken?: (tokenSpec: TypeTokenSpec) => void;
|
|
45
|
+
|
|
46
|
+
/** Fine-grained control of removing tokens. Use with onAddToken instead of onChangeTokens */
|
|
47
|
+
onRemoveToken?: (tokenId: string) => void;
|
|
48
|
+
|
|
49
|
+
/** Controls clicking on a token. When absent, clicking a token removes itself */
|
|
50
|
+
onClickToken?: (
|
|
51
|
+
e: React.SyntheticEvent<HTMLButtonElement>,
|
|
52
|
+
tokenId: string
|
|
53
|
+
) => void;
|
|
54
|
+
|
|
55
|
+
/** Fine-grained control of the input text used to create tokens */
|
|
56
|
+
onChange?: (e: React.SyntheticEvent<HTMLInputElement>, value: string) => void;
|
|
57
|
+
|
|
58
|
+
/** Fine-grained control of pasted text */
|
|
59
|
+
onPaste?: (e: React.ClipboardEvent<HTMLInputElement>, value: string) => void;
|
|
60
|
+
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
|
61
|
+
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
|
|
62
|
+
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>, value: string) => void;
|
|
63
|
+
onKeyUp?: (e: React.KeyboardEvent<HTMLInputElement>, value: string) => void;
|
|
64
|
+
|
|
65
|
+
/** Attribute used to associate other elements that describe the Input, like an error */
|
|
66
|
+
ariaDescribedby?: string;
|
|
67
|
+
|
|
68
|
+
/** Label used to describe the input if not used with an accompanying visual label */
|
|
69
|
+
ariaLabel?: string;
|
|
70
|
+
|
|
71
|
+
/** Placeholder text for when value is undefined or empty */
|
|
72
|
+
placeholder?: string;
|
|
73
|
+
|
|
74
|
+
/** Will autofocus the element when mounted to the DOM */
|
|
75
|
+
autoFocus?: boolean;
|
|
76
|
+
|
|
77
|
+
/** HTML disabled attribute */
|
|
78
|
+
disabled?: boolean;
|
|
79
|
+
|
|
80
|
+
/** Whether or not the current contents of the input are invalid */
|
|
81
|
+
isInvalid?: boolean;
|
|
82
|
+
|
|
83
|
+
/** Whether or not the current contents of the input has any warnings */
|
|
84
|
+
hasWarning?: boolean;
|
|
85
|
+
|
|
86
|
+
/** HTML required attribute */
|
|
87
|
+
required?: boolean;
|
|
88
|
+
|
|
89
|
+
/** 16x16 element, such as an icon */
|
|
90
|
+
elemBefore?: React.ReactNode;
|
|
91
|
+
|
|
92
|
+
/** 16x16 element, such as an icon */
|
|
93
|
+
elemAfter?: React.ReactNode;
|
|
94
|
+
|
|
95
|
+
/** Max input text length */
|
|
96
|
+
maxLength?: number;
|
|
97
|
+
|
|
98
|
+
/** Max length of the token */
|
|
99
|
+
tokenMaxLength?: number;
|
|
100
|
+
|
|
101
|
+
/** Props to spread onto the underlying input element */
|
|
102
|
+
inputProps?: React.ComponentPropsWithoutRef<"input">;
|
|
103
|
+
|
|
104
|
+
/** Used to get a reference to the underlying element */
|
|
105
|
+
innerRef?: React.Ref<HTMLInputElement>;
|
|
106
|
+
qa?: object;
|
|
107
|
+
|
|
108
|
+
/** Browser autocomplete support */
|
|
109
|
+
autocomplete?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface TypeTokenInputProps
|
|
113
|
+
extends TypeBaseTokenInputProps,
|
|
114
|
+
TypeStyledComponentsCommonProps,
|
|
115
|
+
TypeSystemCommonProps,
|
|
116
|
+
Omit<
|
|
117
|
+
React.ComponentPropsWithoutRef<"div">,
|
|
118
|
+
keyof TypeBaseTokenInputProps | "color"
|
|
119
|
+
> {}
|