uikit-react-public 0.47.2 → 0.48.1
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/dist/components/ConfidentialInput/ConfidentialInput.d.ts +10 -0
- package/dist/components/ConfidentialInput/ConfidentialInput.stories.d.ts +21 -0
- package/dist/components/ConfidentialInput/__tests__/ConfidentialInput.test.d.ts +1 -0
- package/dist/components/ConfidentialInput/index.d.ts +2 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/index.js +4588 -4432
- package/lib/components/ConfidentialInput/ConfidentialInput.stories.tsx +51 -0
- package/lib/components/ConfidentialInput/ConfidentialInput.tsx +280 -0
- package/lib/components/ConfidentialInput/Documentation.mdx +70 -0
- package/lib/components/ConfidentialInput/__tests__/ConfidentialInput.test.tsx +339 -0
- package/lib/components/ConfidentialInput/index.ts +2 -0
- package/lib/components/Input/Input.tsx +18 -2
- package/lib/components/Input/__tests__/Input.test.tsx +34 -0
- package/lib/components/index.ts +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { createRef } from 'react';
|
|
2
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import { ThemeContextProvider } from '../../../theme/useTheme';
|
|
6
|
+
import ConfidentialInput from '../ConfidentialInput';
|
|
7
|
+
import Field from '../../Field';
|
|
8
|
+
import Icon from '../../Icon';
|
|
9
|
+
|
|
10
|
+
const renderInput = (props: React.ComponentProps<typeof ConfidentialInput>) =>
|
|
11
|
+
render(
|
|
12
|
+
<ThemeContextProvider>
|
|
13
|
+
<ConfidentialInput {...props} />
|
|
14
|
+
</ThemeContextProvider>
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
describe('ConfidentialInput', () => {
|
|
18
|
+
test('applies a string mask without changing the input value', () => {
|
|
19
|
+
renderInput({
|
|
20
|
+
maskFormat: '1111****',
|
|
21
|
+
value: '12345678',
|
|
22
|
+
onChange: () => {},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(screen.getByTestId('ucl-uikit-confidential-input')).toHaveValue(
|
|
26
|
+
'12345678'
|
|
27
|
+
);
|
|
28
|
+
expect(
|
|
29
|
+
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
30
|
+
).toHaveTextContent('1234••••');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('supports separators and masks characters beyond the template', () => {
|
|
34
|
+
renderInput({
|
|
35
|
+
maskFormat: '**-**-11',
|
|
36
|
+
value: '1234729',
|
|
37
|
+
onChange: () => {},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(
|
|
41
|
+
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
42
|
+
).toHaveTextContent('••-••-72•');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('applies a function mask', () => {
|
|
46
|
+
renderInput({
|
|
47
|
+
maskFormat: (value) => `**** ending in ${value.slice(-2)}`,
|
|
48
|
+
value: '12345678',
|
|
49
|
+
onChange: () => {},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(
|
|
53
|
+
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
54
|
+
).toHaveTextContent('•••• ending in 78');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('does not add a visibility control by default', () => {
|
|
58
|
+
renderInput({
|
|
59
|
+
maskFormat: '****',
|
|
60
|
+
defaultValue: '1234',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(
|
|
64
|
+
screen.queryByRole('button', { name: 'Show confidential value' })
|
|
65
|
+
).not.toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('toggles between the masked and shown value', async () => {
|
|
69
|
+
const user = userEvent.setup();
|
|
70
|
+
renderInput({
|
|
71
|
+
maskFormat: '****1111',
|
|
72
|
+
defaultValue: '12345678',
|
|
73
|
+
showable: true,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const input = screen.getByTestId(
|
|
77
|
+
'ucl-uikit-confidential-input'
|
|
78
|
+
) as HTMLInputElement;
|
|
79
|
+
const showButton = screen.getByRole('button', {
|
|
80
|
+
name: 'Show confidential value',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(input).toHaveAttribute('type', 'password');
|
|
84
|
+
expect(
|
|
85
|
+
screen.getByTestId('ucl-uikit-confidential-input-show-icon')
|
|
86
|
+
).toBeInTheDocument();
|
|
87
|
+
|
|
88
|
+
await user.click(showButton);
|
|
89
|
+
|
|
90
|
+
expect(input).toHaveAttribute('type', 'text');
|
|
91
|
+
expect(
|
|
92
|
+
screen.queryByTestId('ucl-uikit-confidential-input-mask')
|
|
93
|
+
).not.toBeInTheDocument();
|
|
94
|
+
expect(
|
|
95
|
+
screen.getByRole('button', { name: 'Hide confidential value' })
|
|
96
|
+
).toHaveAttribute('aria-pressed', 'true');
|
|
97
|
+
expect(
|
|
98
|
+
screen.getByTestId('ucl-uikit-confidential-input-hide-icon')
|
|
99
|
+
).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('updates an uncontrolled value and reports the original value', async () => {
|
|
103
|
+
const user = userEvent.setup();
|
|
104
|
+
const onChange = vi.fn();
|
|
105
|
+
renderInput({
|
|
106
|
+
maskFormat: '****',
|
|
107
|
+
onChange,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const input = screen.getByTestId('ucl-uikit-confidential-input');
|
|
111
|
+
await user.type(input, '1234');
|
|
112
|
+
|
|
113
|
+
expect(input).toHaveValue('1234');
|
|
114
|
+
expect(input).toHaveAttribute('type', 'text');
|
|
115
|
+
expect(
|
|
116
|
+
screen.queryByTestId('ucl-uikit-confidential-input-mask')
|
|
117
|
+
).not.toBeInTheDocument();
|
|
118
|
+
|
|
119
|
+
await user.tab();
|
|
120
|
+
|
|
121
|
+
expect(
|
|
122
|
+
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
123
|
+
).toHaveTextContent('••••');
|
|
124
|
+
expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({}));
|
|
125
|
+
expect(onChange.mock.lastCall?.[0].target.value).toBe('1234');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('submits the original value', () => {
|
|
129
|
+
render(
|
|
130
|
+
<ThemeContextProvider>
|
|
131
|
+
<form data-testid='form'>
|
|
132
|
+
<ConfidentialInput
|
|
133
|
+
name='accountNumber'
|
|
134
|
+
maskFormat='****1111'
|
|
135
|
+
defaultValue='12345678'
|
|
136
|
+
/>
|
|
137
|
+
</form>
|
|
138
|
+
</ThemeContextProvider>
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const form = screen.getByTestId('form') as HTMLFormElement;
|
|
142
|
+
expect(new FormData(form).get('accountNumber')).toBe('12345678');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('resynchronises the mask after a native form reset', async () => {
|
|
146
|
+
const user = userEvent.setup();
|
|
147
|
+
render(
|
|
148
|
+
<ThemeContextProvider>
|
|
149
|
+
<form data-testid='form'>
|
|
150
|
+
<ConfidentialInput
|
|
151
|
+
maskFormat={(value) => value}
|
|
152
|
+
defaultValue='1234'
|
|
153
|
+
/>
|
|
154
|
+
</form>
|
|
155
|
+
</ThemeContextProvider>
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const input = screen.getByTestId('ucl-uikit-confidential-input');
|
|
159
|
+
const form = screen.getByTestId('form') as HTMLFormElement;
|
|
160
|
+
|
|
161
|
+
await user.clear(input);
|
|
162
|
+
await user.type(input, '9876');
|
|
163
|
+
|
|
164
|
+
await user.tab();
|
|
165
|
+
|
|
166
|
+
expect(
|
|
167
|
+
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
168
|
+
).toHaveTextContent('9876');
|
|
169
|
+
|
|
170
|
+
form.reset();
|
|
171
|
+
|
|
172
|
+
await waitFor(() => {
|
|
173
|
+
expect(input).toHaveValue('1234');
|
|
174
|
+
expect(
|
|
175
|
+
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
176
|
+
).toHaveTextContent('1234');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('shows the raw value while editing a non-showable value', async () => {
|
|
181
|
+
const user = userEvent.setup();
|
|
182
|
+
renderInput({
|
|
183
|
+
maskFormat: '**-**-11',
|
|
184
|
+
defaultValue: '123472',
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const input = screen.getByTestId(
|
|
188
|
+
'ucl-uikit-confidential-input'
|
|
189
|
+
) as HTMLInputElement;
|
|
190
|
+
expect(
|
|
191
|
+
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
192
|
+
).toHaveTextContent('••-••-72');
|
|
193
|
+
|
|
194
|
+
await user.click(input);
|
|
195
|
+
|
|
196
|
+
expect(input).toHaveAttribute('type', 'text');
|
|
197
|
+
expect(input).toHaveValue('123472');
|
|
198
|
+
expect(
|
|
199
|
+
screen.queryByTestId('ucl-uikit-confidential-input-mask')
|
|
200
|
+
).not.toBeInTheDocument();
|
|
201
|
+
|
|
202
|
+
await user.tab();
|
|
203
|
+
|
|
204
|
+
expect(
|
|
205
|
+
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
206
|
+
).toHaveTextContent('••-••-72');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('uses native password masking when visibleWhenFocused is false', async () => {
|
|
210
|
+
const user = userEvent.setup();
|
|
211
|
+
renderInput({
|
|
212
|
+
maskFormat: '**-**-11',
|
|
213
|
+
defaultValue: '123472',
|
|
214
|
+
visibleWhenFocused: false,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const input = screen.getByTestId(
|
|
218
|
+
'ucl-uikit-confidential-input'
|
|
219
|
+
) as HTMLInputElement;
|
|
220
|
+
expect(
|
|
221
|
+
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
222
|
+
).toHaveTextContent('••-••-72');
|
|
223
|
+
|
|
224
|
+
await user.click(input);
|
|
225
|
+
|
|
226
|
+
expect(input).toHaveAttribute('type', 'password');
|
|
227
|
+
expect(
|
|
228
|
+
screen.queryByTestId('ucl-uikit-confidential-input-mask')
|
|
229
|
+
).not.toBeInTheDocument();
|
|
230
|
+
input.setSelectionRange(4, 4);
|
|
231
|
+
expect(input.selectionStart).toBe(4);
|
|
232
|
+
|
|
233
|
+
await user.tab();
|
|
234
|
+
|
|
235
|
+
expect(
|
|
236
|
+
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
237
|
+
).toHaveTextContent('••-••-72');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('shows the raw value while editing a showable value', async () => {
|
|
241
|
+
const user = userEvent.setup();
|
|
242
|
+
renderInput({
|
|
243
|
+
maskFormat: '**-**-11',
|
|
244
|
+
defaultValue: '123472',
|
|
245
|
+
showable: true,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const input = screen.getByTestId(
|
|
249
|
+
'ucl-uikit-confidential-input'
|
|
250
|
+
) as HTMLInputElement;
|
|
251
|
+
expect(
|
|
252
|
+
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
253
|
+
).toHaveTextContent('••-••-72');
|
|
254
|
+
|
|
255
|
+
await user.click(input);
|
|
256
|
+
|
|
257
|
+
expect(input).toHaveAttribute('type', 'text');
|
|
258
|
+
expect(
|
|
259
|
+
screen.queryByTestId('ucl-uikit-confidential-input-mask')
|
|
260
|
+
).not.toBeInTheDocument();
|
|
261
|
+
expect(
|
|
262
|
+
screen.getByRole('button', { name: 'Show confidential value' })
|
|
263
|
+
).toHaveAttribute('aria-pressed', 'false');
|
|
264
|
+
input.setSelectionRange(4, 4);
|
|
265
|
+
expect(input.selectionStart).toBe(4);
|
|
266
|
+
|
|
267
|
+
await user.tab();
|
|
268
|
+
|
|
269
|
+
expect(
|
|
270
|
+
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
271
|
+
).toHaveTextContent('••-••-72');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('supports a right icon alongside the visibility control', () => {
|
|
275
|
+
renderInput({
|
|
276
|
+
maskFormat: '****',
|
|
277
|
+
defaultValue: '1234',
|
|
278
|
+
showable: true,
|
|
279
|
+
icon: <Icon.Search testId='right-icon' />,
|
|
280
|
+
iconPosition: 'right',
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
expect(screen.getByTestId('right-icon').parentElement).toHaveStyle({
|
|
284
|
+
right: '48px',
|
|
285
|
+
});
|
|
286
|
+
expect(
|
|
287
|
+
screen.getByTestId('ucl-uikit-confidential-input-visibility-toggle')
|
|
288
|
+
.parentElement
|
|
289
|
+
).toHaveStyle({ right: '16px' });
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('disables the visibility control with the input', () => {
|
|
293
|
+
renderInput({
|
|
294
|
+
maskFormat: '****',
|
|
295
|
+
defaultValue: '1234',
|
|
296
|
+
showable: true,
|
|
297
|
+
disabled: true,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
expect(
|
|
301
|
+
screen.getByRole('button', { name: 'Show confidential value' })
|
|
302
|
+
).toBeDisabled();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('inherits its disabled state from Field', () => {
|
|
306
|
+
render(
|
|
307
|
+
<ThemeContextProvider>
|
|
308
|
+
<Field disabled>
|
|
309
|
+
<ConfidentialInput
|
|
310
|
+
maskFormat='****'
|
|
311
|
+
defaultValue='1234'
|
|
312
|
+
showable
|
|
313
|
+
/>
|
|
314
|
+
</Field>
|
|
315
|
+
</ThemeContextProvider>
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
expect(screen.getByTestId('ucl-uikit-confidential-input')).toBeDisabled();
|
|
319
|
+
expect(
|
|
320
|
+
screen.getByRole('button', { name: 'Show confidential value' })
|
|
321
|
+
).toBeDisabled();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test('forwards its ref to the input', () => {
|
|
325
|
+
const ref = createRef<HTMLInputElement>();
|
|
326
|
+
render(
|
|
327
|
+
<ThemeContextProvider>
|
|
328
|
+
<ConfidentialInput
|
|
329
|
+
ref={ref}
|
|
330
|
+
maskFormat='****'
|
|
331
|
+
/>
|
|
332
|
+
</ThemeContextProvider>
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
expect(ref.current).toBe(
|
|
336
|
+
screen.getByTestId('ucl-uikit-confidential-input')
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
@@ -88,13 +88,24 @@ const Input = forwardRef<Ref, InputProps>(
|
|
|
88
88
|
padding-right: ${theme.padding.p48};
|
|
89
89
|
`;
|
|
90
90
|
|
|
91
|
+
const padRightForTwoControlsStyle = css`
|
|
92
|
+
padding-right: ${theme.padding.p80};
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
const hasRightIconAndButton = Boolean(
|
|
96
|
+
icon && iconPosition === 'right' && iconButton
|
|
97
|
+
);
|
|
98
|
+
|
|
91
99
|
const inputStyle = cx(
|
|
92
100
|
INPUT_NAME,
|
|
93
101
|
inputBaseStyle,
|
|
94
102
|
!disabled && activeAndFocusStyle,
|
|
95
103
|
disabled && disabledStyle,
|
|
96
104
|
icon && iconPosition === 'left' && padLeftStyle,
|
|
97
|
-
((icon && iconPosition === 'right') || iconButton) &&
|
|
105
|
+
((icon && iconPosition === 'right') || iconButton) &&
|
|
106
|
+
!hasRightIconAndButton &&
|
|
107
|
+
padRightStyle,
|
|
108
|
+
hasRightIconAndButton && padRightForTwoControlsStyle,
|
|
98
109
|
inputClassName
|
|
99
110
|
);
|
|
100
111
|
|
|
@@ -126,10 +137,15 @@ const Input = forwardRef<Ref, InputProps>(
|
|
|
126
137
|
right: ${theme.padding.p16};
|
|
127
138
|
`;
|
|
128
139
|
|
|
140
|
+
const rightIconWithButtonPositionStyle = css`
|
|
141
|
+
right: ${theme.padding.p48};
|
|
142
|
+
`;
|
|
143
|
+
|
|
129
144
|
const iconWrapperStyle = cx(
|
|
130
145
|
iconWrapperBaseStyle,
|
|
131
146
|
iconPosition === 'left' && leftPositionStyle,
|
|
132
|
-
iconPosition === 'right' &&
|
|
147
|
+
iconPosition === 'right' &&
|
|
148
|
+
(iconButton ? rightIconWithButtonPositionStyle : rightPositionStyle)
|
|
133
149
|
);
|
|
134
150
|
|
|
135
151
|
const iconButtonStyle = cx(iconWrapperBaseStyle, rightPositionStyle);
|
|
@@ -3,6 +3,8 @@ import { render, screen } from '@testing-library/react';
|
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
4
|
import Input from '../Input';
|
|
5
5
|
import { ThemeContextProvider } from '../../../theme/useTheme';
|
|
6
|
+
import Icon from '../../Icon';
|
|
7
|
+
import IconButton from '../../IconButton';
|
|
6
8
|
|
|
7
9
|
describe('Input', () => {
|
|
8
10
|
// Snapshot tests
|
|
@@ -105,4 +107,36 @@ describe('Input', () => {
|
|
|
105
107
|
const input = screen.getByRole('textbox') as HTMLInputElement;
|
|
106
108
|
expect(input.id).toBe('custom-id');
|
|
107
109
|
});
|
|
110
|
+
|
|
111
|
+
test('positions a right icon before an icon button', () => {
|
|
112
|
+
render(
|
|
113
|
+
<ThemeContextProvider>
|
|
114
|
+
<Input
|
|
115
|
+
icon={
|
|
116
|
+
<Icon.Search
|
|
117
|
+
testId='right-icon'
|
|
118
|
+
aria-hidden='true'
|
|
119
|
+
/>
|
|
120
|
+
}
|
|
121
|
+
iconPosition='right'
|
|
122
|
+
iconButton={
|
|
123
|
+
<IconButton
|
|
124
|
+
testId='icon-button'
|
|
125
|
+
aria-label='Search'
|
|
126
|
+
>
|
|
127
|
+
<Icon.Search />
|
|
128
|
+
</IconButton>
|
|
129
|
+
}
|
|
130
|
+
/>
|
|
131
|
+
</ThemeContextProvider>
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const input = screen.getByTestId('ucl-uikit-input');
|
|
135
|
+
const iconWrapper = screen.getByTestId('right-icon').parentElement;
|
|
136
|
+
const buttonWrapper = screen.getByTestId('icon-button').parentElement;
|
|
137
|
+
|
|
138
|
+
expect(input).toHaveStyle({ paddingRight: '80px' });
|
|
139
|
+
expect(iconWrapper).toHaveStyle({ right: '48px' });
|
|
140
|
+
expect(buttonWrapper).toHaveStyle({ right: '16px' });
|
|
141
|
+
});
|
|
108
142
|
});
|
package/lib/components/index.ts
CHANGED
|
@@ -4,6 +4,9 @@ export type { IconProps } from './Icon';
|
|
|
4
4
|
export { default as Input } from './Input';
|
|
5
5
|
export type { InputProps } from './Input';
|
|
6
6
|
|
|
7
|
+
export { default as ConfidentialInput } from './ConfidentialInput';
|
|
8
|
+
export type { ConfidentialInputProps } from './ConfidentialInput';
|
|
9
|
+
|
|
7
10
|
export { default as Link } from './Link';
|
|
8
11
|
export type { LinkProps } from './Link';
|
|
9
12
|
|