@pie-lib/text-select 2.1.1-next.0 → 2.2.0-next.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.
Files changed (38) hide show
  1. package/lib/index.js +0 -7
  2. package/lib/index.js.map +1 -1
  3. package/lib/legend.js +0 -12
  4. package/lib/legend.js.map +1 -1
  5. package/lib/text-select.js +0 -14
  6. package/lib/text-select.js.map +1 -1
  7. package/lib/token-select/index.js +3 -24
  8. package/lib/token-select/index.js.map +1 -1
  9. package/lib/token-select/token.js +0 -17
  10. package/lib/token-select/token.js.map +1 -1
  11. package/lib/tokenizer/builder.js +3 -14
  12. package/lib/tokenizer/builder.js.map +1 -1
  13. package/lib/tokenizer/controls.js +0 -2
  14. package/lib/tokenizer/controls.js.map +1 -1
  15. package/lib/tokenizer/index.js +4 -19
  16. package/lib/tokenizer/index.js.map +1 -1
  17. package/lib/tokenizer/selection-utils.js +0 -4
  18. package/lib/tokenizer/selection-utils.js.map +1 -1
  19. package/lib/tokenizer/token-text.js +0 -11
  20. package/lib/tokenizer/token-text.js.map +1 -1
  21. package/lib/utils.js +0 -9
  22. package/lib/utils.js.map +1 -1
  23. package/package.json +11 -8
  24. package/src/__tests__/legend.test.jsx +211 -0
  25. package/src/token-select/__tests__/index.test.jsx +264 -2
  26. package/src/token-select/__tests__/token.test.jsx +207 -1
  27. package/src/token-select/index.jsx +1 -2
  28. package/src/token-select/token.jsx +1 -5
  29. package/src/tokenizer/__tests__/builder.test.js +1 -1
  30. package/src/tokenizer/__tests__/controls.test.jsx +1 -1
  31. package/src/tokenizer/__tests__/index.test.jsx +289 -7
  32. package/src/tokenizer/__tests__/selection-utils.test.js +122 -3
  33. package/src/tokenizer/__tests__/token-text.test.jsx +285 -9
  34. package/src/tokenizer/builder.js +2 -3
  35. package/src/tokenizer/controls.jsx +3 -18
  36. package/src/tokenizer/index.jsx +2 -4
  37. package/src/tokenizer/token-text.jsx +2 -2
  38. package/NEXT.CHANGELOG.json +0 -1
@@ -1,6 +1,6 @@
1
- import { TokenSelect } from '../index';
2
1
  import React from 'react';
3
- import { render } from '@testing-library/react';
2
+ import { fireEvent, render } from '@testing-library/react';
3
+ import { TokenSelect } from '../index';
4
4
 
5
5
  describe('token-select', () => {
6
6
  const defaultProps = {
@@ -115,6 +115,268 @@ describe('token-select', () => {
115
115
  const { container } = render(<TokenSelect {...defaultProps} maxNoOfSelections={5} />);
116
116
  expect(container.firstChild).toBeInTheDocument();
117
117
  });
118
+
119
+ it('renders with highlightChoices enabled', () => {
120
+ const { container } = render(<TokenSelect {...defaultProps} highlightChoices={true} />);
121
+ expect(container.firstChild).toBeInTheDocument();
122
+ });
123
+
124
+ it('strips HTML tags from token text', () => {
125
+ const tokens = [
126
+ {
127
+ text: '<b>bold text</b>',
128
+ start: 0,
129
+ end: 16,
130
+ predefined: true,
131
+ selectable: true,
132
+ selected: false,
133
+ },
134
+ ];
135
+ const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} />);
136
+ expect(container.textContent).toContain('bold text');
137
+ expect(container.innerHTML).not.toContain('<b>');
138
+ });
139
+
140
+ it('renders with custom className', () => {
141
+ const { container } = render(<TokenSelect {...defaultProps} className="custom-token-select" />);
142
+ expect(container.firstChild).toHaveClass('custom-token-select');
143
+ });
144
+
145
+ it('renders tokens with correct and incorrect states', () => {
146
+ const tokens = [
147
+ {
148
+ text: 'correct',
149
+ start: 0,
150
+ end: 7,
151
+ predefined: true,
152
+ selectable: true,
153
+ selected: true,
154
+ correct: true,
155
+ },
156
+ {
157
+ text: 'incorrect',
158
+ start: 8,
159
+ end: 17,
160
+ predefined: true,
161
+ selectable: true,
162
+ selected: true,
163
+ correct: false,
164
+ },
165
+ ];
166
+ const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} />);
167
+ expect(container.firstChild).toBeInTheDocument();
168
+ const checkIcon = container.querySelector('svg[data-testid="CheckIcon"]');
169
+ const closeIcon = container.querySelector('svg[data-testid="CloseIcon"]');
170
+ expect(checkIcon).toBeInTheDocument();
171
+ expect(closeIcon).toBeInTheDocument();
172
+ });
173
+
174
+ it('renders tokens with isMissing state', () => {
175
+ const tokens = [
176
+ {
177
+ text: 'missing answer',
178
+ start: 0,
179
+ end: 14,
180
+ predefined: true,
181
+ selectable: true,
182
+ selected: false,
183
+ isMissing: true,
184
+ },
185
+ ];
186
+ const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} />);
187
+ expect(container.firstChild).toBeInTheDocument();
188
+ const closeIcon = container.querySelector('svg[data-testid="CloseIcon"]');
189
+ expect(closeIcon).toBeInTheDocument();
190
+ });
191
+ });
192
+
193
+ describe('token interaction', () => {
194
+ it('calls onChange when clicking a selectable token', () => {
195
+ const onChange = jest.fn();
196
+ const tokens = [
197
+ {
198
+ text: 'foo bar',
199
+ start: 0,
200
+ end: 7,
201
+ predefined: true,
202
+ selectable: true,
203
+ selected: false,
204
+ },
205
+ ];
206
+ const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} onChange={onChange} />);
207
+
208
+ const tokenElement = container.querySelector('.tokenRootClass');
209
+ if (tokenElement) {
210
+ fireEvent.click(tokenElement);
211
+ expect(onChange).toHaveBeenCalled();
212
+ const updatedTokens = onChange.mock.calls[0][0];
213
+ expect(updatedTokens[0].selected).toBe(true);
214
+ }
215
+ });
216
+
217
+ it('handles maxNoOfSelections of 1 by deselecting previous token', () => {
218
+ const onChange = jest.fn();
219
+ const tokens = [
220
+ {
221
+ text: 'first',
222
+ start: 0,
223
+ end: 5,
224
+ predefined: true,
225
+ selectable: true,
226
+ selected: true,
227
+ },
228
+ {
229
+ text: 'second',
230
+ start: 6,
231
+ end: 12,
232
+ predefined: true,
233
+ selectable: true,
234
+ selected: false,
235
+ },
236
+ ];
237
+ const { container } = render(
238
+ <TokenSelect {...defaultProps} tokens={tokens} onChange={onChange} maxNoOfSelections={1} />,
239
+ );
240
+
241
+ const tokenElements = container.querySelectorAll('.tokenRootClass');
242
+ if (tokenElements.length > 1) {
243
+ fireEvent.click(tokenElements[1]);
244
+ expect(onChange).toHaveBeenCalled();
245
+ const updatedTokens = onChange.mock.calls[0][0];
246
+ expect(updatedTokens[0].selected).toBe(false);
247
+ expect(updatedTokens[1].selected).toBe(true);
248
+ }
249
+ });
250
+
251
+ it('prevents selecting more tokens when maxNoOfSelections is reached', () => {
252
+ const onChange = jest.fn();
253
+ const tokens = [
254
+ {
255
+ text: 'first',
256
+ start: 0,
257
+ end: 5,
258
+ predefined: true,
259
+ selectable: true,
260
+ selected: true,
261
+ },
262
+ {
263
+ text: 'second',
264
+ start: 6,
265
+ end: 12,
266
+ predefined: true,
267
+ selectable: true,
268
+ selected: true,
269
+ },
270
+ {
271
+ text: 'third',
272
+ start: 13,
273
+ end: 18,
274
+ predefined: true,
275
+ selectable: true,
276
+ selected: false,
277
+ },
278
+ ];
279
+ const { container } = render(
280
+ <TokenSelect {...defaultProps} tokens={tokens} onChange={onChange} maxNoOfSelections={2} />,
281
+ );
282
+
283
+ const tokenElements = container.querySelectorAll('.tokenRootClass');
284
+ if (tokenElements.length > 2) {
285
+ fireEvent.click(tokenElements[2]);
286
+ // onChange should not be called because max is reached
287
+ expect(onChange).not.toHaveBeenCalled();
288
+ }
289
+ });
290
+
291
+ it('does not toggle token when in animationsDisabled mode', () => {
292
+ const onChange = jest.fn();
293
+ const tokens = [
294
+ {
295
+ text: 'foo bar',
296
+ start: 0,
297
+ end: 7,
298
+ predefined: true,
299
+ selectable: true,
300
+ selected: false,
301
+ },
302
+ ];
303
+ const { container } = render(
304
+ <TokenSelect {...defaultProps} tokens={tokens} onChange={onChange} animationsDisabled={true} />,
305
+ );
306
+
307
+ const tokenElement = container.querySelector('.tokenRootClass');
308
+ if (tokenElement) {
309
+ fireEvent.click(tokenElement);
310
+ expect(onChange).not.toHaveBeenCalled();
311
+ }
312
+ });
313
+
314
+ it('does not toggle token when correct is defined', () => {
315
+ const onChange = jest.fn();
316
+ const tokens = [
317
+ {
318
+ text: 'foo bar',
319
+ start: 0,
320
+ end: 7,
321
+ predefined: true,
322
+ selectable: true,
323
+ selected: false,
324
+ correct: true,
325
+ },
326
+ ];
327
+ const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} onChange={onChange} />);
328
+
329
+ const tokenElement = container.querySelector('.tokenRootClass');
330
+ if (tokenElement) {
331
+ fireEvent.click(tokenElement);
332
+ expect(onChange).not.toHaveBeenCalled();
333
+ }
334
+ });
335
+
336
+ it('does not toggle token when isMissing is true', () => {
337
+ const onChange = jest.fn();
338
+ const tokens = [
339
+ {
340
+ text: 'foo bar',
341
+ start: 0,
342
+ end: 7,
343
+ predefined: true,
344
+ selectable: true,
345
+ selected: false,
346
+ isMissing: true,
347
+ },
348
+ ];
349
+ const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} onChange={onChange} />);
350
+
351
+ const tokenElement = container.querySelector('.tokenRootClass');
352
+ if (tokenElement) {
353
+ fireEvent.click(tokenElement);
354
+ expect(onChange).not.toHaveBeenCalled();
355
+ }
356
+ });
357
+
358
+ it('allows toggling token off when selected', () => {
359
+ const onChange = jest.fn();
360
+ const tokens = [
361
+ {
362
+ text: 'foo bar',
363
+ start: 0,
364
+ end: 7,
365
+ predefined: true,
366
+ selectable: true,
367
+ selected: true,
368
+ },
369
+ ];
370
+ const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} onChange={onChange} />);
371
+
372
+ const tokenElement = container.querySelector('.tokenRootClass');
373
+ if (tokenElement) {
374
+ fireEvent.click(tokenElement);
375
+ expect(onChange).toHaveBeenCalled();
376
+ const updatedTokens = onChange.mock.calls[0][0];
377
+ expect(updatedTokens[0].selected).toBe(false);
378
+ }
379
+ });
118
380
  });
119
381
 
120
382
  // Note: Tests for internal methods (selectedCount, canSelectMore, toggleToken) are
@@ -1,6 +1,6 @@
1
- import { Token } from '../token';
2
1
  import React from 'react';
3
2
  import { render } from '@testing-library/react';
3
+ import { Token } from '../token';
4
4
 
5
5
  describe('token', () => {
6
6
  const defaultProps = {
@@ -11,6 +11,10 @@ describe('token', () => {
11
11
  text: 'foo bar',
12
12
  };
13
13
 
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ });
17
+
14
18
  describe('rendering', () => {
15
19
  it('renders with text', () => {
16
20
  const { container } = render(<Token {...defaultProps} />);
@@ -26,5 +30,207 @@ describe('token', () => {
26
30
  const { container } = render(<Token {...defaultProps} text="line1\nline2\nline3" />);
27
31
  expect(container.firstChild).toBeInTheDocument();
28
32
  });
33
+
34
+ it('renders with custom className', () => {
35
+ const { container } = render(<Token {...defaultProps} className="custom-class" />);
36
+ expect(container.querySelector('.custom-class')).toBeInTheDocument();
37
+ });
38
+
39
+ it('renders with data-indexkey attribute', () => {
40
+ const { container } = render(<Token {...defaultProps} index={5} />);
41
+ const token = container.querySelector('[data-indexkey]');
42
+ expect(token).toHaveAttribute('data-indexkey', '5');
43
+ });
44
+
45
+ it('renders empty text gracefully', () => {
46
+ const { container } = render(<Token {...defaultProps} text="" />);
47
+ expect(container.firstChild).toBeInTheDocument();
48
+ });
49
+
50
+ it('renders null text gracefully', () => {
51
+ const { container } = render(<Token {...defaultProps} text={null} />);
52
+ expect(container.firstChild).toBeInTheDocument();
53
+ });
54
+ });
55
+
56
+ describe('selectable state', () => {
57
+ it('applies selectable class when selectable is true', () => {
58
+ const { container } = render(<Token {...defaultProps} selectable={true} />);
59
+ expect(container.querySelector('.selectable')).toBeInTheDocument();
60
+ });
61
+
62
+ it('does not apply selectable class when selectable is false', () => {
63
+ const { container } = render(<Token {...defaultProps} selectable={false} />);
64
+ expect(container.querySelector('.selectable')).not.toBeInTheDocument();
65
+ });
66
+
67
+ it('does not apply selectable class when disabled', () => {
68
+ const { container } = render(<Token {...defaultProps} selectable={true} disabled={true} />);
69
+ expect(container.querySelector('.selectable')).not.toBeInTheDocument();
70
+ });
71
+ });
72
+
73
+ describe('selected state', () => {
74
+ it('applies selected class when selected is true', () => {
75
+ const { container } = render(<Token {...defaultProps} selected={true} />);
76
+ expect(container.querySelector('.selected')).toBeInTheDocument();
77
+ });
78
+
79
+ it('does not apply selected class when selected is false', () => {
80
+ const { container } = render(<Token {...defaultProps} selected={false} />);
81
+ expect(container.querySelector('.selected')).not.toBeInTheDocument();
82
+ });
83
+
84
+ it('applies disabledBlack class when selected and disabled without correct prop', () => {
85
+ const { container } = render(<Token {...defaultProps} selected={true} disabled={true} />);
86
+ expect(container.querySelector('.disabledBlack')).toBeInTheDocument();
87
+ });
88
+ });
89
+
90
+ describe('disabled state', () => {
91
+ it('applies disabled class when disabled is true', () => {
92
+ const { container } = render(<Token {...defaultProps} disabled={true} />);
93
+ expect(container.querySelector('.disabled')).toBeInTheDocument();
94
+ });
95
+
96
+ it('does not apply disabled class when disabled is false', () => {
97
+ const { container } = render(<Token {...defaultProps} disabled={false} />);
98
+ expect(container.querySelector('.disabled')).not.toBeInTheDocument();
99
+ });
100
+ });
101
+
102
+ describe('highlight state', () => {
103
+ it('applies highlight class when highlight is true and selectable', () => {
104
+ const { container } = render(<Token {...defaultProps} highlight={true} selectable={true} />);
105
+ expect(container.querySelector('.highlight')).toBeInTheDocument();
106
+ });
107
+
108
+ it('does not apply highlight class when not selectable', () => {
109
+ const { container } = render(<Token {...defaultProps} highlight={true} selectable={false} />);
110
+ expect(container.querySelector('.highlight')).not.toBeInTheDocument();
111
+ });
112
+
113
+ it('does not apply highlight class when disabled', () => {
114
+ const { container } = render(<Token {...defaultProps} highlight={true} selectable={true} disabled={true} />);
115
+ expect(container.querySelector('.highlight')).not.toBeInTheDocument();
116
+ });
117
+
118
+ it('does not apply highlight class when selected', () => {
119
+ const { container } = render(<Token {...defaultProps} highlight={true} selectable={true} selected={true} />);
120
+ expect(container.querySelector('.highlight')).not.toBeInTheDocument();
121
+ });
122
+ });
123
+
124
+ describe('correct/incorrect state', () => {
125
+ it('applies custom class when correct is true', () => {
126
+ const { container } = render(<Token {...defaultProps} correct={true} />);
127
+ expect(container.querySelector('.custom')).toBeInTheDocument();
128
+ });
129
+
130
+ it('applies custom class when correct is false', () => {
131
+ const { container } = render(<Token {...defaultProps} correct={false} />);
132
+ expect(container.querySelector('.custom')).toBeInTheDocument();
133
+ });
134
+
135
+ it('renders check icon when correct is true', () => {
136
+ const { container } = render(<Token {...defaultProps} correct={true} />);
137
+ const checkIcon = container.querySelector('svg[data-testid="CheckIcon"]');
138
+ expect(checkIcon).toBeInTheDocument();
139
+ });
140
+
141
+ it('renders close icon when correct is false', () => {
142
+ const { container } = render(<Token {...defaultProps} correct={false} />);
143
+ const closeIcon = container.querySelector('svg[data-testid="CloseIcon"]');
144
+ expect(closeIcon).toBeInTheDocument();
145
+ });
146
+
147
+ it('wraps token in correct container when correct is true', () => {
148
+ const { container } = render(<Token {...defaultProps} correct={true} />);
149
+ expect(container.firstChild).toBeInTheDocument();
150
+ });
151
+
152
+ it('wraps token in incorrect container when correct is false', () => {
153
+ const { container } = render(<Token {...defaultProps} correct={false} />);
154
+ expect(container.firstChild).toBeInTheDocument();
155
+ });
156
+ });
157
+
158
+ describe('missing state', () => {
159
+ it('applies missing class when isMissing is true', () => {
160
+ const { container } = render(<Token {...defaultProps} isMissing={true} />);
161
+ expect(container.querySelector('.missing')).toBeInTheDocument();
162
+ });
163
+
164
+ it('renders close icon when isMissing is true', () => {
165
+ const { container } = render(<Token {...defaultProps} isMissing={true} />);
166
+ const closeIcon = container.querySelector('svg[data-testid="CloseIcon"]');
167
+ expect(closeIcon).toBeInTheDocument();
168
+ });
169
+
170
+ it('applies custom class when isMissing is true', () => {
171
+ const { container } = render(<Token {...defaultProps} isMissing={true} />);
172
+ expect(container.querySelector('.custom')).toBeInTheDocument();
173
+ });
174
+ });
175
+
176
+ describe('animations disabled state', () => {
177
+ it('applies print class when animationsDisabled is true', () => {
178
+ const { container } = render(<Token {...defaultProps} animationsDisabled={true} />);
179
+ expect(container.querySelector('.print')).toBeInTheDocument();
180
+ });
181
+
182
+ it('does not apply print class when animationsDisabled is false', () => {
183
+ const { container } = render(<Token {...defaultProps} animationsDisabled={false} />);
184
+ expect(container.querySelector('.print')).not.toBeInTheDocument();
185
+ });
186
+ });
187
+
188
+ describe('Wrapper component behavior', () => {
189
+ it('uses wrapper when correct is defined', () => {
190
+ const { container } = render(<Token {...defaultProps} correct={true} />);
191
+ const icon = container.querySelector('svg');
192
+ expect(icon).toBeInTheDocument();
193
+ });
194
+
195
+ it('uses wrapper when isMissing is true', () => {
196
+ const { container } = render(<Token {...defaultProps} isMissing={true} />);
197
+ const icon = container.querySelector('svg');
198
+ expect(icon).toBeInTheDocument();
199
+ });
200
+
201
+ it('does not use wrapper when correct is undefined and isMissing is false', () => {
202
+ const { container } = render(<Token {...defaultProps} />);
203
+ const icon = container.querySelector('svg');
204
+ expect(icon).not.toBeInTheDocument();
205
+ });
206
+ });
207
+
208
+ describe('className combinations', () => {
209
+ it('combines multiple state classes', () => {
210
+ const { container } = render(<Token {...defaultProps} selectable={true} selected={true} highlight={true} />);
211
+ expect(container.querySelector('.selected')).toBeInTheDocument();
212
+ });
213
+
214
+ it('applies correct precedence for disabled and selected', () => {
215
+ const { container } = render(<Token {...defaultProps} selectable={true} selected={true} disabled={true} />);
216
+ expect(container.querySelector('.disabledBlack')).toBeInTheDocument();
217
+ });
218
+
219
+ it('applies tokenRootClass to all tokens', () => {
220
+ const { container } = render(<Token {...defaultProps} />);
221
+ expect(container.querySelector('.tokenRootClass')).toBeInTheDocument();
222
+ });
223
+ });
224
+
225
+ describe('prop defaults', () => {
226
+ it('uses default value for selectable', () => {
227
+ const { container } = render(<Token text="test" />);
228
+ expect(container.firstChild).toBeInTheDocument();
229
+ });
230
+
231
+ it('uses default value for text', () => {
232
+ const { container } = render(<Token />);
233
+ expect(container.firstChild).toBeInTheDocument();
234
+ });
29
235
  });
30
236
  });
@@ -2,10 +2,9 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import Token, { TokenTypes } from './token';
4
4
  import { styled } from '@mui/material/styles';
5
- import clone from 'lodash/clone';
5
+ import { clone, isEqual } from 'lodash-es';
6
6
  import debug from 'debug';
7
7
  import { noSelect } from '@pie-lib/style-utils';
8
- import isEqual from 'lodash/isEqual';
9
8
 
10
9
  const log = debug('@pie-lib:text-select:token-select');
11
10
 
@@ -209,11 +209,7 @@ export class Token extends React.Component {
209
209
  const TokenComponent = Component || StyledToken;
210
210
 
211
211
  return (
212
- <Wrapper
213
- useWrapper={correct !== undefined || isMissing}
214
- Container={Container}
215
- Icon={Icon}
216
- >
212
+ <Wrapper useWrapper={correct !== undefined || isMissing} Container={Container} Icon={Icon}>
217
213
  <TokenComponent
218
214
  className={className}
219
215
  dangerouslySetInnerHTML={{ __html: (text || '').replace(/\n/g, '<br>') }}
@@ -1,4 +1,4 @@
1
- import { normalize, sentences, words, paragraphs, sort, intersection } from '../builder';
1
+ import { intersection, normalize, paragraphs, sentences, sort, words } from '../builder';
2
2
 
3
3
  const token = (start, end, text) => ({ start, end, text });
4
4
 
@@ -1,6 +1,6 @@
1
1
  import { Controls } from '../controls';
2
2
  import React from 'react';
3
- import { render, screen } from '@testing-library/react';
3
+ import { render } from '@testing-library/react';
4
4
 
5
5
  describe('controls', () => {
6
6
  const defaultProps = {