@primer/components 31.0.2-rc.1e80de40 → 31.0.2-rc.95622264

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.
@@ -48,6 +48,7 @@ render(BasicExample)
48
48
  | preventTokenWrapping | `boolean` | `false` | Optional. Whether tokens should render inline horizontally. By default, tokens wrap to new lines. |
49
49
  | size | `TokenSizeKeys` | `extralarge` | Optional. The size of the tokens |
50
50
  | hideTokenRemoveButtons | `boolean` | `false` | Optional. Whether the remove buttons should be rendered in the tokens |
51
+ | visibleTokenCount | `number` | `undefined` | Optional. The number of tokens to display before truncating |
51
52
 
52
53
  ## Adding and removing tokens
53
54
 
@@ -95,3 +96,116 @@ const UsingIssueLabelTokens = () => {
95
96
 
96
97
  render(<UsingIssueLabelTokens />)
97
98
  ```
99
+
100
+ ## Dealing with long lists of tokens
101
+
102
+ By default, all tokens will be visible when the component is rendered.
103
+
104
+ If the component is being used in an area where it's height needs to be constrained, there are options to limit the height of the input.
105
+
106
+ ### Hide and show tokens
107
+
108
+ ```javascript live noinline
109
+ const VisibleTokenCountExample = () => {
110
+ const [tokens, setTokens] = React.useState([
111
+ {text: 'zero', id: 0},
112
+ {text: 'one', id: 1},
113
+ {text: 'two', id: 2},
114
+ {text: 'three', id: 3}
115
+ ])
116
+ const onTokenRemove = tokenId => {
117
+ setTokens(tokens.filter(token => token.id !== tokenId))
118
+ }
119
+
120
+ return (
121
+ <Box maxWidth="500px">
122
+ <Box as="label" display="block" htmlFor="inputWithTokens-basic">
123
+ Tokens truncated after 2
124
+ </Box>
125
+ <TextInputWithTokens
126
+ visibleTokenCount={2}
127
+ block
128
+ tokens={tokens}
129
+ onTokenRemove={onTokenRemove}
130
+ id="inputWithTokens-basic"
131
+ />
132
+ </Box>
133
+ )
134
+ }
135
+
136
+ render(VisibleTokenCountExample)
137
+ ```
138
+
139
+ ### Render tokens on a single line
140
+
141
+ ```javascript live noinline
142
+ const PreventTokenWrappingExample = () => {
143
+ const [tokens, setTokens] = React.useState([
144
+ {text: 'zero', id: 0},
145
+ {text: 'one', id: 1},
146
+ {text: 'two', id: 2},
147
+ {text: 'three', id: 3},
148
+ {text: 'four', id: 4},
149
+ {text: 'five', id: 5},
150
+ {text: 'six', id: 6},
151
+ {text: 'seven', id: 7}
152
+ ])
153
+ const onTokenRemove = tokenId => {
154
+ setTokens(tokens.filter(token => token.id !== tokenId))
155
+ }
156
+
157
+ return (
158
+ <Box maxWidth="500px">
159
+ <Box as="label" display="block" htmlFor="inputWithTokens-basic">
160
+ Tokens on one line
161
+ </Box>
162
+ <TextInputWithTokens
163
+ preventTokenWrapping
164
+ block
165
+ tokens={tokens}
166
+ onTokenRemove={onTokenRemove}
167
+ id="inputWithTokens-basic"
168
+ />
169
+ </Box>
170
+ )
171
+ }
172
+
173
+ render(PreventTokenWrappingExample)
174
+ ```
175
+
176
+ ### Set a maximum height for the input
177
+
178
+ ```javascript live noinline
179
+ const MaxHeightExample = () => {
180
+ const [tokens, setTokens] = React.useState([
181
+ {text: 'zero', id: 0},
182
+ {text: 'one', id: 1},
183
+ {text: 'two', id: 2},
184
+ {text: 'three', id: 3},
185
+ {text: 'four', id: 4},
186
+ {text: 'five', id: 5},
187
+ {text: 'six', id: 6},
188
+ {text: 'seven', id: 7}
189
+ ])
190
+ const onTokenRemove = tokenId => {
191
+ setTokens(tokens.filter(token => token.id !== tokenId))
192
+ }
193
+
194
+ return (
195
+ <Box maxWidth="500px">
196
+ <Box as="label" display="block" htmlFor="inputWithTokens-basic">
197
+ Tokens restricted to a max height
198
+ </Box>
199
+ <TextInputWithTokens
200
+ maxHeight="50px"
201
+ block
202
+ tokens={tokens}
203
+ onTokenRemove={onTokenRemove}
204
+ id="inputWithTokens-basic"
205
+ />
206
+ </Box>
207
+ )
208
+ }
209
+
210
+ render(MaxHeightExample)
211
+ ```
@@ -32,6 +32,10 @@ declare const TextInputWithTokens: React.ForwardRefExoticComponent<Pick<{
32
32
  * Whether the remove buttons should be rendered in the tokens
33
33
  */
34
34
  hideTokenRemoveButtons?: boolean | undefined;
35
+ /**
36
+ * The number of tokens to display before truncating
37
+ */
38
+ visibleTokenCount?: number | undefined;
35
39
  } & Pick<Omit<Pick<{
36
40
  [x: string]: any;
37
41
  [x: number]: any;
@@ -25,6 +25,10 @@ var _TextInputWrapper = _interopRequireDefault(require("./_TextInputWrapper"));
25
25
 
26
26
  var _Box = _interopRequireDefault(require("./Box"));
27
27
 
28
+ var _Text = _interopRequireDefault(require("./Text"));
29
+
30
+ var _iterateFocusableElements = require("./utils/iterateFocusableElements");
31
+
28
32
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
29
33
 
30
34
  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); }
@@ -33,7 +37,13 @@ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj &&
33
37
 
34
38
  function _extends() { _extends = Object.assign || 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); }
35
39
 
36
- // using forwardRef is important so that other components (ex. Autocomplete) can use the ref
40
+ const overflowCountFontSizeMap = {
41
+ small: 0,
42
+ medium: 1,
43
+ large: 1,
44
+ extralarge: 2
45
+ }; // using forwardRef is important so that other components (ex. Autocomplete) can use the ref
46
+
37
47
  function TextInputWithTokensInnerComponent({
38
48
  icon: IconComponent,
39
49
  contrast,
@@ -53,9 +63,11 @@ function TextInputWithTokensInnerComponent({
53
63
  minWidth: minWidthProp,
54
64
  maxWidth: maxWidthProp,
55
65
  variant: variantProp,
66
+ visibleTokenCount,
56
67
  ...rest
57
68
  }, externalRef) {
58
69
  const {
70
+ onBlur,
59
71
  onFocus,
60
72
  onKeyDown,
61
73
  ...inputPropsRest
@@ -64,6 +76,7 @@ function TextInputWithTokensInnerComponent({
64
76
  const localInputRef = (0, _react.useRef)(null);
65
77
  const combinedInputRef = (0, _useCombinedRefs.useCombinedRefs)(localInputRef, ref);
66
78
  const [selectedTokenIndex, setSelectedTokenIndex] = (0, _react.useState)();
79
+ const [tokensAreTruncated, setTokensAreTruncated] = (0, _react.useState)(Boolean(visibleTokenCount));
67
80
  const {
68
81
  containerRef
69
82
  } = (0, _useFocusZone.useFocusZone)({
@@ -98,14 +111,25 @@ function TextInputWithTokensInnerComponent({
98
111
  }, [selectedTokenIndex]);
99
112
 
100
113
  const handleTokenRemove = tokenId => {
101
- onTokenRemove(tokenId);
114
+ onTokenRemove(tokenId); // HACK: wait a tick for the the token node to be removed from the DOM
102
115
 
103
- if (selectedTokenIndex) {
104
- var _containerRef$current2;
116
+ setTimeout(() => {
117
+ var _containerRef$current2, _containerRef$current3;
105
118
 
106
- const nextElementToFocus = (_containerRef$current2 = containerRef.current) === null || _containerRef$current2 === void 0 ? void 0 : _containerRef$current2.children[selectedTokenIndex];
107
- nextElementToFocus.focus();
108
- }
119
+ const nextElementToFocus = (_containerRef$current2 = containerRef.current) === null || _containerRef$current2 === void 0 ? void 0 : _containerRef$current2.children[selectedTokenIndex || 0]; // when removing the first token by keying "Backspace" or "Delete",
120
+ // `nextFocusableElement` is the div that wraps the input
121
+
122
+ const firstFocusable = nextElementToFocus && (0, _iterateFocusableElements.isFocusable)(nextElementToFocus) ? nextElementToFocus : Array.from(((_containerRef$current3 = containerRef.current) === null || _containerRef$current3 === void 0 ? void 0 : _containerRef$current3.children) || []).find(el => (0, _iterateFocusableElements.isFocusable)(el));
123
+
124
+ if (firstFocusable) {
125
+ firstFocusable.focus();
126
+ } else {
127
+ var _ref$current;
128
+
129
+ // if there are no tokens left, focus the input
130
+ (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.focus();
131
+ }
132
+ }, 0);
109
133
  };
110
134
 
111
135
  const handleTokenFocus = tokenIndex => () => {
@@ -113,30 +137,55 @@ function TextInputWithTokensInnerComponent({
113
137
  };
114
138
 
115
139
  const handleTokenBlur = () => {
116
- setSelectedTokenIndex(undefined);
140
+ setSelectedTokenIndex(undefined); // HACK: wait a tick and check the focused element before hiding truncated tokens
141
+ // this prevents the tokens from hiding when the user is moving focus between tokens,
142
+ // but still hides the tokens when the user blurs the token by tabbing out or clicking somewhere else on the page
143
+
144
+ setTimeout(() => {
145
+ var _containerRef$current4;
146
+
147
+ if (!((_containerRef$current4 = containerRef.current) !== null && _containerRef$current4 !== void 0 && _containerRef$current4.contains(document.activeElement)) && visibleTokenCount) {
148
+ setTokensAreTruncated(true);
149
+ }
150
+ }, 0);
117
151
  };
118
152
 
119
- const handleTokenKeyUp = e => {
120
- if (e.key === 'Escape') {
121
- var _ref$current;
153
+ const handleTokenKeyUp = event => {
154
+ if (event.key === 'Escape') {
155
+ var _ref$current2;
122
156
 
123
- (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.focus();
157
+ (_ref$current2 = ref.current) === null || _ref$current2 === void 0 ? void 0 : _ref$current2.focus();
124
158
  }
125
159
  };
126
160
 
127
- const handleInputFocus = e => {
128
- onFocus && onFocus(e);
161
+ const handleInputFocus = event => {
162
+ onFocus && onFocus(event);
129
163
  setSelectedTokenIndex(undefined);
164
+ visibleTokenCount && setTokensAreTruncated(false);
165
+ };
166
+
167
+ const handleInputBlur = event => {
168
+ onBlur && onBlur(event); // HACK: wait a tick and check the focused element before hiding truncated tokens
169
+ // this prevents the tokens from hiding when the user is moving focus from the input to a token,
170
+ // but still hides the tokens when the user blurs the input by tabbing out or clicking somewhere else on the page
171
+
172
+ setTimeout(() => {
173
+ var _containerRef$current5;
174
+
175
+ if (!((_containerRef$current5 = containerRef.current) !== null && _containerRef$current5 !== void 0 && _containerRef$current5.contains(document.activeElement)) && visibleTokenCount) {
176
+ setTokensAreTruncated(true);
177
+ }
178
+ }, 0);
130
179
  };
131
180
 
132
181
  const handleInputKeyDown = e => {
133
- var _ref$current2;
182
+ var _ref$current3;
134
183
 
135
184
  if (onKeyDown) {
136
185
  onKeyDown(e);
137
186
  }
138
187
 
139
- if ((_ref$current2 = ref.current) !== null && _ref$current2 !== void 0 && _ref$current2.value) {
188
+ if ((_ref$current3 = ref.current) !== null && _ref$current3 !== void 0 && _ref$current3.value) {
140
189
  return;
141
190
  }
142
191
 
@@ -157,13 +206,24 @@ function TextInputWithTokensInnerComponent({
157
206
 
158
207
 
159
208
  setTimeout(() => {
160
- var _ref$current3;
209
+ var _ref$current4;
161
210
 
162
- (_ref$current3 = ref.current) === null || _ref$current3 === void 0 ? void 0 : _ref$current3.select();
211
+ (_ref$current4 = ref.current) === null || _ref$current4 === void 0 ? void 0 : _ref$current4.select();
163
212
  }, 0);
164
213
  }
165
214
  };
166
215
 
216
+ const focusInput = () => {
217
+ var _combinedInputRef$cur;
218
+
219
+ (_combinedInputRef$cur = combinedInputRef.current) === null || _combinedInputRef$cur === void 0 ? void 0 : _combinedInputRef$cur.focus();
220
+ };
221
+
222
+ const preventTokenClickPropagation = event => {
223
+ event.stopPropagation();
224
+ };
225
+
226
+ const visibleTokens = tokensAreTruncated ? tokens.slice(0, visibleTokenCount) : tokens;
167
227
  return /*#__PURE__*/_react.default.createElement(_TextInputWrapper.default, {
168
228
  block: block,
169
229
  className: className,
@@ -175,6 +235,7 @@ function TextInputWithTokensInnerComponent({
175
235
  minWidth: minWidthProp,
176
236
  maxWidth: maxWidthProp,
177
237
  variant: variantProp,
238
+ onClick: focusInput,
178
239
  sx: { ...(block ? {
179
240
  display: 'flex',
180
241
  width: '100%'
@@ -214,12 +275,13 @@ function TextInputWithTokensInnerComponent({
214
275
  ref: combinedInputRef,
215
276
  disabled: disabled,
216
277
  onFocus: handleInputFocus,
278
+ onBlur: handleInputBlur,
217
279
  onKeyDown: handleInputKeyDown,
218
280
  type: "text",
219
281
  sx: {
220
282
  height: '100%'
221
283
  }
222
- }, inputPropsRest))), tokens.length && TokenComponent ? tokens.map(({
284
+ }, inputPropsRest))), TokenComponent ? visibleTokens.map(({
223
285
  id,
224
286
  ...tokenRest
225
287
  }, i) => /*#__PURE__*/_react.default.createElement(TokenComponent, _extends({
@@ -227,6 +289,7 @@ function TextInputWithTokensInnerComponent({
227
289
  onFocus: handleTokenFocus(i),
228
290
  onBlur: handleTokenBlur,
229
291
  onKeyUp: handleTokenKeyUp,
292
+ onClick: preventTokenClickPropagation,
230
293
  isSelected: selectedTokenIndex === i,
231
294
  onRemove: () => {
232
295
  handleTokenRemove(id);
@@ -234,7 +297,10 @@ function TextInputWithTokensInnerComponent({
234
297
  hideRemoveButton: hideTokenRemoveButtons,
235
298
  size: size,
236
299
  tabIndex: 0
237
- }, tokenRest))) : null));
300
+ }, tokenRest))) : null, tokensAreTruncated ? /*#__PURE__*/_react.default.createElement(_Text.default, {
301
+ color: "fg.muted",
302
+ fontSize: size && overflowCountFontSizeMap[size]
303
+ }, "+", tokens.length - visibleTokens.length) : null));
238
304
  }
239
305
 
240
306
  TextInputWithTokensInnerComponent.displayName = "TextInputWithTokensInnerComponent";
@@ -39,7 +39,7 @@ const sizeVariants = (0, _styledSystem.variant)({
39
39
  const TextInputWrapper = _styledComponents.default.span.withConfig({
40
40
  displayName: "_TextInputWrapper__TextInputWrapper",
41
41
  componentId: "sc-5vfcis-0"
42
- })(["display:inline-flex;align-items:stretch;min-height:34px;font-size:", ";line-height:20px;color:", ";vertical-align:middle;background-repeat:no-repeat;background-position:right 8px center;border:1px solid ", ";border-radius:", ";outline:none;box-shadow:", ";", " .TextInput-icon{align-self:center;color:", ";margin:0 ", ";flex-shrink:0;}&:focus-within{border-color:", ";box-shadow:", ";}", " ", " ", " @media (min-width:", "){font-size:", ";}", " ", " ", " ", " ", ";"], (0, _constants.get)('fontSizes.1'), (0, _constants.get)('colors.fg.default'), (0, _constants.get)('colors.border.default'), (0, _constants.get)('radii.2'), (0, _constants.get)('shadows.primer.shadow.inset'), props => {
42
+ })(["display:inline-flex;align-items:stretch;min-height:34px;font-size:", ";line-height:20px;color:", ";vertical-align:middle;background-repeat:no-repeat;background-position:right 8px center;border:1px solid ", ";border-radius:", ";outline:none;box-shadow:", ";cursor:text;", " .TextInput-icon{align-self:center;color:", ";margin:0 ", ";flex-shrink:0;}&:focus-within{border-color:", ";box-shadow:", ";}", " ", " ", " @media (min-width:", "){font-size:", ";}", " ", " ", " ", " ", ";"], (0, _constants.get)('fontSizes.1'), (0, _constants.get)('colors.fg.default'), (0, _constants.get)('colors.border.default'), (0, _constants.get)('radii.2'), (0, _constants.get)('shadows.primer.shadow.inset'), props => {
43
43
  if (props.hasIcon) {
44
44
  return (0, _styledComponents.css)(["padding:0;"]);
45
45
  } else {
@@ -58,8 +58,18 @@ const LabelledTextInputWithTokens = ({
58
58
  tokens: tokens,
59
59
  onTokenRemove: onTokenRemove,
60
60
  id: "tokenInput"
61
- }, rest)));
62
-
61
+ }, rest))); // describe('axe test', () => {
62
+ // it('should have no axe violations', async () => {
63
+ // const onRemoveMock = jest.fn()
64
+ // const {container} = HTMLRender(<LabelledTextInputWithTokens tokens={mockTokens} onTokenRemove={onRemoveMock} />)
65
+ // const results = await axe(container)
66
+ // expect(results).toHaveNoViolations()
67
+ // cleanup()
68
+ // })
69
+ // })
70
+
71
+
72
+ jest.useFakeTimers();
63
73
  describe('TextInputWithTokens', () => {
64
74
  it('renders without tokens', () => {
65
75
  const onRemoveMock = jest.fn();
@@ -127,6 +137,14 @@ describe('TextInputWithTokens', () => {
127
137
  onTokenRemove: onRemoveMock
128
138
  }))).toMatchSnapshot();
129
139
  });
140
+ it('renders a truncated set of tokens', () => {
141
+ const onRemoveMock = jest.fn();
142
+ expect((0, _testing.render)( /*#__PURE__*/_react.default.createElement(_TextInputWithTokens.default, {
143
+ tokens: mockTokens,
144
+ onTokenRemove: onRemoveMock,
145
+ visibleTokenCount: 2
146
+ }))).toMatchSnapshot();
147
+ });
130
148
  it('focuses the previous token when keying ArrowLeft', () => {
131
149
  var _document$activeEleme, _document$activeEleme2;
132
150
 
@@ -250,8 +268,130 @@ describe('TextInputWithTokens', () => {
250
268
  expect((_document$activeEleme8 = document.activeElement) === null || _document$activeEleme8 === void 0 ? void 0 : _document$activeEleme8.id).not.toEqual(lastTokenNode.id);
251
269
  expect((_document$activeEleme9 = document.activeElement) === null || _document$activeEleme9 === void 0 ? void 0 : _document$activeEleme9.id).toEqual(inputNode.id);
252
270
  });
271
+ it('does not focus the input when clicking a token', () => {
272
+ var _document$activeEleme10;
273
+
274
+ const onRemoveMock = jest.fn();
275
+ const {
276
+ getByLabelText,
277
+ getByText
278
+ } = (0, _react2.render)( /*#__PURE__*/_react.default.createElement(LabelledTextInputWithTokens, {
279
+ tokens: mockTokens,
280
+ onTokenRemove: onRemoveMock,
281
+ visibleTokenCount: 2
282
+ }));
283
+ const inputNode = getByLabelText('Tokens');
284
+ const tokenNode = getByText(mockTokens[0].text);
285
+ expect(document.activeElement).not.toEqual(inputNode.id);
286
+
287
+ _react2.fireEvent.click(tokenNode);
288
+
289
+ expect((_document$activeEleme10 = document.activeElement) === null || _document$activeEleme10 === void 0 ? void 0 : _document$activeEleme10.id).not.toEqual(inputNode.id);
290
+ });
291
+ it('focuses the input when clicking somewhere in the component besides the tokens', () => {
292
+ var _document$activeEleme11;
293
+
294
+ const onRemoveMock = jest.fn();
295
+ const {
296
+ getByLabelText,
297
+ getByText
298
+ } = (0, _react2.render)( /*#__PURE__*/_react.default.createElement(LabelledTextInputWithTokens, {
299
+ tokens: mockTokens,
300
+ onTokenRemove: onRemoveMock,
301
+ visibleTokenCount: 2
302
+ }));
303
+ const inputNode = getByLabelText('Tokens');
304
+ const truncatedTokenCount = getByText('+6');
305
+ expect(document.activeElement).not.toEqual(inputNode.id);
306
+
307
+ _react2.fireEvent.click(truncatedTokenCount);
308
+
309
+ expect((_document$activeEleme11 = document.activeElement) === null || _document$activeEleme11 === void 0 ? void 0 : _document$activeEleme11.id).toEqual(inputNode.id);
310
+ });
311
+ it('shows all tokens when the input is focused and hides them when it is blurred (when visibleTokenCount is set)', () => {
312
+ const onRemoveMock = jest.fn();
313
+ const visibleTokenCount = 2;
314
+ const {
315
+ getByLabelText,
316
+ getByText
317
+ } = (0, _react2.render)( /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(LabelledTextInputWithTokens, {
318
+ tokens: mockTokens,
319
+ onTokenRemove: onRemoveMock,
320
+ visibleTokenCount: visibleTokenCount
321
+ }), /*#__PURE__*/_react.default.createElement("button", {
322
+ id: "focusableOutsideComponent"
323
+ }, "Focus me")));
324
+ const inputNode = getByLabelText('Tokens');
325
+ const focusableOutsideComponentNode = getByText('Focus me');
326
+ const allTokenLabels = mockTokens.map(token => token.text);
327
+ const truncatedTokenCountNode = getByText('+6');
328
+ (0, _react2.act)(() => {
329
+ jest.runAllTimers();
330
+
331
+ _react2.fireEvent.focus(inputNode);
332
+ });
333
+ setTimeout(() => {
334
+ for (const tokenLabel of allTokenLabels) {
335
+ const tokenNode = getByText(tokenLabel);
336
+ expect(tokenNode).toBeDefined();
337
+ }
338
+ }, 0);
339
+ (0, _react2.act)(() => {
340
+ jest.runAllTimers(); // onBlur isn't called on input unless we specifically fire the "blur" event
341
+ // eslint-disable-next-line github/no-blur
342
+
343
+ _react2.fireEvent.blur(inputNode);
344
+
345
+ _react2.fireEvent.focus(focusableOutsideComponentNode);
346
+ });
347
+ setTimeout(() => {
348
+ expect(truncatedTokenCountNode).toBeDefined();
349
+
350
+ for (const tokenLabel of allTokenLabels) {
351
+ const tokenNode = getByText(tokenLabel);
352
+
353
+ if (allTokenLabels.indexOf(tokenLabel) > visibleTokenCount) {
354
+ // eslint-disable-next-line jest/no-conditional-expect
355
+ expect(tokenNode).toBeDefined();
356
+ } else {
357
+ // eslint-disable-next-line jest/no-conditional-expect
358
+ expect(tokenNode).not.toBeDefined();
359
+ }
360
+ }
361
+ }, 0);
362
+ jest.useRealTimers();
363
+ });
364
+ it('does not hide tokens when blurring the input to focus within the component (when visibleTokenCount is set)', () => {
365
+ const onRemoveMock = jest.fn();
366
+ const visibleTokenCount = 2;
367
+ const {
368
+ getByLabelText,
369
+ getByText
370
+ } = (0, _react2.render)( /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(LabelledTextInputWithTokens, {
371
+ tokens: mockTokens,
372
+ onTokenRemove: onRemoveMock,
373
+ visibleTokenCount: visibleTokenCount
374
+ }), /*#__PURE__*/_react.default.createElement("button", {
375
+ id: "focusableOutsideComponent"
376
+ }, "Focus me")));
377
+ const inputNode = getByLabelText('Tokens');
378
+ const firstTokenNode = getByText(mockTokens[visibleTokenCount - 1].text);
379
+ const allTokenLabels = mockTokens.map(token => token.text);
380
+ const truncatedTokenCountNode = getByText('+6');
381
+ (0, _react2.act)(() => {
382
+ _react2.fireEvent.focus(inputNode);
383
+
384
+ _react2.fireEvent.focus(firstTokenNode);
385
+ });
386
+ expect(truncatedTokenCountNode).toBeDefined();
387
+
388
+ for (const tokenLabel of allTokenLabels) {
389
+ const tokenNode = getByText(tokenLabel);
390
+ expect(tokenNode).toBeDefined();
391
+ }
392
+ });
253
393
  it('focuses the first token when keying ArrowRight in the input', () => {
254
- var _document$activeEleme10, _document$activeEleme11;
394
+ var _document$activeEleme12, _document$activeEleme13;
255
395
 
256
396
  const onRemoveMock = jest.fn();
257
397
  const {
@@ -271,8 +411,8 @@ describe('TextInputWithTokens', () => {
271
411
  key: 'ArrowRight'
272
412
  });
273
413
 
274
- expect((_document$activeEleme10 = document.activeElement) === null || _document$activeEleme10 === void 0 ? void 0 : _document$activeEleme10.id).not.toEqual(inputNode.id);
275
- expect((_document$activeEleme11 = document.activeElement) === null || _document$activeEleme11 === void 0 ? void 0 : _document$activeEleme11.id).toEqual(firstTokenNode.id);
414
+ expect((_document$activeEleme12 = document.activeElement) === null || _document$activeEleme12 === void 0 ? void 0 : _document$activeEleme12.id).not.toEqual(inputNode.id);
415
+ expect((_document$activeEleme13 = document.activeElement) === null || _document$activeEleme13 === void 0 ? void 0 : _document$activeEleme13.id).toEqual(firstTokenNode.id);
276
416
  });
277
417
  it('calls onTokenRemove on the last token when keying Backspace in an empty input', () => {
278
418
  const onRemoveMock = jest.fn();
@@ -353,6 +493,58 @@ describe('TextInputWithTokens', () => {
353
493
 
354
494
  expect(onRemoveMock).toHaveBeenCalledWith(mockTokens[4].id);
355
495
  });
496
+ it('moves focus to the next token when removing the first token', () => {
497
+ jest.useFakeTimers();
498
+ const onRemoveMock = jest.fn();
499
+ const {
500
+ getByText
501
+ } = (0, _react2.render)( /*#__PURE__*/_react.default.createElement(_TextInputWithTokens.default, {
502
+ tokens: [...mockTokens].slice(0, 2),
503
+ onTokenRemove: onRemoveMock
504
+ }));
505
+ const tokenNode = getByText(mockTokens[0].text);
506
+
507
+ _react2.fireEvent.focus(tokenNode);
508
+
509
+ _react2.fireEvent.keyDown(tokenNode, {
510
+ key: 'Backspace'
511
+ });
512
+
513
+ jest.runAllTimers();
514
+ setTimeout(() => {
515
+ var _document$activeEleme14;
516
+
517
+ expect((_document$activeEleme14 = document.activeElement) === null || _document$activeEleme14 === void 0 ? void 0 : _document$activeEleme14.textContent).toBe(mockTokens[1].text);
518
+ }, 0);
519
+ jest.useRealTimers();
520
+ });
521
+ it('moves focus to the input when the last token is removed', () => {
522
+ jest.useFakeTimers();
523
+ const onRemoveMock = jest.fn();
524
+ const {
525
+ getByText,
526
+ getByLabelText
527
+ } = (0, _react2.render)( /*#__PURE__*/_react.default.createElement(LabelledTextInputWithTokens, {
528
+ tokens: [mockTokens[0]],
529
+ onTokenRemove: onRemoveMock
530
+ }));
531
+ const tokenNode = getByText(mockTokens[0].text);
532
+ const inputNode = getByLabelText('Tokens');
533
+
534
+ _react2.fireEvent.focus(tokenNode);
535
+
536
+ _react2.fireEvent.keyDown(tokenNode, {
537
+ key: 'Backspace'
538
+ });
539
+
540
+ jest.runAllTimers();
541
+ setTimeout(() => {
542
+ var _document$activeEleme15;
543
+
544
+ expect((_document$activeEleme15 = document.activeElement) === null || _document$activeEleme15 === void 0 ? void 0 : _document$activeEleme15.id).toBe(inputNode.id);
545
+ }, 0);
546
+ jest.useRealTimers();
547
+ });
356
548
  it('calls onKeyDown', () => {
357
549
  const onRemoveMock = jest.fn();
358
550
  const onKeyDownMock = jest.fn();
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.Unstyled = exports.TokenRemoveButtonsHidden = exports.TokenWrappingPrevented = exports.MaxHeight = exports.TokenSizeVariants = exports.UsingIssueLabelTokens = exports.Default = exports.default = void 0;
6
+ exports.Unstyled = exports.WithVisibleTokenCount = exports.TokenRemoveButtonsHidden = exports.TokenWrappingPrevented = exports.MaxHeight = exports.TokenSizeVariants = exports.UsingIssueLabelTokens = exports.Default = exports.default = void 0;
7
7
 
8
8
  var _react = _interopRequireWildcard(require("react"));
9
9
 
@@ -72,7 +72,7 @@ const mockTokens = [{
72
72
  }];
73
73
 
74
74
  const Default = () => {
75
- const [tokens, setTokens] = (0, _react.useState)([...mockTokens].slice(0, 2));
75
+ const [tokens, setTokens] = (0, _react.useState)([...mockTokens].slice(0, 3));
76
76
 
77
77
  const onTokenRemove = tokenId => {
78
78
  setTokens(tokens.filter(token => token.id !== tokenId));
@@ -208,6 +208,23 @@ const TokenRemoveButtonsHidden = () => {
208
208
  exports.TokenRemoveButtonsHidden = TokenRemoveButtonsHidden;
209
209
  TokenRemoveButtonsHidden.displayName = "TokenRemoveButtonsHidden";
210
210
 
211
+ const WithVisibleTokenCount = () => {
212
+ const [tokens, setTokens] = (0, _react.useState)([...mockTokens].slice(0, 5));
213
+
214
+ const onTokenRemove = tokenId => {
215
+ setTokens(tokens.filter(token => token.id !== tokenId));
216
+ };
217
+
218
+ return /*#__PURE__*/_react.default.createElement(_TextInputWithTokens.default, {
219
+ tokens: tokens,
220
+ onTokenRemove: onTokenRemove,
221
+ visibleTokenCount: 2
222
+ });
223
+ };
224
+
225
+ exports.WithVisibleTokenCount = WithVisibleTokenCount;
226
+ WithVisibleTokenCount.displayName = "WithVisibleTokenCount";
227
+
211
228
  const Unstyled = () => {
212
229
  const [tokens, setTokens] = (0, _react.useState)([...mockTokens].slice(0, 2));
213
230
 
@@ -32,6 +32,10 @@ declare const TextInputWithTokens: React.ForwardRefExoticComponent<Pick<{
32
32
  * Whether the remove buttons should be rendered in the tokens
33
33
  */
34
34
  hideTokenRemoveButtons?: boolean | undefined;
35
+ /**
36
+ * The number of tokens to display before truncating
37
+ */
38
+ visibleTokenCount?: number | undefined;
35
39
  } & Pick<Omit<Pick<{
36
40
  [x: string]: any;
37
41
  [x: number]: any;