uikit-react-public 0.48.0 → 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 +1 -0
- package/dist/components/ConfidentialInput/ConfidentialInput.stories.d.ts +1 -0
- package/dist/index.js +1028 -1027
- package/lib/components/ConfidentialInput/ConfidentialInput.stories.tsx +6 -0
- package/lib/components/ConfidentialInput/ConfidentialInput.tsx +17 -10
- package/lib/components/ConfidentialInput/Documentation.mdx +12 -4
- package/lib/components/ConfidentialInput/__tests__/ConfidentialInput.test.tsx +76 -9
- package/package.json +1 -1
|
@@ -25,6 +25,7 @@ export interface ConfidentialInputProps extends Omit<
|
|
|
25
25
|
> {
|
|
26
26
|
maskFormat: string | ((value: string) => string);
|
|
27
27
|
showable?: boolean;
|
|
28
|
+
visibleWhenFocused?: boolean;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export type Ref = HTMLInputElement;
|
|
@@ -65,6 +66,7 @@ const ConfidentialInput = forwardRef<Ref, ConfidentialInputProps>(
|
|
|
65
66
|
{
|
|
66
67
|
maskFormat,
|
|
67
68
|
showable = false,
|
|
69
|
+
visibleWhenFocused = true,
|
|
68
70
|
value,
|
|
69
71
|
defaultValue,
|
|
70
72
|
disabled,
|
|
@@ -97,7 +99,9 @@ const ConfidentialInput = forwardRef<Ref, ConfidentialInputProps>(
|
|
|
97
99
|
getStringValue(defaultValue)
|
|
98
100
|
);
|
|
99
101
|
const resolvedDisabled = disabled ?? contextDisabled;
|
|
100
|
-
const
|
|
102
|
+
const valueIsPersistentlyShown = showable && isShown;
|
|
103
|
+
const valueIsVisible =
|
|
104
|
+
(visibleWhenFocused && isFocused) || valueIsPersistentlyShown;
|
|
101
105
|
|
|
102
106
|
const setInputRef = useCallback(
|
|
103
107
|
(element: HTMLInputElement | null) => {
|
|
@@ -142,8 +146,9 @@ const ConfidentialInput = forwardRef<Ref, ConfidentialInputProps>(
|
|
|
142
146
|
typeof maskFormat === 'function'
|
|
143
147
|
? maskFormat(stringValue)
|
|
144
148
|
: applyMaskTemplate(stringValue, maskFormat);
|
|
145
|
-
const
|
|
146
|
-
|
|
149
|
+
const displayedMaskedValue = maskedValue.split('*').join('•');
|
|
150
|
+
const configuredMaskIsVisible =
|
|
151
|
+
!valueIsPersistentlyShown && !isFocused && stringValue.length > 0;
|
|
147
152
|
|
|
148
153
|
const wrapperStyle = cx(
|
|
149
154
|
NAME,
|
|
@@ -215,13 +220,15 @@ const ConfidentialInput = forwardRef<Ref, ConfidentialInputProps>(
|
|
|
215
220
|
type='button'
|
|
216
221
|
disabled={resolvedDisabled}
|
|
217
222
|
aria-label={
|
|
218
|
-
|
|
223
|
+
valueIsPersistentlyShown
|
|
224
|
+
? 'Hide confidential value'
|
|
225
|
+
: 'Show confidential value'
|
|
219
226
|
}
|
|
220
|
-
aria-pressed={
|
|
227
|
+
aria-pressed={valueIsPersistentlyShown}
|
|
221
228
|
testId={`${testId}-visibility-toggle`}
|
|
222
229
|
onClick={() => setIsShown((shown) => !shown)}
|
|
223
230
|
>
|
|
224
|
-
{
|
|
231
|
+
{valueIsPersistentlyShown ? (
|
|
225
232
|
<Icon.EyeOff
|
|
226
233
|
aria-hidden='true'
|
|
227
234
|
testId={`${testId}-hide-icon`}
|
|
@@ -247,22 +254,22 @@ const ConfidentialInput = forwardRef<Ref, ConfidentialInputProps>(
|
|
|
247
254
|
iconPosition={iconPosition}
|
|
248
255
|
iconButton={visibilityButton}
|
|
249
256
|
inputClassName={cx(
|
|
250
|
-
|
|
257
|
+
configuredMaskIsVisible && hiddenInputStyle,
|
|
251
258
|
inputClassName
|
|
252
259
|
)}
|
|
253
260
|
testId={testId}
|
|
254
|
-
type={
|
|
261
|
+
type={valueIsVisible ? 'text' : 'password'}
|
|
255
262
|
onChange={handleChange}
|
|
256
263
|
onFocus={handleFocus}
|
|
257
264
|
onBlur={handleBlur}
|
|
258
265
|
/>
|
|
259
|
-
{
|
|
266
|
+
{configuredMaskIsVisible && (
|
|
260
267
|
<span
|
|
261
268
|
className={maskStyle}
|
|
262
269
|
data-testid={`${testId}-mask`}
|
|
263
270
|
aria-hidden='true'
|
|
264
271
|
>
|
|
265
|
-
{
|
|
272
|
+
{displayedMaskedValue}
|
|
266
273
|
</span>
|
|
267
274
|
)}
|
|
268
275
|
</span>
|
|
@@ -20,10 +20,14 @@ change events, and form submission while displaying a masked value.
|
|
|
20
20
|
## Usage
|
|
21
21
|
|
|
22
22
|
The string mask is a simple template. `1` reveals the corresponding value
|
|
23
|
-
character, `*` masks it
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
the
|
|
23
|
+
character, `*` masks it with a `•`, and all other characters are displayed as
|
|
24
|
+
separators. Asterisks returned by a formatter function are also displayed as
|
|
25
|
+
bullets. Focusing the input temporarily reveals the original value for editing,
|
|
26
|
+
and blurring restores the configured mask. When `showable` is `true`, the Eye
|
|
27
|
+
control lets users keep the original value visible while the input is
|
|
28
|
+
unfocused. Set `visibleWhenFocused` to `false` to use the browser's native
|
|
29
|
+
password mask while editing; this keeps the caret aligned with the underlying
|
|
30
|
+
value. The configured mask returns on blur.
|
|
27
31
|
|
|
28
32
|
<Source
|
|
29
33
|
code={`<ConfidentialInput
|
|
@@ -51,6 +55,9 @@ A function can be supplied when the template syntax is not sufficient:
|
|
|
51
55
|
or formatter.
|
|
52
56
|
- `showable`: **boolean** - Adds a button that toggles the original value.
|
|
53
57
|
Defaults to `false`.
|
|
58
|
+
- `visibleWhenFocused`: **boolean** - Reveals the original value while focused.
|
|
59
|
+
When `false`, the native password mask is shown while editing. Defaults to
|
|
60
|
+
`true`.
|
|
54
61
|
- All `Input` props except `type` and `iconButton` are supported.
|
|
55
62
|
|
|
56
63
|
<ArgTypes />
|
|
@@ -60,3 +67,4 @@ A function can be supplied when the template syntax is not sufficient:
|
|
|
60
67
|
<Canvas of={ConfidentialInputStories.Showable} />
|
|
61
68
|
<Canvas of={ConfidentialInputStories.WithSeparators} />
|
|
62
69
|
<Canvas of={ConfidentialInputStories.FunctionMask} />
|
|
70
|
+
<Canvas of={ConfidentialInputStories.MaskedWhenFocused} />
|
|
@@ -27,7 +27,7 @@ describe('ConfidentialInput', () => {
|
|
|
27
27
|
);
|
|
28
28
|
expect(
|
|
29
29
|
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
30
|
-
).toHaveTextContent('1234
|
|
30
|
+
).toHaveTextContent('1234••••');
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
test('supports separators and masks characters beyond the template', () => {
|
|
@@ -39,19 +39,19 @@ describe('ConfidentialInput', () => {
|
|
|
39
39
|
|
|
40
40
|
expect(
|
|
41
41
|
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
42
|
-
).toHaveTextContent('
|
|
42
|
+
).toHaveTextContent('••-••-72•');
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
test('applies a function mask', () => {
|
|
46
46
|
renderInput({
|
|
47
|
-
maskFormat: (value) =>
|
|
47
|
+
maskFormat: (value) => `**** ending in ${value.slice(-2)}`,
|
|
48
48
|
value: '12345678',
|
|
49
49
|
onChange: () => {},
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
expect(
|
|
53
53
|
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
54
|
-
).toHaveTextContent('ending in 78');
|
|
54
|
+
).toHaveTextContent('•••• ending in 78');
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
test('does not add a visibility control by default', () => {
|
|
@@ -73,7 +73,9 @@ describe('ConfidentialInput', () => {
|
|
|
73
73
|
showable: true,
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
const input = screen.getByTestId(
|
|
76
|
+
const input = screen.getByTestId(
|
|
77
|
+
'ucl-uikit-confidential-input'
|
|
78
|
+
) as HTMLInputElement;
|
|
77
79
|
const showButton = screen.getByRole('button', {
|
|
78
80
|
name: 'Show confidential value',
|
|
79
81
|
});
|
|
@@ -109,6 +111,7 @@ describe('ConfidentialInput', () => {
|
|
|
109
111
|
await user.type(input, '1234');
|
|
110
112
|
|
|
111
113
|
expect(input).toHaveValue('1234');
|
|
114
|
+
expect(input).toHaveAttribute('type', 'text');
|
|
112
115
|
expect(
|
|
113
116
|
screen.queryByTestId('ucl-uikit-confidential-input-mask')
|
|
114
117
|
).not.toBeInTheDocument();
|
|
@@ -117,7 +120,7 @@ describe('ConfidentialInput', () => {
|
|
|
117
120
|
|
|
118
121
|
expect(
|
|
119
122
|
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
120
|
-
).toHaveTextContent('
|
|
123
|
+
).toHaveTextContent('••••');
|
|
121
124
|
expect(onChange).toHaveBeenLastCalledWith(expect.objectContaining({}));
|
|
122
125
|
expect(onChange.mock.lastCall?.[0].target.value).toBe('1234');
|
|
123
126
|
});
|
|
@@ -174,11 +177,41 @@ describe('ConfidentialInput', () => {
|
|
|
174
177
|
});
|
|
175
178
|
});
|
|
176
179
|
|
|
177
|
-
test('
|
|
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 () => {
|
|
178
210
|
const user = userEvent.setup();
|
|
179
211
|
renderInput({
|
|
180
212
|
maskFormat: '**-**-11',
|
|
181
213
|
defaultValue: '123472',
|
|
214
|
+
visibleWhenFocused: false,
|
|
182
215
|
});
|
|
183
216
|
|
|
184
217
|
const input = screen.getByTestId(
|
|
@@ -186,7 +219,7 @@ describe('ConfidentialInput', () => {
|
|
|
186
219
|
) as HTMLInputElement;
|
|
187
220
|
expect(
|
|
188
221
|
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
189
|
-
).toHaveTextContent('
|
|
222
|
+
).toHaveTextContent('••-••-72');
|
|
190
223
|
|
|
191
224
|
await user.click(input);
|
|
192
225
|
|
|
@@ -201,7 +234,41 @@ describe('ConfidentialInput', () => {
|
|
|
201
234
|
|
|
202
235
|
expect(
|
|
203
236
|
screen.getByTestId('ucl-uikit-confidential-input-mask')
|
|
204
|
-
).toHaveTextContent('
|
|
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');
|
|
205
272
|
});
|
|
206
273
|
|
|
207
274
|
test('supports a right icon alongside the visibility control', () => {
|