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

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.
@@ -25,6 +25,8 @@ var _TextInputWrapper = _interopRequireDefault(require("./_TextInputWrapper"));
25
25
 
26
26
  var _Box = _interopRequireDefault(require("./Box"));
27
27
 
28
+ var _iterateFocusableElements = require("./utils/iterateFocusableElements");
29
+
28
30
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
29
31
 
30
32
  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); }
@@ -98,14 +100,25 @@ function TextInputWithTokensInnerComponent({
98
100
  }, [selectedTokenIndex]);
99
101
 
100
102
  const handleTokenRemove = tokenId => {
101
- onTokenRemove(tokenId);
103
+ onTokenRemove(tokenId); // HACK: wait a tick for the the token node to be removed from the DOM
102
104
 
103
- if (selectedTokenIndex) {
104
- var _containerRef$current2;
105
+ setTimeout(() => {
106
+ var _containerRef$current2, _containerRef$current3;
105
107
 
106
- const nextElementToFocus = (_containerRef$current2 = containerRef.current) === null || _containerRef$current2 === void 0 ? void 0 : _containerRef$current2.children[selectedTokenIndex];
107
- nextElementToFocus.focus();
108
- }
108
+ 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",
109
+ // `nextFocusableElement` is the div that wraps the input
110
+
111
+ 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));
112
+
113
+ if (firstFocusable) {
114
+ firstFocusable.focus();
115
+ } else {
116
+ var _ref$current;
117
+
118
+ // if there are no tokens left, focus the input
119
+ (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.focus();
120
+ }
121
+ }, 0);
109
122
  };
110
123
 
111
124
  const handleTokenFocus = tokenIndex => () => {
@@ -118,9 +131,9 @@ function TextInputWithTokensInnerComponent({
118
131
 
119
132
  const handleTokenKeyUp = e => {
120
133
  if (e.key === 'Escape') {
121
- var _ref$current;
134
+ var _ref$current2;
122
135
 
123
- (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.focus();
136
+ (_ref$current2 = ref.current) === null || _ref$current2 === void 0 ? void 0 : _ref$current2.focus();
124
137
  }
125
138
  };
126
139
 
@@ -130,13 +143,13 @@ function TextInputWithTokensInnerComponent({
130
143
  };
131
144
 
132
145
  const handleInputKeyDown = e => {
133
- var _ref$current2;
146
+ var _ref$current3;
134
147
 
135
148
  if (onKeyDown) {
136
149
  onKeyDown(e);
137
150
  }
138
151
 
139
- if ((_ref$current2 = ref.current) !== null && _ref$current2 !== void 0 && _ref$current2.value) {
152
+ if ((_ref$current3 = ref.current) !== null && _ref$current3 !== void 0 && _ref$current3.value) {
140
153
  return;
141
154
  }
142
155
 
@@ -157,9 +170,9 @@ function TextInputWithTokensInnerComponent({
157
170
 
158
171
 
159
172
  setTimeout(() => {
160
- var _ref$current3;
173
+ var _ref$current4;
161
174
 
162
- (_ref$current3 = ref.current) === null || _ref$current3 === void 0 ? void 0 : _ref$current3.select();
175
+ (_ref$current4 = ref.current) === null || _ref$current4 === void 0 ? void 0 : _ref$current4.select();
163
176
  }, 0);
164
177
  }
165
178
  };
@@ -353,6 +353,58 @@ describe('TextInputWithTokens', () => {
353
353
 
354
354
  expect(onRemoveMock).toHaveBeenCalledWith(mockTokens[4].id);
355
355
  });
356
+ it('moves focus to the next token when removing the first token', () => {
357
+ jest.useFakeTimers();
358
+ const onRemoveMock = jest.fn();
359
+ const {
360
+ getByText
361
+ } = (0, _react2.render)( /*#__PURE__*/_react.default.createElement(_TextInputWithTokens.default, {
362
+ tokens: [...mockTokens].slice(0, 2),
363
+ onTokenRemove: onRemoveMock
364
+ }));
365
+ const tokenNode = getByText(mockTokens[0].text);
366
+
367
+ _react2.fireEvent.focus(tokenNode);
368
+
369
+ _react2.fireEvent.keyDown(tokenNode, {
370
+ key: 'Backspace'
371
+ });
372
+
373
+ jest.runAllTimers();
374
+ setTimeout(() => {
375
+ var _document$activeEleme12;
376
+
377
+ expect((_document$activeEleme12 = document.activeElement) === null || _document$activeEleme12 === void 0 ? void 0 : _document$activeEleme12.textContent).toBe(mockTokens[1].text);
378
+ }, 0);
379
+ jest.useRealTimers();
380
+ });
381
+ it('moves focus to the input when the last token is removed', () => {
382
+ jest.useFakeTimers();
383
+ const onRemoveMock = jest.fn();
384
+ const {
385
+ getByText,
386
+ getByLabelText
387
+ } = (0, _react2.render)( /*#__PURE__*/_react.default.createElement(LabelledTextInputWithTokens, {
388
+ tokens: [mockTokens[0]],
389
+ onTokenRemove: onRemoveMock
390
+ }));
391
+ const tokenNode = getByText(mockTokens[0].text);
392
+ const inputNode = getByLabelText('Tokens');
393
+
394
+ _react2.fireEvent.focus(tokenNode);
395
+
396
+ _react2.fireEvent.keyDown(tokenNode, {
397
+ key: 'Backspace'
398
+ });
399
+
400
+ jest.runAllTimers();
401
+ setTimeout(() => {
402
+ var _document$activeEleme13;
403
+
404
+ expect((_document$activeEleme13 = document.activeElement) === null || _document$activeEleme13 === void 0 ? void 0 : _document$activeEleme13.id).toBe(inputNode.id);
405
+ }, 0);
406
+ jest.useRealTimers();
407
+ });
356
408
  it('calls onKeyDown', () => {
357
409
  const onRemoveMock = jest.fn();
358
410
  const onKeyDownMock = jest.fn();
@@ -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));
@@ -9,7 +9,8 @@ import Token from './Token/Token';
9
9
  import { useProvidedRefOrCreate } from './hooks';
10
10
  import UnstyledTextInput from './_UnstyledTextInput';
11
11
  import TextInputWrapper from './_TextInputWrapper';
12
- import Box from './Box'; // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ import Box from './Box';
13
+ import { isFocusable } from './utils/iterateFocusableElements'; // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
14
 
14
15
  // using forwardRef is important so that other components (ex. Autocomplete) can use the ref
15
16
  function TextInputWithTokensInnerComponent({
@@ -76,14 +77,25 @@ function TextInputWithTokensInnerComponent({
76
77
  }, [selectedTokenIndex]);
77
78
 
78
79
  const handleTokenRemove = tokenId => {
79
- onTokenRemove(tokenId);
80
+ onTokenRemove(tokenId); // HACK: wait a tick for the the token node to be removed from the DOM
80
81
 
81
- if (selectedTokenIndex) {
82
- var _containerRef$current2;
82
+ setTimeout(() => {
83
+ var _containerRef$current2, _containerRef$current3;
83
84
 
84
- const nextElementToFocus = (_containerRef$current2 = containerRef.current) === null || _containerRef$current2 === void 0 ? void 0 : _containerRef$current2.children[selectedTokenIndex];
85
- nextElementToFocus.focus();
86
- }
85
+ 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",
86
+ // `nextFocusableElement` is the div that wraps the input
87
+
88
+ const firstFocusable = nextElementToFocus && isFocusable(nextElementToFocus) ? nextElementToFocus : Array.from(((_containerRef$current3 = containerRef.current) === null || _containerRef$current3 === void 0 ? void 0 : _containerRef$current3.children) || []).find(el => isFocusable(el));
89
+
90
+ if (firstFocusable) {
91
+ firstFocusable.focus();
92
+ } else {
93
+ var _ref$current;
94
+
95
+ // if there are no tokens left, focus the input
96
+ (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.focus();
97
+ }
98
+ }, 0);
87
99
  };
88
100
 
89
101
  const handleTokenFocus = tokenIndex => () => {
@@ -96,9 +108,9 @@ function TextInputWithTokensInnerComponent({
96
108
 
97
109
  const handleTokenKeyUp = e => {
98
110
  if (e.key === 'Escape') {
99
- var _ref$current;
111
+ var _ref$current2;
100
112
 
101
- (_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.focus();
113
+ (_ref$current2 = ref.current) === null || _ref$current2 === void 0 ? void 0 : _ref$current2.focus();
102
114
  }
103
115
  };
104
116
 
@@ -108,13 +120,13 @@ function TextInputWithTokensInnerComponent({
108
120
  };
109
121
 
110
122
  const handleInputKeyDown = e => {
111
- var _ref$current2;
123
+ var _ref$current3;
112
124
 
113
125
  if (onKeyDown) {
114
126
  onKeyDown(e);
115
127
  }
116
128
 
117
- if ((_ref$current2 = ref.current) !== null && _ref$current2 !== void 0 && _ref$current2.value) {
129
+ if ((_ref$current3 = ref.current) !== null && _ref$current3 !== void 0 && _ref$current3.value) {
118
130
  return;
119
131
  }
120
132
 
@@ -135,9 +147,9 @@ function TextInputWithTokensInnerComponent({
135
147
 
136
148
 
137
149
  setTimeout(() => {
138
- var _ref$current3;
150
+ var _ref$current4;
139
151
 
140
- (_ref$current3 = ref.current) === null || _ref$current3 === void 0 ? void 0 : _ref$current3.select();
152
+ (_ref$current4 = ref.current) === null || _ref$current4 === void 0 ? void 0 : _ref$current4.select();
141
153
  }, 0);
142
154
  }
143
155
  };
@@ -308,6 +308,52 @@ describe('TextInputWithTokens', () => {
308
308
  });
309
309
  expect(onRemoveMock).toHaveBeenCalledWith(mockTokens[4].id);
310
310
  });
311
+ it('moves focus to the next token when removing the first token', () => {
312
+ jest.useFakeTimers();
313
+ const onRemoveMock = jest.fn();
314
+ const {
315
+ getByText
316
+ } = HTMLRender( /*#__PURE__*/React.createElement(TextInputWithTokens, {
317
+ tokens: [...mockTokens].slice(0, 2),
318
+ onTokenRemove: onRemoveMock
319
+ }));
320
+ const tokenNode = getByText(mockTokens[0].text);
321
+ fireEvent.focus(tokenNode);
322
+ fireEvent.keyDown(tokenNode, {
323
+ key: 'Backspace'
324
+ });
325
+ jest.runAllTimers();
326
+ setTimeout(() => {
327
+ var _document$activeEleme12;
328
+
329
+ expect((_document$activeEleme12 = document.activeElement) === null || _document$activeEleme12 === void 0 ? void 0 : _document$activeEleme12.textContent).toBe(mockTokens[1].text);
330
+ }, 0);
331
+ jest.useRealTimers();
332
+ });
333
+ it('moves focus to the input when the last token is removed', () => {
334
+ jest.useFakeTimers();
335
+ const onRemoveMock = jest.fn();
336
+ const {
337
+ getByText,
338
+ getByLabelText
339
+ } = HTMLRender( /*#__PURE__*/React.createElement(LabelledTextInputWithTokens, {
340
+ tokens: [mockTokens[0]],
341
+ onTokenRemove: onRemoveMock
342
+ }));
343
+ const tokenNode = getByText(mockTokens[0].text);
344
+ const inputNode = getByLabelText('Tokens');
345
+ fireEvent.focus(tokenNode);
346
+ fireEvent.keyDown(tokenNode, {
347
+ key: 'Backspace'
348
+ });
349
+ jest.runAllTimers();
350
+ setTimeout(() => {
351
+ var _document$activeEleme13;
352
+
353
+ expect((_document$activeEleme13 = document.activeElement) === null || _document$activeEleme13 === void 0 ? void 0 : _document$activeEleme13.id).toBe(inputNode.id);
354
+ }, 0);
355
+ jest.useRealTimers();
356
+ });
311
357
  it('calls onKeyDown', () => {
312
358
  const onRemoveMock = jest.fn();
313
359
  const onKeyDownMock = jest.fn();
@@ -53,7 +53,7 @@ const mockTokens = [{
53
53
  id: 21
54
54
  }];
55
55
  export const Default = () => {
56
- const [tokens, setTokens] = useState([...mockTokens].slice(0, 2));
56
+ const [tokens, setTokens] = useState([...mockTokens].slice(0, 3));
57
57
 
58
58
  const onTokenRemove = tokenId => {
59
59
  setTokens(tokens.filter(token => token.id !== tokenId));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primer/components",
3
- "version": "31.0.2-rc.1e80de40",
3
+ "version": "31.0.2-rc.c7dafefb",
4
4
  "description": "Primer react components",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib-esm/index.js",
@@ -11,6 +11,7 @@ import {useProvidedRefOrCreate} from './hooks'
11
11
  import UnstyledTextInput from './_UnstyledTextInput'
12
12
  import TextInputWrapper from './_TextInputWrapper'
13
13
  import Box from './Box'
14
+ import {isFocusable} from './utils/iterateFocusableElements'
14
15
 
15
16
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
17
  type AnyReactComponent = React.ComponentType<any>
@@ -117,10 +118,24 @@ function TextInputWithTokensInnerComponent<TokenComponentType extends AnyReactCo
117
118
  const handleTokenRemove = (tokenId: string | number) => {
118
119
  onTokenRemove(tokenId)
119
120
 
120
- if (selectedTokenIndex) {
121
- const nextElementToFocus = containerRef.current?.children[selectedTokenIndex] as HTMLElement
122
- nextElementToFocus.focus()
123
- }
121
+ // HACK: wait a tick for the the token node to be removed from the DOM
122
+ setTimeout(() => {
123
+ const nextElementToFocus = containerRef.current?.children[selectedTokenIndex || 0] as HTMLElement | undefined
124
+
125
+ // when removing the first token by keying "Backspace" or "Delete",
126
+ // `nextFocusableElement` is the div that wraps the input
127
+ const firstFocusable =
128
+ nextElementToFocus && isFocusable(nextElementToFocus)
129
+ ? nextElementToFocus
130
+ : (Array.from(containerRef.current?.children || []) as HTMLElement[]).find(el => isFocusable(el))
131
+
132
+ if (firstFocusable) {
133
+ firstFocusable.focus()
134
+ } else {
135
+ // if there are no tokens left, focus the input
136
+ ref.current?.focus()
137
+ }
138
+ }, 0)
124
139
  }
125
140
 
126
141
  const handleTokenFocus: (tokenIndex: number) => FocusEventHandler = tokenIndex => () => {
@@ -238,6 +238,44 @@ describe('TextInputWithTokens', () => {
238
238
  expect(onRemoveMock).toHaveBeenCalledWith(mockTokens[4].id)
239
239
  })
240
240
 
241
+ it('moves focus to the next token when removing the first token', () => {
242
+ jest.useFakeTimers()
243
+ const onRemoveMock = jest.fn()
244
+ const {getByText} = HTMLRender(
245
+ <TextInputWithTokens tokens={[...mockTokens].slice(0, 2)} onTokenRemove={onRemoveMock} />
246
+ )
247
+ const tokenNode = getByText(mockTokens[0].text)
248
+
249
+ fireEvent.focus(tokenNode)
250
+ fireEvent.keyDown(tokenNode, {key: 'Backspace'})
251
+
252
+ jest.runAllTimers()
253
+ setTimeout(() => {
254
+ expect(document.activeElement?.textContent).toBe(mockTokens[1].text)
255
+ }, 0)
256
+
257
+ jest.useRealTimers()
258
+ })
259
+
260
+ it('moves focus to the input when the last token is removed', () => {
261
+ jest.useFakeTimers()
262
+ const onRemoveMock = jest.fn()
263
+ const {getByText, getByLabelText} = HTMLRender(
264
+ <LabelledTextInputWithTokens tokens={[mockTokens[0]]} onTokenRemove={onRemoveMock} />
265
+ )
266
+ const tokenNode = getByText(mockTokens[0].text)
267
+ const inputNode = getByLabelText('Tokens')
268
+
269
+ fireEvent.focus(tokenNode)
270
+ fireEvent.keyDown(tokenNode, {key: 'Backspace'})
271
+
272
+ jest.runAllTimers()
273
+ setTimeout(() => {
274
+ expect(document.activeElement?.id).toBe(inputNode.id)
275
+ }, 0)
276
+ jest.useRealTimers()
277
+ })
278
+
241
279
  it('calls onKeyDown', () => {
242
280
  const onRemoveMock = jest.fn()
243
281
  const onKeyDownMock = jest.fn()
@@ -47,7 +47,7 @@ const mockTokens = [
47
47
  ]
48
48
 
49
49
  export const Default = () => {
50
- const [tokens, setTokens] = useState([...mockTokens].slice(0, 2))
50
+ const [tokens, setTokens] = useState([...mockTokens].slice(0, 3))
51
51
  const onTokenRemove: (tokenId: string | number) => void = tokenId => {
52
52
  setTokens(tokens.filter(token => token.id !== tokenId))
53
53
  }