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.
@@ -43,3 +43,9 @@ export const FunctionMask: Story = {
43
43
  showable: true,
44
44
  },
45
45
  };
46
+
47
+ export const MaskedWhenFocused: Story = {
48
+ args: {
49
+ visibleWhenFocused: false,
50
+ },
51
+ };
@@ -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 valueIsShown = showable && isShown;
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 visuallyHidden =
146
- !valueIsShown && !isFocused && stringValue.length > 0;
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
- valueIsShown ? 'Hide confidential value' : 'Show confidential value'
223
+ valueIsPersistentlyShown
224
+ ? 'Hide confidential value'
225
+ : 'Show confidential value'
219
226
  }
220
- aria-pressed={valueIsShown}
227
+ aria-pressed={valueIsPersistentlyShown}
221
228
  testId={`${testId}-visibility-toggle`}
222
229
  onClick={() => setIsShown((shown) => !shown)}
223
230
  >
224
- {valueIsShown ? (
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
- visuallyHidden && hiddenInputStyle,
257
+ configuredMaskIsVisible && hiddenInputStyle,
251
258
  inputClassName
252
259
  )}
253
260
  testId={testId}
254
- type={valueIsShown ? 'text' : 'password'}
261
+ type={valueIsVisible ? 'text' : 'password'}
255
262
  onChange={handleChange}
256
263
  onFocus={handleFocus}
257
264
  onBlur={handleBlur}
258
265
  />
259
- {visuallyHidden && (
266
+ {configuredMaskIsVisible && (
260
267
  <span
261
268
  className={maskStyle}
262
269
  data-testid={`${testId}-mask`}
263
270
  aria-hidden='true'
264
271
  >
265
- {maskedValue}
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, and all other characters are displayed as separators.
24
- The configured mask is displayed while the input is unfocused. While editing,
25
- the native password mask is used so the caret and selection remain aligned with
26
- the underlying value.
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('**-**-72*');
42
+ ).toHaveTextContent('••-••-72');
43
43
  });
44
44
 
45
45
  test('applies a function mask', () => {
46
46
  renderInput({
47
- maskFormat: (value) => `ending in ${value.slice(-2)}`,
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('ucl-uikit-confidential-input');
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('uses native password rendering while editing formatted values', async () => {
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('**-**-72');
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('**-**-72');
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', () => {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "uikit-react-public",
3
3
  "private": false,
4
4
  "license": "UNLICENSED",
5
- "version": "0.48.0",
5
+ "version": "0.48.1",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",