@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.
- package/.changeset/tiny-ghosts-repeat.md +5 -0
- package/CHANGELOG.md +3 -1
- package/dist/browser.esm.js +11 -10
- package/dist/browser.esm.js.map +1 -1
- package/dist/browser.umd.js +31 -30
- package/dist/browser.umd.js.map +1 -1
- package/docs/content/TextInputWithTokens.mdx +114 -0
- package/lib/TextInputWithTokens.d.ts +4 -0
- package/lib/TextInputWithTokens.js +86 -20
- package/lib/_TextInputWrapper.js +1 -1
- package/lib/__tests__/TextInputWithTokens.test.js +197 -5
- package/lib/stories/TextInputWithTokens.stories.js +19 -2
- package/lib-esm/TextInputWithTokens.d.ts +4 -0
- package/lib-esm/TextInputWithTokens.js +85 -21
- package/lib-esm/_TextInputWrapper.js +1 -1
- package/lib-esm/__tests__/TextInputWithTokens.test.js +184 -5
- package/lib-esm/stories/TextInputWithTokens.stories.js +15 -1
- package/package.json +1 -1
- package/src/TextInputWithTokens.tsx +83 -12
- package/src/_TextInputWrapper.tsx +1 -0
- package/src/__tests__/TextInputWithTokens.test.tsx +171 -1
- package/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap +7 -0
- package/src/__tests__/__snapshots__/TextInput.test.tsx.snap +6 -0
- package/src/__tests__/__snapshots__/TextInputWithTokens.test.tsx.snap +463 -0
- package/src/stories/TextInputWithTokens.stories.tsx +10 -1
- package/stats.html +1 -1
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
120
|
-
if (
|
121
|
-
var _ref$
|
153
|
+
const handleTokenKeyUp = event => {
|
154
|
+
if (event.key === 'Escape') {
|
155
|
+
var _ref$current2;
|
122
156
|
|
123
|
-
(_ref$
|
157
|
+
(_ref$current2 = ref.current) === null || _ref$current2 === void 0 ? void 0 : _ref$current2.focus();
|
124
158
|
}
|
125
159
|
};
|
126
160
|
|
127
|
-
const handleInputFocus =
|
128
|
-
onFocus && onFocus(
|
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$
|
182
|
+
var _ref$current3;
|
134
183
|
|
135
184
|
if (onKeyDown) {
|
136
185
|
onKeyDown(e);
|
137
186
|
}
|
138
187
|
|
139
|
-
if ((_ref$
|
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$
|
209
|
+
var _ref$current4;
|
161
210
|
|
162
|
-
(_ref$
|
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))),
|
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";
|
package/lib/_TextInputWrapper.js
CHANGED
@@ -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$
|
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$
|
275
|
-
expect((_document$
|
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,
|
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;
|