@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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { VisuallyHidden } from "@sproutsocial/seeds-react-visually-hidden";
|
|
4
|
+
import type { TypeTokenInputProps } from "./";
|
|
5
|
+
|
|
6
|
+
function usePrevious(value: TypeTokenInputProps["tokens"]) {
|
|
7
|
+
const ref = useRef<TypeTokenInputProps["tokens"]>();
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
ref.current = value;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
return ref.current;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const TokenScreenReaderStatus = ({
|
|
17
|
+
tokens,
|
|
18
|
+
}: {
|
|
19
|
+
tokens: TypeTokenInputProps["tokens"];
|
|
20
|
+
}) => {
|
|
21
|
+
const prevTokens = usePrevious(tokens);
|
|
22
|
+
const [tokenStatus, setTokenStatus] = useState("");
|
|
23
|
+
|
|
24
|
+
// TODO: Use callbacks so consumers can pass localized messaging to the screen reader
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (prevTokens && tokens) {
|
|
27
|
+
if (prevTokens.length > tokens.length) {
|
|
28
|
+
setTokenStatus(
|
|
29
|
+
`${
|
|
30
|
+
prevTokens.filter((item) => tokens.indexOf(item) === -1)[0]?.value
|
|
31
|
+
} has been removed`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (prevTokens.length < tokens.length) {
|
|
36
|
+
setTokenStatus(`${tokens[tokens.length - 1]?.value} has been added.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}, [prevTokens, tokens, tokenStatus]);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<VisuallyHidden as="div" role="status">
|
|
43
|
+
{tokenStatus}
|
|
44
|
+
</VisuallyHidden>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
/* eslint-disable testing-library/no-render-in-setup */
|
|
2
|
+
import React from "react";
|
|
3
|
+
import {
|
|
4
|
+
render,
|
|
5
|
+
screen,
|
|
6
|
+
type UserEvent,
|
|
7
|
+
} from "@sproutsocial/seeds-react-testing-library";
|
|
8
|
+
import TokenInput from "../TokenInput";
|
|
9
|
+
|
|
10
|
+
describe("When clicking on...", () => {
|
|
11
|
+
describe("...a token", () => {
|
|
12
|
+
test("the token should be removed", async () => {
|
|
13
|
+
const mockOnAdd = jest.fn();
|
|
14
|
+
const mockOnRemove = jest.fn();
|
|
15
|
+
const { user } = render(
|
|
16
|
+
<TokenInput
|
|
17
|
+
id="0"
|
|
18
|
+
name="token-input"
|
|
19
|
+
tokens={[
|
|
20
|
+
{
|
|
21
|
+
id: "0",
|
|
22
|
+
value: "you",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "1",
|
|
26
|
+
value: "are",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "2",
|
|
30
|
+
value: "beautiful",
|
|
31
|
+
},
|
|
32
|
+
]}
|
|
33
|
+
onAddToken={mockOnAdd}
|
|
34
|
+
onRemoveToken={mockOnRemove}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
const token = screen.getByText("are");
|
|
38
|
+
await user.click(token);
|
|
39
|
+
const firstArgs = mockOnRemove.mock.calls[0];
|
|
40
|
+
expect(mockOnRemove).toBeCalledTimes(1);
|
|
41
|
+
expect(firstArgs).toEqual(["1"]);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("When deleting...", () => {
|
|
47
|
+
describe("...in an empty input", () => {
|
|
48
|
+
it("should do nothing", async () => {
|
|
49
|
+
const mockCallback = jest.fn();
|
|
50
|
+
const { user } = render(
|
|
51
|
+
<TokenInput
|
|
52
|
+
id="0"
|
|
53
|
+
name="token-input"
|
|
54
|
+
placeholder="Enter text"
|
|
55
|
+
onRemoveToken={mockCallback}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
const input = screen.getByPlaceholderText("Enter text");
|
|
59
|
+
await user.type(input, "{backspace}");
|
|
60
|
+
expect(mockCallback).toHaveBeenCalledTimes(0);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("...in an input with text", () => {
|
|
65
|
+
it("should delete the last character of text", async () => {
|
|
66
|
+
const { user } = render(
|
|
67
|
+
<TokenInput id="0" name="token-input" placeholder="Enter text" />
|
|
68
|
+
);
|
|
69
|
+
const input = screen.getByPlaceholderText("Enter text");
|
|
70
|
+
await user.type(input, "Hello World");
|
|
71
|
+
await user.type(input, "{backspace}");
|
|
72
|
+
expect(input).toHaveValue("Hello Worl");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("...in an input with at least one token and no text", () => {
|
|
77
|
+
it("should delete the last token", async () => {
|
|
78
|
+
const mockCallback = jest.fn();
|
|
79
|
+
const { user } = render(
|
|
80
|
+
<TokenInput
|
|
81
|
+
id="0"
|
|
82
|
+
name="token-input"
|
|
83
|
+
tokens={[
|
|
84
|
+
{
|
|
85
|
+
id: "0",
|
|
86
|
+
value: "you",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "1",
|
|
90
|
+
value: "are",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "2",
|
|
94
|
+
value: "beautiful",
|
|
95
|
+
},
|
|
96
|
+
]}
|
|
97
|
+
placeholder="Enter text"
|
|
98
|
+
onRemoveToken={mockCallback}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
const input = screen.getByPlaceholderText("Enter text");
|
|
102
|
+
await user.type(input, "{backspace}");
|
|
103
|
+
expect(mockCallback).toHaveBeenCalledTimes(1);
|
|
104
|
+
const result = mockCallback.mock.calls;
|
|
105
|
+
expect(result[0][0]).toBe("2");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("...in an input with at least one token and text", () => {
|
|
110
|
+
it("should delete all the text and then the token", async () => {
|
|
111
|
+
const mockCallback = jest.fn();
|
|
112
|
+
const { user } = render(
|
|
113
|
+
<TokenInput
|
|
114
|
+
id="0"
|
|
115
|
+
name="token-input"
|
|
116
|
+
tokens={[
|
|
117
|
+
{
|
|
118
|
+
id: "0",
|
|
119
|
+
value: "you",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "1",
|
|
123
|
+
value: "are",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "2",
|
|
127
|
+
value: "beautiful",
|
|
128
|
+
},
|
|
129
|
+
]}
|
|
130
|
+
placeholder="Enter text"
|
|
131
|
+
onRemoveToken={mockCallback}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
const input = screen.getByPlaceholderText(
|
|
135
|
+
"Enter text"
|
|
136
|
+
) as HTMLInputElement;
|
|
137
|
+
await user.type(input, "william");
|
|
138
|
+
input.setSelectionRange(0, input.value.length);
|
|
139
|
+
await user.keyboard("{backspace}");
|
|
140
|
+
expect(input).toHaveValue("");
|
|
141
|
+
expect(mockCallback).toHaveBeenCalledTimes(0);
|
|
142
|
+
const result = mockCallback.mock.calls;
|
|
143
|
+
await user.keyboard("{backspace}");
|
|
144
|
+
expect(result[0][0]).toBe("2");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("When tabbing...", () => {
|
|
150
|
+
describe("...the tokens", () => {
|
|
151
|
+
it("should be focused", async () => {
|
|
152
|
+
const { user } = render(
|
|
153
|
+
<TokenInput
|
|
154
|
+
id="0"
|
|
155
|
+
placeholder="Please enter a value..."
|
|
156
|
+
name="token-input"
|
|
157
|
+
tokens={[
|
|
158
|
+
{
|
|
159
|
+
id: "0",
|
|
160
|
+
value: "you",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: "1",
|
|
164
|
+
value: "are",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "2",
|
|
168
|
+
value: "beautiful",
|
|
169
|
+
},
|
|
170
|
+
]}
|
|
171
|
+
/>
|
|
172
|
+
);
|
|
173
|
+
const input = screen.getByPlaceholderText("Please enter a value...");
|
|
174
|
+
await user.click(input);
|
|
175
|
+
await user.tab({
|
|
176
|
+
shift: true,
|
|
177
|
+
});
|
|
178
|
+
expect(screen.getByText("beautiful").closest("button")).toHaveFocus();
|
|
179
|
+
await user.tab({
|
|
180
|
+
shift: true,
|
|
181
|
+
});
|
|
182
|
+
expect(screen.getByText("are").closest("button")).toHaveFocus();
|
|
183
|
+
await user.tab();
|
|
184
|
+
expect(screen.getByText("beautiful").closest("button")).toHaveFocus();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("When inputting...", () => {
|
|
190
|
+
describe("...simple text into an empty input", () => {
|
|
191
|
+
it("should add characters to the input", async () => {
|
|
192
|
+
const { user } = render(
|
|
193
|
+
<TokenInput id="0" name="token-input" placeholder="Enter text" />
|
|
194
|
+
);
|
|
195
|
+
const input = screen.getByPlaceholderText("Enter text");
|
|
196
|
+
await user.type(input, "Hello world!");
|
|
197
|
+
expect(input).toHaveValue("Hello world!");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("...a delimiter into an empty input", () => {
|
|
202
|
+
it("should add a value, if it's printable", async () => {
|
|
203
|
+
const { user } = render(
|
|
204
|
+
<TokenInput
|
|
205
|
+
id="0"
|
|
206
|
+
name="token-input"
|
|
207
|
+
delimiters={[".", "Enter"]}
|
|
208
|
+
placeholder="Enter text"
|
|
209
|
+
/>
|
|
210
|
+
);
|
|
211
|
+
const input = screen.getByPlaceholderText("Enter text");
|
|
212
|
+
await user.type(input, ".");
|
|
213
|
+
expect(input).toHaveValue(".");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("...a delimiter into an input with a value", () => {
|
|
218
|
+
it("should add a token rather than a value", async () => {
|
|
219
|
+
const mockCallback = jest.fn();
|
|
220
|
+
const { user } = render(
|
|
221
|
+
<TokenInput
|
|
222
|
+
id="0"
|
|
223
|
+
name="token-input"
|
|
224
|
+
value=","
|
|
225
|
+
placeholder="Enter text"
|
|
226
|
+
onAddToken={mockCallback}
|
|
227
|
+
/>
|
|
228
|
+
);
|
|
229
|
+
const input = screen.getByPlaceholderText("Enter text");
|
|
230
|
+
await user.type(input, ",");
|
|
231
|
+
expect(mockCallback).toHaveBeenCalledTimes(1);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("When pasting...", () => {
|
|
237
|
+
let mockHandleAdd: jest.Mock<any, any, any>;
|
|
238
|
+
let mockHandleRemove:
|
|
239
|
+
| jest.Mock<any, any, any>
|
|
240
|
+
| ((tokenId: string) => void)
|
|
241
|
+
| undefined;
|
|
242
|
+
let mockHandleChange:
|
|
243
|
+
| jest.Mock<any, any, any>
|
|
244
|
+
| ((e: React.SyntheticEvent<HTMLInputElement>, value: string) => void)
|
|
245
|
+
| undefined;
|
|
246
|
+
let mockHandlePaste:
|
|
247
|
+
| jest.Mock<any, any, any>
|
|
248
|
+
| ((e: React.ClipboardEvent<HTMLInputElement>, value: string) => void)
|
|
249
|
+
| undefined;
|
|
250
|
+
let tokenInput;
|
|
251
|
+
let input: HTMLElement;
|
|
252
|
+
let user: UserEvent;
|
|
253
|
+
|
|
254
|
+
describe("...with no tokens...", () => {
|
|
255
|
+
beforeEach(() => {
|
|
256
|
+
// TokenInput is a stateless component, so the value managed in state is cleared in the wrapper.
|
|
257
|
+
mockHandleAdd = jest.fn();
|
|
258
|
+
mockHandleRemove = jest.fn();
|
|
259
|
+
mockHandleChange = jest.fn();
|
|
260
|
+
mockHandlePaste = jest.fn();
|
|
261
|
+
tokenInput = (
|
|
262
|
+
<TokenInput
|
|
263
|
+
id="0"
|
|
264
|
+
name="token-input"
|
|
265
|
+
placeholder="Enter text"
|
|
266
|
+
onAddToken={mockHandleAdd}
|
|
267
|
+
onRemoveToken={mockHandleRemove}
|
|
268
|
+
onChange={mockHandleChange}
|
|
269
|
+
onPaste={mockHandlePaste}
|
|
270
|
+
/>
|
|
271
|
+
);
|
|
272
|
+
const { user: userEvents } = render(tokenInput);
|
|
273
|
+
user = userEvents;
|
|
274
|
+
input = screen.getByPlaceholderText("Enter text") as HTMLInputElement;
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe("...simple text into an empty input", () => {
|
|
278
|
+
it("should insert the simple text", async () => {
|
|
279
|
+
expect(input).toHaveValue("");
|
|
280
|
+
await user.click(input);
|
|
281
|
+
await user.paste("Hello world");
|
|
282
|
+
expect(mockHandleAdd).toHaveBeenCalledTimes(0);
|
|
283
|
+
expect(mockHandleRemove).toHaveBeenCalledTimes(0);
|
|
284
|
+
expect(mockHandleChange).toHaveBeenCalledTimes(1);
|
|
285
|
+
if (jest.isMockFunction(mockHandleChange)) {
|
|
286
|
+
expect(mockHandleChange.mock.calls[0][1]).toBe("Hello world");
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("...a delimiter into an empty input", () => {
|
|
292
|
+
it("shouldn't add anything... with a ','", async () => {
|
|
293
|
+
await user.click(input);
|
|
294
|
+
await user.paste(",");
|
|
295
|
+
expect(mockHandleAdd).toHaveBeenCalledTimes(0);
|
|
296
|
+
expect(mockHandleRemove).toHaveBeenCalledTimes(0);
|
|
297
|
+
});
|
|
298
|
+
it("shouldn't add anything... with an 'new-line'", async () => {
|
|
299
|
+
await user.click(input);
|
|
300
|
+
await user.paste("\n");
|
|
301
|
+
expect(mockHandleAdd).toHaveBeenCalledTimes(0);
|
|
302
|
+
expect(mockHandleRemove).toHaveBeenCalledTimes(0);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe("...delimited text into an empty input", () => {
|
|
307
|
+
it("should add tokens", async () => {
|
|
308
|
+
expect(input).toHaveValue("");
|
|
309
|
+
await user.click(input);
|
|
310
|
+
await user.paste("one, two, three");
|
|
311
|
+
expect(mockHandleRemove).toHaveBeenCalledTimes(0);
|
|
312
|
+
expect(mockHandleAdd).toHaveBeenCalledTimes(3);
|
|
313
|
+
expect(mockHandlePaste).toHaveBeenCalledTimes(1);
|
|
314
|
+
if (jest.isMockFunction(mockHandlePaste)) {
|
|
315
|
+
expect(mockHandlePaste.mock.calls[0][1]).toBe("one, two, three");
|
|
316
|
+
}
|
|
317
|
+
// onChange seems to not be called with delimited text value, possibly because of the preventDefault used
|
|
318
|
+
// expect(mockHandleChange).toHaveBeenCalledTimes(1);
|
|
319
|
+
// expect(mockHandleChange.mock.calls[0][1]).toBe("one, two, three");
|
|
320
|
+
if (jest.isMockFunction(mockHandleAdd)) {
|
|
321
|
+
const tokenSpec1 = (mockHandleAdd as jest.Mock).mock.calls[0][0];
|
|
322
|
+
const tokenSpec2 = mockHandleAdd.mock.calls[1][0];
|
|
323
|
+
const tokenSpec3 = mockHandleAdd.mock.calls[2][0];
|
|
324
|
+
expect(tokenSpec1).toEqual(
|
|
325
|
+
expect.objectContaining({
|
|
326
|
+
id: expect.any(String),
|
|
327
|
+
value: "one",
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
expect(tokenSpec2).toEqual(
|
|
331
|
+
expect.objectContaining({
|
|
332
|
+
id: expect.any(String),
|
|
333
|
+
value: "two",
|
|
334
|
+
})
|
|
335
|
+
);
|
|
336
|
+
expect(tokenSpec3).toEqual(
|
|
337
|
+
expect.objectContaining({
|
|
338
|
+
id: expect.any(String),
|
|
339
|
+
value: "three",
|
|
340
|
+
})
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe("...simple text into an input with text", () => {
|
|
347
|
+
it("should simply paste the text", async () => {
|
|
348
|
+
const preText = "4321!";
|
|
349
|
+
const pasteText = "hello world";
|
|
350
|
+
await user.type(input, preText);
|
|
351
|
+
await user.click(input);
|
|
352
|
+
await user.paste(pasteText);
|
|
353
|
+
expect(mockHandleAdd).toHaveBeenCalledTimes(0);
|
|
354
|
+
expect(mockHandleRemove).toHaveBeenCalledTimes(0);
|
|
355
|
+
expect(mockHandleChange).toHaveBeenCalledTimes(6);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe("...with text...", () => {
|
|
361
|
+
beforeEach(() => {
|
|
362
|
+
// TokenInput is a stateless component, so the value managed in state is cleared in the wrapper.
|
|
363
|
+
mockHandleAdd = jest.fn();
|
|
364
|
+
mockHandleRemove = jest.fn();
|
|
365
|
+
tokenInput = (
|
|
366
|
+
<TokenInput
|
|
367
|
+
id="0"
|
|
368
|
+
name="token-input"
|
|
369
|
+
placeholder="Enter text"
|
|
370
|
+
value="Pre Text!"
|
|
371
|
+
onAddToken={mockHandleAdd}
|
|
372
|
+
onRemoveToken={mockHandleRemove}
|
|
373
|
+
/>
|
|
374
|
+
);
|
|
375
|
+
const { user: userEvents } = render(tokenInput);
|
|
376
|
+
user = userEvents;
|
|
377
|
+
input = screen.getByPlaceholderText("Enter text");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe("...delimited text into an input with text", () => {
|
|
381
|
+
it("should make new tokens from the pasted text", async () => {
|
|
382
|
+
await user.click(input);
|
|
383
|
+
await user.paste("this, that");
|
|
384
|
+
expect(mockHandleRemove).toHaveBeenCalledTimes(0);
|
|
385
|
+
expect(mockHandleAdd).toHaveBeenCalledTimes(2);
|
|
386
|
+
const tokenSpec1 = mockHandleAdd.mock.calls[0][0];
|
|
387
|
+
const tokenSpec2 = mockHandleAdd.mock.calls[1][0];
|
|
388
|
+
expect(tokenSpec1).toEqual(
|
|
389
|
+
expect.objectContaining({
|
|
390
|
+
id: expect.any(String),
|
|
391
|
+
value: "this",
|
|
392
|
+
})
|
|
393
|
+
);
|
|
394
|
+
expect(tokenSpec2).toEqual(
|
|
395
|
+
expect.objectContaining({
|
|
396
|
+
id: expect.any(String),
|
|
397
|
+
value: "that",
|
|
398
|
+
})
|
|
399
|
+
);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
describe("...with tokens...", () => {
|
|
405
|
+
beforeEach(() => {
|
|
406
|
+
// TokenInput is a stateless component, so the value managed in state is cleared in the wrapper.
|
|
407
|
+
mockHandleAdd = jest.fn();
|
|
408
|
+
mockHandleRemove = jest.fn();
|
|
409
|
+
tokenInput = (
|
|
410
|
+
<TokenInput
|
|
411
|
+
id="0"
|
|
412
|
+
name="token-input"
|
|
413
|
+
placeholder="Enter text"
|
|
414
|
+
tokens={[
|
|
415
|
+
{
|
|
416
|
+
id: "1",
|
|
417
|
+
value: "one",
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
id: "2",
|
|
421
|
+
value: "two",
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: "3",
|
|
425
|
+
value: "three",
|
|
426
|
+
},
|
|
427
|
+
]}
|
|
428
|
+
onAddToken={mockHandleAdd}
|
|
429
|
+
onRemoveToken={mockHandleRemove}
|
|
430
|
+
/>
|
|
431
|
+
);
|
|
432
|
+
const { user: userEvents } = render(tokenInput);
|
|
433
|
+
user = userEvents;
|
|
434
|
+
input = screen.getByPlaceholderText("Enter text");
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
describe("...simple text after one or more tokens", () => {
|
|
438
|
+
it("simply paste the text", async () => {
|
|
439
|
+
await user.click(input);
|
|
440
|
+
await user.paste("Hello world");
|
|
441
|
+
expect(mockHandleRemove).toHaveBeenCalledTimes(0);
|
|
442
|
+
expect(mockHandleAdd).toHaveBeenCalledTimes(0); // expect(mockHandleChange.mock.calls[0][1]).toBe("Hello world");
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
describe("...delimited text after one or more tokens", () => {
|
|
447
|
+
it("should add multiple new tokens", async () => {
|
|
448
|
+
await user.click(input);
|
|
449
|
+
await user.paste("this, that");
|
|
450
|
+
expect(mockHandleRemove).toHaveBeenCalledTimes(0);
|
|
451
|
+
expect(mockHandleAdd).toHaveBeenCalledTimes(2);
|
|
452
|
+
// expect(mockHandleChange.mock.calls[0][1]).toBe("this, that");
|
|
453
|
+
const tokenSpec1 = mockHandleAdd.mock.calls[0][0];
|
|
454
|
+
const tokenSpec2 = mockHandleAdd.mock.calls[1][0];
|
|
455
|
+
expect(tokenSpec1).toEqual(
|
|
456
|
+
expect.objectContaining({
|
|
457
|
+
id: expect.any(String),
|
|
458
|
+
value: "this",
|
|
459
|
+
})
|
|
460
|
+
);
|
|
461
|
+
expect(tokenSpec2).toEqual(
|
|
462
|
+
expect.objectContaining({
|
|
463
|
+
id: expect.any(String),
|
|
464
|
+
value: "that",
|
|
465
|
+
})
|
|
466
|
+
);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
describe("When rendering...", () => {
|
|
473
|
+
it("should render disabled status correctly", () => {
|
|
474
|
+
render(
|
|
475
|
+
<TokenInput
|
|
476
|
+
id="0"
|
|
477
|
+
placeholder="Please enter a value..."
|
|
478
|
+
name="token-input"
|
|
479
|
+
tokens={[
|
|
480
|
+
{
|
|
481
|
+
id: "0",
|
|
482
|
+
value: "you",
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
id: "1",
|
|
486
|
+
value: "are",
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
id: "2",
|
|
490
|
+
value: "beautiful",
|
|
491
|
+
},
|
|
492
|
+
]}
|
|
493
|
+
disabled
|
|
494
|
+
/>
|
|
495
|
+
);
|
|
496
|
+
expect(
|
|
497
|
+
screen.getByDataQaLabel({
|
|
498
|
+
"input-isdisabled": true,
|
|
499
|
+
})
|
|
500
|
+
).toBeTruthy();
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("should render before and after elements", () => {
|
|
504
|
+
render(
|
|
505
|
+
<TokenInput
|
|
506
|
+
elemAfter={<p>After</p>}
|
|
507
|
+
elemBefore={<p>Before</p>}
|
|
508
|
+
id="name"
|
|
509
|
+
name="name"
|
|
510
|
+
value="User"
|
|
511
|
+
/>
|
|
512
|
+
);
|
|
513
|
+
expect(screen.getByText("Before")).toBeInTheDocument();
|
|
514
|
+
expect(screen.getByText("After")).toBeInTheDocument();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
describe("...the isInvalid prop", () => {
|
|
518
|
+
it.each([true, "foobar", 1])(
|
|
519
|
+
"should correctly set aria-invalid to true for truthy values: %p",
|
|
520
|
+
(truthyValue) => {
|
|
521
|
+
render(
|
|
522
|
+
<TokenInput id="name" name="name" isInvalid={Boolean(truthyValue)} />
|
|
523
|
+
);
|
|
524
|
+
expect(
|
|
525
|
+
screen.getByDataQaLabel({
|
|
526
|
+
input: "name",
|
|
527
|
+
})
|
|
528
|
+
).toHaveAttribute("aria-invalid", "true");
|
|
529
|
+
}
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
it.each([false, null, undefined, 0])(
|
|
533
|
+
"should correctly set aria-invalid to false for falsy values: %p",
|
|
534
|
+
(truthyValue) => {
|
|
535
|
+
render(
|
|
536
|
+
<TokenInput id="name" name="name" isInvalid={Boolean(truthyValue)} />
|
|
537
|
+
);
|
|
538
|
+
expect(
|
|
539
|
+
screen.getByDataQaLabel({
|
|
540
|
+
input: "name",
|
|
541
|
+
})
|
|
542
|
+
).toHaveAttribute("aria-invalid", "false");
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
describe("...tokens", () => {
|
|
548
|
+
it("should render tokens in container", () => {
|
|
549
|
+
render(
|
|
550
|
+
<TokenInput
|
|
551
|
+
id="name"
|
|
552
|
+
name="name"
|
|
553
|
+
value="User"
|
|
554
|
+
tokens={[
|
|
555
|
+
{
|
|
556
|
+
id: "0",
|
|
557
|
+
value: "han",
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
id: "1",
|
|
561
|
+
value: "solo",
|
|
562
|
+
},
|
|
563
|
+
]}
|
|
564
|
+
/>
|
|
565
|
+
);
|
|
566
|
+
expect(screen.getByText("han")).toBeInTheDocument();
|
|
567
|
+
expect(screen.getByText("solo")).toBeInTheDocument();
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("should render tokens with icons", () => {
|
|
571
|
+
render(
|
|
572
|
+
<TokenInput
|
|
573
|
+
id="name"
|
|
574
|
+
iconName="lock-outline"
|
|
575
|
+
name="name"
|
|
576
|
+
value="User"
|
|
577
|
+
tokens={[
|
|
578
|
+
{
|
|
579
|
+
id: "0",
|
|
580
|
+
value: "han",
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
id: "1",
|
|
584
|
+
value: "solo",
|
|
585
|
+
},
|
|
586
|
+
]}
|
|
587
|
+
/>
|
|
588
|
+
);
|
|
589
|
+
expect(
|
|
590
|
+
screen.getAllByDataQaLabel({
|
|
591
|
+
icon: "lock-outline",
|
|
592
|
+
}).length
|
|
593
|
+
).toBe(2);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it("should render tokens with individual icons", () => {
|
|
597
|
+
render(
|
|
598
|
+
<TokenInput
|
|
599
|
+
id="name"
|
|
600
|
+
name="name"
|
|
601
|
+
value="User"
|
|
602
|
+
tokens={[
|
|
603
|
+
{
|
|
604
|
+
id: "0",
|
|
605
|
+
value: "han",
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
id: "1",
|
|
609
|
+
iconName: "sun-outline",
|
|
610
|
+
value: "solo",
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
id: "2",
|
|
614
|
+
iconName: "sun-outline",
|
|
615
|
+
value: "darth",
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
id: "3",
|
|
619
|
+
value: "vader",
|
|
620
|
+
},
|
|
621
|
+
]}
|
|
622
|
+
/>
|
|
623
|
+
);
|
|
624
|
+
expect(
|
|
625
|
+
screen.getAllByDataQaLabel({
|
|
626
|
+
icon: "sun-outline",
|
|
627
|
+
}).length
|
|
628
|
+
).toBe(2);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("should render tokens with individual icons and fallback icon", () => {
|
|
632
|
+
render(
|
|
633
|
+
<TokenInput
|
|
634
|
+
id="name"
|
|
635
|
+
iconName="lock-outline"
|
|
636
|
+
name="name"
|
|
637
|
+
value="User"
|
|
638
|
+
tokens={[
|
|
639
|
+
{
|
|
640
|
+
id: "0",
|
|
641
|
+
value: "han",
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
id: "1",
|
|
645
|
+
iconName: "sun-outline",
|
|
646
|
+
value: "solo",
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
id: "2",
|
|
650
|
+
iconName: "sun-outline",
|
|
651
|
+
value: "darth",
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
id: "3",
|
|
655
|
+
value: "vader",
|
|
656
|
+
},
|
|
657
|
+
]}
|
|
658
|
+
/>
|
|
659
|
+
);
|
|
660
|
+
expect(
|
|
661
|
+
screen.getAllByDataQaLabel({
|
|
662
|
+
icon: "lock-outline",
|
|
663
|
+
}).length
|
|
664
|
+
).toBe(2);
|
|
665
|
+
expect(
|
|
666
|
+
screen.getAllByDataQaLabel({
|
|
667
|
+
icon: "sun-outline",
|
|
668
|
+
}).length
|
|
669
|
+
).toBe(2);
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
});
|