@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.
- package/CHANGELOG.md +3 -1
- package/dist/browser.esm.js +1 -1
- package/dist/browser.esm.js.map +1 -1
- package/dist/browser.umd.js +27 -27
- package/dist/browser.umd.js.map +1 -1
- package/lib/TextInputWithTokens.js +25 -12
- package/lib/__tests__/TextInputWithTokens.test.js +52 -0
- package/lib/stories/TextInputWithTokens.stories.js +1 -1
- package/lib-esm/TextInputWithTokens.js +25 -13
- package/lib-esm/__tests__/TextInputWithTokens.test.js +46 -0
- package/lib-esm/stories/TextInputWithTokens.stories.js +1 -1
- package/package.json +1 -1
- package/src/TextInputWithTokens.tsx +19 -4
- package/src/__tests__/TextInputWithTokens.test.tsx +38 -0
- package/src/stories/TextInputWithTokens.stories.tsx +1 -1
- package/stats.html +1 -1
@@ -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
|
-
|
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
|
-
|
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$
|
134
|
+
var _ref$current2;
|
122
135
|
|
123
|
-
(_ref$
|
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$
|
146
|
+
var _ref$current3;
|
134
147
|
|
135
148
|
if (onKeyDown) {
|
136
149
|
onKeyDown(e);
|
137
150
|
}
|
138
151
|
|
139
|
-
if ((_ref$
|
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$
|
173
|
+
var _ref$current4;
|
161
174
|
|
162
|
-
(_ref$
|
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,
|
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';
|
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
|
-
|
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
|
-
|
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$
|
111
|
+
var _ref$current2;
|
100
112
|
|
101
|
-
(_ref$
|
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$
|
123
|
+
var _ref$current3;
|
112
124
|
|
113
125
|
if (onKeyDown) {
|
114
126
|
onKeyDown(e);
|
115
127
|
}
|
116
128
|
|
117
|
-
if ((_ref$
|
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$
|
150
|
+
var _ref$current4;
|
139
151
|
|
140
|
-
(_ref$
|
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,
|
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
@@ -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
|
-
|
121
|
-
|
122
|
-
nextElementToFocus.
|
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,
|
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
|
}
|