uikit-react-public 0.49.0 → 0.49.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.
Files changed (25) hide show
  1. package/dist/components/DisplayField/DisplayField.d.ts +3 -0
  2. package/dist/components/DisplayField/DisplayField.stories.d.ts +3 -0
  3. package/dist/components/Input/Input.d.ts +2 -0
  4. package/dist/components/Input/Input.stories.d.ts +1 -0
  5. package/dist/components/common/formatDisplayValue.d.ts +3 -0
  6. package/dist/index.js +4974 -4894
  7. package/lib/components/ConfidentialDisplayField/ConfidentialDisplayField.stories.tsx +3 -2
  8. package/lib/components/ConfidentialDisplayField/ConfidentialDisplayField.tsx +2 -0
  9. package/lib/components/ConfidentialDisplayField/Documentation.mdx +6 -3
  10. package/lib/components/ConfidentialDisplayField/__tests__/ConfidentialDisplayField.test.tsx +25 -6
  11. package/lib/components/ConfidentialInput/ConfidentialInput.stories.tsx +2 -2
  12. package/lib/components/ConfidentialInput/ConfidentialInput.tsx +13 -5
  13. package/lib/components/ConfidentialInput/Documentation.mdx +4 -2
  14. package/lib/components/ConfidentialInput/__tests__/ConfidentialInput.test.tsx +33 -7
  15. package/lib/components/DisplayField/DisplayField.stories.tsx +22 -0
  16. package/lib/components/DisplayField/DisplayField.tsx +20 -8
  17. package/lib/components/DisplayField/Documentation.mdx +9 -0
  18. package/lib/components/DisplayField/__tests__/DisplayField.test.tsx +36 -1
  19. package/lib/components/Input/Documentation.mdx +11 -0
  20. package/lib/components/Input/Input.stories.tsx +7 -0
  21. package/lib/components/Input/Input.tsx +134 -2
  22. package/lib/components/Input/__tests__/Input.test.tsx +96 -1
  23. package/lib/components/common/formatConfidentialValue.ts +1 -1
  24. package/lib/components/common/formatDisplayValue.ts +29 -0
  25. package/package.json +1 -1
@@ -10,7 +10,7 @@ const meta = {
10
10
  tags: ['autodocs'],
11
11
  args: {
12
12
  value: '12345678',
13
- maskFormat: '******11',
13
+ maskFormat: '******##',
14
14
  },
15
15
  } satisfies Meta<typeof ConfidentialDisplayField>;
16
16
 
@@ -29,7 +29,8 @@ export const Showable: Story = {
29
29
  export const WithSeparators: Story = {
30
30
  args: {
31
31
  value: '123472',
32
- maskFormat: '**-**-11',
32
+ maskFormat: '****##',
33
+ format: '##-##-##',
33
34
  showable: true,
34
35
  },
35
36
  };
@@ -22,6 +22,7 @@ const ConfidentialDisplayField = forwardRef<Ref, ConfidentialDisplayFieldProps>(
22
22
  (
23
23
  {
24
24
  value,
25
+ numeric,
25
26
  maskFormat,
26
27
  showable = false,
27
28
  testId = NAME,
@@ -67,6 +68,7 @@ const ConfidentialDisplayField = forwardRef<Ref, ConfidentialDisplayFieldProps>(
67
68
  {...props}
68
69
  ref={ref}
69
70
  value={displayedValue}
71
+ numeric={numeric ?? typeof value === 'number'}
70
72
  testId={testId}
71
73
  noMargins
72
74
  />
@@ -13,13 +13,14 @@ import {
13
13
  <Subtitle>A DisplayField that masks confidential values</Subtitle>
14
14
 
15
15
  `ConfidentialDisplayField` extends `DisplayField` with the same simple mask
16
- format used by `ConfidentialInput`. `1` reveals a value character, `*` masks it
16
+ format used by `ConfidentialInput`. `#` reveals a value character, `*` masks it
17
17
  with a `•`, and other characters are displayed as separators.
18
18
 
19
19
  <Source
20
20
  code={`<ConfidentialDisplayField
21
- value='12345678'
22
- maskFormat='******11'
21
+ value='123472'
22
+ maskFormat='****##'
23
+ format='##-##-##'
23
24
  showable
24
25
  />`}
25
26
  />
@@ -31,6 +32,8 @@ with a `•`, and other characters are displayed as separators.
31
32
  or formatter.
32
33
  - `showable`: **boolean** - Adds a button that toggles the original value.
33
34
  Defaults to `false`.
35
+ - `format`: **string | (value: string) => string** - Formats both the masked
36
+ and visible values using the `DisplayField` format syntax.
34
37
  - All `DisplayField` props are supported.
35
38
 
36
39
  <ArgTypes />
@@ -18,7 +18,7 @@ describe('ConfidentialDisplayField', () => {
18
18
  test('applies a string mask', () => {
19
19
  renderDisplayField({
20
20
  value: '12345678',
21
- maskFormat: '******11',
21
+ maskFormat: '******##',
22
22
  });
23
23
 
24
24
  expect(
@@ -40,7 +40,7 @@ describe('ConfidentialDisplayField', () => {
40
40
  test('does not add a visibility control by default', () => {
41
41
  renderDisplayField({
42
42
  value: '12345678',
43
- maskFormat: '******11',
43
+ maskFormat: '******##',
44
44
  });
45
45
 
46
46
  expect(
@@ -52,7 +52,7 @@ describe('ConfidentialDisplayField', () => {
52
52
  const user = userEvent.setup();
53
53
  renderDisplayField({
54
54
  value: '12345678',
55
- maskFormat: '******11',
55
+ maskFormat: '******##',
56
56
  showable: true,
57
57
  });
58
58
 
@@ -72,6 +72,25 @@ describe('ConfidentialDisplayField', () => {
72
72
  expect(output).toHaveTextContent('••••••78');
73
73
  });
74
74
 
75
+ test('formats both masked and visible values', async () => {
76
+ const user = userEvent.setup();
77
+ renderDisplayField({
78
+ value: '123472',
79
+ maskFormat: '****##',
80
+ format: '##-##-##',
81
+ showable: true,
82
+ });
83
+
84
+ const output = screen.getByTestId('ucl-uikit-confidential-display-field');
85
+ expect(output).toHaveTextContent('••-••-72');
86
+
87
+ await user.click(
88
+ screen.getByRole('button', { name: 'Show confidential value' })
89
+ );
90
+
91
+ expect(output).toHaveTextContent('12-34-72');
92
+ });
93
+
75
94
  test('forwards its ref to the output', () => {
76
95
  const ref = createRef<HTMLOutputElement>();
77
96
  render(
@@ -79,7 +98,7 @@ describe('ConfidentialDisplayField', () => {
79
98
  <ConfidentialDisplayField
80
99
  ref={ref}
81
100
  value='12345678'
82
- maskFormat='******11'
101
+ maskFormat='******##'
83
102
  />
84
103
  </ThemeContextProvider>
85
104
  );
@@ -95,7 +114,7 @@ describe('ConfidentialDisplayField', () => {
95
114
  <ThemeContextProvider>
96
115
  <ConfidentialDisplayField
97
116
  value='12345678'
98
- maskFormat='******11'
117
+ maskFormat='******##'
99
118
  showable
100
119
  />
101
120
  </ThemeContextProvider>
@@ -112,7 +131,7 @@ describe('ConfidentialDisplayField', () => {
112
131
  <ThemeContextProvider>
113
132
  <ConfidentialDisplayField
114
133
  value='12345678'
115
- maskFormat='******11'
134
+ maskFormat='******##'
116
135
  showable={false}
117
136
  />
118
137
  </ThemeContextProvider>
@@ -11,7 +11,7 @@ const meta = {
11
11
  args: {
12
12
  'aria-label': 'Account number',
13
13
  defaultValue: '12345678',
14
- maskFormat: '****1111',
14
+ maskFormat: '****####',
15
15
  },
16
16
  } satisfies Meta<typeof ConfidentialInput>;
17
17
 
@@ -31,7 +31,7 @@ export const WithSeparators: Story = {
31
31
  args: {
32
32
  'aria-label': 'Sort code',
33
33
  defaultValue: '123472',
34
- maskFormat: '**-**-11',
34
+ maskFormat: '**-**-##',
35
35
  showable: true,
36
36
  },
37
37
  };
@@ -19,6 +19,7 @@ import marginsStyle from '../common/marginsStyle';
19
19
  import formatConfidentialValue, {
20
20
  MaskFormat,
21
21
  } from '../common/formatConfidentialValue';
22
+ import formatDisplayValue from '../common/formatDisplayValue';
22
23
 
23
24
  export const NAME = 'ucl-uikit-confidential-input';
24
25
 
@@ -46,6 +47,7 @@ const ConfidentialInput = forwardRef<Ref, ConfidentialInputProps>(
46
47
  maskFormat,
47
48
  showable = false,
48
49
  visibleWhenFocused = true,
50
+ format,
49
51
  value,
50
52
  defaultValue,
51
53
  disabled,
@@ -125,8 +127,14 @@ const ConfidentialInput = forwardRef<Ref, ConfidentialInputProps>(
125
127
  stringValue,
126
128
  maskFormat
127
129
  );
128
- const configuredMaskIsVisible =
129
- !valueIsPersistentlyShown && !isFocused && stringValue.length > 0;
130
+ const configuredValueIsVisible =
131
+ !isFocused &&
132
+ stringValue.length > 0 &&
133
+ (!valueIsPersistentlyShown || Boolean(format));
134
+ const configuredValue = formatDisplayValue(
135
+ valueIsPersistentlyShown ? stringValue : displayedMaskedValue,
136
+ format
137
+ );
130
138
 
131
139
  const wrapperStyle = cx(
132
140
  NAME,
@@ -232,7 +240,7 @@ const ConfidentialInput = forwardRef<Ref, ConfidentialInputProps>(
232
240
  iconPosition={iconPosition}
233
241
  iconButton={visibilityButton}
234
242
  inputClassName={cx(
235
- configuredMaskIsVisible && hiddenInputStyle,
243
+ configuredValueIsVisible && hiddenInputStyle,
236
244
  inputClassName
237
245
  )}
238
246
  testId={testId}
@@ -241,13 +249,13 @@ const ConfidentialInput = forwardRef<Ref, ConfidentialInputProps>(
241
249
  onFocus={handleFocus}
242
250
  onBlur={handleBlur}
243
251
  />
244
- {configuredMaskIsVisible && (
252
+ {configuredValueIsVisible && (
245
253
  <span
246
254
  className={maskStyle}
247
255
  data-testid={`${testId}-mask`}
248
256
  aria-hidden='true'
249
257
  >
250
- {displayedMaskedValue}
258
+ {configuredValue}
251
259
  </span>
252
260
  )}
253
261
  </span>
@@ -19,7 +19,7 @@ change events, and form submission while displaying a masked value.
19
19
 
20
20
  ## Usage
21
21
 
22
- The string mask is a simple template. `1` reveals the corresponding value
22
+ The string mask is a simple template. `#` reveals the corresponding value
23
23
  character, `*` masks it with a `•`, and all other characters are displayed as
24
24
  separators. Asterisks returned by a formatter function are also displayed as
25
25
  bullets. Focusing the input temporarily reveals the original value for editing,
@@ -32,7 +32,7 @@ value. The configured mask returns on blur.
32
32
  <Source
33
33
  code={`<ConfidentialInput
34
34
  defaultValue='12345678'
35
- maskFormat='****1111'
35
+ maskFormat='****####'
36
36
  showable
37
37
  aria-label='Account number'
38
38
  />`}
@@ -58,6 +58,8 @@ A function can be supplied when the template syntax is not sufficient:
58
58
  - `visibleWhenFocused`: **boolean** - Reveals the original value while focused.
59
59
  When `false`, the native password mask is shown while editing. Defaults to
60
60
  `true`.
61
+ - `format`: **string | (value: string) => string** - Formats both the masked
62
+ and visible values while blurred using the `Input` format syntax.
61
63
  - All `Input` props except `type` and `iconButton` are supported.
62
64
 
63
65
  <ArgTypes />
@@ -17,7 +17,7 @@ const renderInput = (props: React.ComponentProps<typeof ConfidentialInput>) =>
17
17
  describe('ConfidentialInput', () => {
18
18
  test('applies a string mask without changing the input value', () => {
19
19
  renderInput({
20
- maskFormat: '1111****',
20
+ maskFormat: '####****',
21
21
  value: '12345678',
22
22
  onChange: () => {},
23
23
  });
@@ -32,7 +32,7 @@ describe('ConfidentialInput', () => {
32
32
 
33
33
  test('supports separators and masks characters beyond the template', () => {
34
34
  renderInput({
35
- maskFormat: '**-**-11',
35
+ maskFormat: '**-**-##',
36
36
  value: '1234729',
37
37
  onChange: () => {},
38
38
  });
@@ -68,7 +68,7 @@ describe('ConfidentialInput', () => {
68
68
  test('toggles between the masked and shown value', async () => {
69
69
  const user = userEvent.setup();
70
70
  renderInput({
71
- maskFormat: '****1111',
71
+ maskFormat: '****####',
72
72
  defaultValue: '12345678',
73
73
  showable: true,
74
74
  });
@@ -131,7 +131,7 @@ describe('ConfidentialInput', () => {
131
131
  <form data-testid='form'>
132
132
  <ConfidentialInput
133
133
  name='accountNumber'
134
- maskFormat='****1111'
134
+ maskFormat='****####'
135
135
  defaultValue='12345678'
136
136
  />
137
137
  </form>
@@ -180,7 +180,7 @@ describe('ConfidentialInput', () => {
180
180
  test('shows the raw value while editing a non-showable value', async () => {
181
181
  const user = userEvent.setup();
182
182
  renderInput({
183
- maskFormat: '**-**-11',
183
+ maskFormat: '**-**-##',
184
184
  defaultValue: '123472',
185
185
  });
186
186
 
@@ -209,7 +209,7 @@ describe('ConfidentialInput', () => {
209
209
  test('uses native password masking when visibleWhenFocused is false', async () => {
210
210
  const user = userEvent.setup();
211
211
  renderInput({
212
- maskFormat: '**-**-11',
212
+ maskFormat: '**-**-##',
213
213
  defaultValue: '123472',
214
214
  visibleWhenFocused: false,
215
215
  });
@@ -240,7 +240,7 @@ describe('ConfidentialInput', () => {
240
240
  test('shows the raw value while editing a showable value', async () => {
241
241
  const user = userEvent.setup();
242
242
  renderInput({
243
- maskFormat: '**-**-11',
243
+ maskFormat: '**-**-##',
244
244
  defaultValue: '123472',
245
245
  showable: true,
246
246
  });
@@ -289,6 +289,32 @@ describe('ConfidentialInput', () => {
289
289
  ).toHaveStyle({ right: '16px' });
290
290
  });
291
291
 
292
+ test('formats both masked and visible values while blurred', async () => {
293
+ const user = userEvent.setup();
294
+ renderInput({
295
+ value: '123472',
296
+ maskFormat: '****##',
297
+ format: '##-##-##',
298
+ showable: true,
299
+ onChange: () => {},
300
+ });
301
+
302
+ expect(
303
+ screen.getByTestId('ucl-uikit-confidential-input-mask')
304
+ ).toHaveTextContent('••-••-72');
305
+
306
+ await user.click(
307
+ screen.getByRole('button', { name: 'Show confidential value' })
308
+ );
309
+
310
+ expect(
311
+ screen.getByTestId('ucl-uikit-confidential-input-mask')
312
+ ).toHaveTextContent('12-34-72');
313
+ expect(screen.getByTestId('ucl-uikit-confidential-input')).toHaveValue(
314
+ '123472'
315
+ );
316
+ });
317
+
292
318
  test('disables the visibility control with the input', () => {
293
319
  renderInput({
294
320
  maskFormat: '****',
@@ -24,3 +24,25 @@ export const Number: Story = {
24
24
  value: 12345678,
25
25
  },
26
26
  };
27
+
28
+ export const NumericString: Story = {
29
+ args: {
30
+ value: '00123456',
31
+ numeric: true,
32
+ },
33
+ };
34
+
35
+ export const StringFormat: Story = {
36
+ args: {
37
+ value: '112233',
38
+ numeric: true,
39
+ format: '##-##-##',
40
+ },
41
+ };
42
+
43
+ export const FunctionFormat: Story = {
44
+ args: {
45
+ value: 1234.5,
46
+ format: (value) => `£${value}`,
47
+ },
48
+ };
@@ -3,6 +3,9 @@ import { css, cx } from '@emotion/css';
3
3
  import useTheme from '../../theme/useTheme';
4
4
  import { FieldContext } from '../Field';
5
5
  import marginsStyle, { MarginProps } from '../common/marginsStyle';
6
+ import formatDisplayValue, {
7
+ DisplayValueFormat,
8
+ } from '../common/formatDisplayValue';
6
9
 
7
10
  export const NAME = 'ucl-uikit-display-field';
8
11
 
@@ -11,6 +14,8 @@ export interface DisplayFieldBaseProps extends Omit<
11
14
  'children' | 'value'
12
15
  > {
13
16
  value: string | number;
17
+ numeric?: boolean;
18
+ format?: DisplayValueFormat;
14
19
  testId?: string;
15
20
  }
16
21
 
@@ -19,21 +24,28 @@ export type DisplayFieldProps = DisplayFieldBaseProps & MarginProps;
19
24
  export type Ref = HTMLOutputElement;
20
25
 
21
26
  const DisplayField = forwardRef<Ref, DisplayFieldProps>(
22
- ({ value, testId = NAME, className, ...props }, ref) => {
27
+ (
28
+ { value, numeric = false, format, testId = NAME, className, ...props },
29
+ ref
30
+ ) => {
23
31
  const [theme] = useTheme();
24
32
  const { id: contextId } = use(FieldContext);
25
33
  const id = props.id ?? contextId;
26
- const { md } = theme.typography.body;
34
+ const useNumericTypography = typeof value === 'number' || numeric;
35
+ const typography = useNumericTypography
36
+ ? theme.typography.body.mdNumeric
37
+ : theme.typography.body.md;
38
+ const displayedValue = formatDisplayValue(String(value), format);
27
39
 
28
40
  const baseStyle = css`
29
41
  display: block;
30
42
  min-height: 24px;
31
43
  color: ${theme.colour.text.default};
32
- font-family: ${md.fontFamily};
33
- font-feature-settings: ${md.fontSettings};
34
- font-size: ${md.fontSize}px;
35
- font-weight: ${md.fontWeight};
36
- line-height: ${md.lineHeight}%;
44
+ font-family: ${typography.fontFamily};
45
+ font-feature-settings: ${typography.fontSettings};
46
+ font-size: ${typography.fontSize}px;
47
+ font-weight: ${typography.fontWeight};
48
+ line-height: ${typography.lineHeight}%;
37
49
  overflow-wrap: anywhere;
38
50
  `;
39
51
 
@@ -47,7 +59,7 @@ const DisplayField = forwardRef<Ref, DisplayFieldProps>(
47
59
  className={style}
48
60
  data-testid={testId}
49
61
  >
50
- {value}
62
+ {displayedValue}
51
63
  </output>
52
64
  );
53
65
  }
@@ -25,6 +25,12 @@ to display a record in the same field structure used by editable forms.
25
25
  ## Props
26
26
 
27
27
  - `value`: **string | number** - The value to display.
28
+ - `numeric`: **boolean** - Applies numeric typography to string values. Number
29
+ values use numeric typography automatically. Defaults to `false`.
30
+ - `format`: **string | (value: string) => string** - Formats the displayed
31
+ value. In a string template, each `#` consumes one value character and other
32
+ characters are displayed literally. For example, `##-##-##` formats `112233`
33
+ as `11-22-33`.
28
34
  - All standard HTML `<output>` props are supported.
29
35
 
30
36
  <ArgTypes />
@@ -33,3 +39,6 @@ to display a record in the same field structure used by editable forms.
33
39
 
34
40
  <Canvas of={DisplayFieldStories.Text} />
35
41
  <Canvas of={DisplayFieldStories.Number} />
42
+ <Canvas of={DisplayFieldStories.NumericString} />
43
+ <Canvas of={DisplayFieldStories.StringFormat} />
44
+ <Canvas of={DisplayFieldStories.FunctionFormat} />
@@ -25,8 +25,43 @@ describe('DisplayField', () => {
25
25
  test('displays a numeric value', () => {
26
26
  renderDisplayField({ value: 12345678 });
27
27
 
28
+ const output = screen.getByTestId('ucl-uikit-display-field');
29
+ expect(output).toHaveTextContent('12345678');
30
+ expect(getComputedStyle(output).fontFamily).toContain('DM Mono');
31
+ });
32
+
33
+ test('uses numeric typography for a string when numeric is true', () => {
34
+ renderDisplayField({ value: '00123456', numeric: true });
35
+
36
+ expect(
37
+ getComputedStyle(screen.getByTestId('ucl-uikit-display-field')).fontFamily
38
+ ).toContain('DM Mono');
39
+ });
40
+
41
+ test('formats a value with a string template', () => {
42
+ renderDisplayField({ value: '112233', format: '##-##-##' });
43
+
44
+ expect(screen.getByTestId('ucl-uikit-display-field')).toHaveTextContent(
45
+ '11-22-33'
46
+ );
47
+ });
48
+
49
+ test('appends value characters beyond a string template', () => {
50
+ renderDisplayField({ value: '11223344', format: '##-##-##' });
51
+
52
+ expect(screen.getByTestId('ucl-uikit-display-field')).toHaveTextContent(
53
+ '11-22-3344'
54
+ );
55
+ });
56
+
57
+ test('formats a value with a function', () => {
58
+ renderDisplayField({
59
+ value: 1234.5,
60
+ format: (value) => `£${value}`,
61
+ });
62
+
28
63
  expect(screen.getByTestId('ucl-uikit-display-field')).toHaveTextContent(
29
- '12345678'
64
+ '£1234.5'
30
65
  );
31
66
  });
32
67
 
@@ -17,6 +17,7 @@ export const usage = {
17
17
  rightIcon: `<Input icon={<Icon.Settings />} iconPosition='right' type='password' />`,
18
18
  password: `<Input type='password' />`,
19
19
  email: `<Input type='email' icon={<Icon.Facebook />} iconPosition='left' />`,
20
+ formatted: `<Input defaultValue='112233' format='##-##-##' />`,
20
21
  };
21
22
 
22
23
  <Meta of={InputStories} />
@@ -47,6 +48,10 @@ Use the Input component wherever standard inputs are required with optional enha
47
48
  - `type`: **string** – Input type (e.g. `text`, `password`, `email`, etc.)
48
49
  - `disabled`: **boolean** – Disables the input field
49
50
  - `placeholder`: **string** – Placeholder text
51
+ - `format`: **string | (value: string) => string** – Formats the displayed
52
+ value while the input is blurred. `#` consumes one raw value character;
53
+ other template characters are displayed literally. The raw value is shown
54
+ while focused and remains unchanged for events and form submission.
50
55
  - All standard HTML `<input>` props are supported
51
56
 
52
57
  # Variants
@@ -92,3 +97,9 @@ Use the Input component wherever standard inputs are required with optional enha
92
97
  sourceState='shown'
93
98
  source={{ code: usage.email }}
94
99
  />
100
+
101
+ <Canvas
102
+ of={InputStories.Formatted}
103
+ sourceState='shown'
104
+ source={{ code: usage.formatted }}
105
+ />
@@ -58,3 +58,10 @@ export const Email: Story = {
58
58
  iconPosition: 'left',
59
59
  },
60
60
  };
61
+
62
+ export const Formatted: Story = {
63
+ args: {
64
+ defaultValue: '112233',
65
+ format: '##-##-##',
66
+ },
67
+ };