uikit-react-public 0.48.1 → 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.
- package/dist/components/ConfidentialDisplayField/ConfidentialDisplayField.d.ts +10 -0
- package/dist/components/ConfidentialDisplayField/ConfidentialDisplayField.stories.d.ts +19 -0
- package/dist/components/ConfidentialDisplayField/__tests__/ConfidentialDisplayField.test.d.ts +1 -0
- package/dist/components/ConfidentialDisplayField/index.d.ts +2 -0
- package/dist/components/ConfidentialInput/ConfidentialInput.d.ts +2 -1
- package/dist/components/DisplayField/DisplayField.d.ts +14 -0
- package/dist/components/DisplayField/DisplayField.stories.d.ts +19 -0
- package/dist/components/DisplayField/__tests__/DisplayField.test.d.ts +1 -0
- package/dist/components/DisplayField/index.d.ts +2 -0
- package/dist/components/Input/Input.d.ts +2 -0
- package/dist/components/Input/Input.stories.d.ts +1 -0
- package/dist/components/common/formatConfidentialValue.d.ts +3 -0
- package/dist/components/common/formatDisplayValue.d.ts +3 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/index.js +5941 -5764
- package/lib/components/ConfidentialDisplayField/ConfidentialDisplayField.stories.tsx +43 -0
- package/lib/components/ConfidentialDisplayField/ConfidentialDisplayField.tsx +105 -0
- package/lib/components/ConfidentialDisplayField/Documentation.mdx +46 -0
- package/lib/components/ConfidentialDisplayField/__tests__/ConfidentialDisplayField.test.tsx +144 -0
- package/lib/components/ConfidentialDisplayField/index.ts +2 -0
- package/lib/components/ConfidentialInput/ConfidentialInput.stories.tsx +2 -2
- package/lib/components/ConfidentialInput/ConfidentialInput.tsx +21 -35
- package/lib/components/ConfidentialInput/Documentation.mdx +4 -2
- package/lib/components/ConfidentialInput/__tests__/ConfidentialInput.test.tsx +33 -7
- package/lib/components/DisplayField/DisplayField.stories.tsx +48 -0
- package/lib/components/DisplayField/DisplayField.tsx +68 -0
- package/lib/components/DisplayField/Documentation.mdx +44 -0
- package/lib/components/DisplayField/__tests__/DisplayField.test.tsx +96 -0
- package/lib/components/DisplayField/index.ts +2 -0
- package/lib/components/Input/Documentation.mdx +11 -0
- package/lib/components/Input/Input.stories.tsx +7 -0
- package/lib/components/Input/Input.tsx +134 -2
- package/lib/components/Input/__tests__/Input.test.tsx +96 -1
- package/lib/components/common/formatConfidentialValue.ts +36 -0
- package/lib/components/common/formatDisplayValue.ts +29 -0
- package/lib/components/index.ts +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import ConfidentialDisplayField from './ConfidentialDisplayField';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Components/ConfidentialDisplayField',
|
|
6
|
+
component: ConfidentialDisplayField,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'centered',
|
|
9
|
+
},
|
|
10
|
+
tags: ['autodocs'],
|
|
11
|
+
args: {
|
|
12
|
+
value: '12345678',
|
|
13
|
+
maskFormat: '******##',
|
|
14
|
+
},
|
|
15
|
+
} satisfies Meta<typeof ConfidentialDisplayField>;
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
|
|
19
|
+
type Story = StoryObj<typeof meta>;
|
|
20
|
+
|
|
21
|
+
export const Default: Story = {};
|
|
22
|
+
|
|
23
|
+
export const Showable: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
showable: true,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const WithSeparators: Story = {
|
|
30
|
+
args: {
|
|
31
|
+
value: '123472',
|
|
32
|
+
maskFormat: '****##',
|
|
33
|
+
format: '##-##-##',
|
|
34
|
+
showable: true,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const FunctionMask: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
maskFormat: (value) =>
|
|
41
|
+
`${'*'.repeat(Math.max(0, value.length - 2))}${value.slice(-2)}`,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { forwardRef, memo, useEffect, useState } from 'react';
|
|
2
|
+
import { css, cx } from '@emotion/css';
|
|
3
|
+
import useTheme from '../../theme/useTheme';
|
|
4
|
+
import DisplayField, { DisplayFieldProps } from '../DisplayField';
|
|
5
|
+
import Icon from '../Icon';
|
|
6
|
+
import IconButton from '../IconButton';
|
|
7
|
+
import formatConfidentialValue, {
|
|
8
|
+
MaskFormat,
|
|
9
|
+
} from '../common/formatConfidentialValue';
|
|
10
|
+
import marginsStyle from '../common/marginsStyle';
|
|
11
|
+
|
|
12
|
+
export const NAME = 'ucl-uikit-confidential-display-field';
|
|
13
|
+
|
|
14
|
+
export interface ConfidentialDisplayFieldProps extends DisplayFieldProps {
|
|
15
|
+
maskFormat: MaskFormat;
|
|
16
|
+
showable?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type Ref = HTMLOutputElement;
|
|
20
|
+
|
|
21
|
+
const ConfidentialDisplayField = forwardRef<Ref, ConfidentialDisplayFieldProps>(
|
|
22
|
+
(
|
|
23
|
+
{
|
|
24
|
+
value,
|
|
25
|
+
numeric,
|
|
26
|
+
maskFormat,
|
|
27
|
+
showable = false,
|
|
28
|
+
testId = NAME,
|
|
29
|
+
className,
|
|
30
|
+
m,
|
|
31
|
+
mv,
|
|
32
|
+
mh,
|
|
33
|
+
mt,
|
|
34
|
+
mb,
|
|
35
|
+
ml,
|
|
36
|
+
mr,
|
|
37
|
+
noMargins,
|
|
38
|
+
...props
|
|
39
|
+
},
|
|
40
|
+
ref
|
|
41
|
+
) => {
|
|
42
|
+
const [theme] = useTheme();
|
|
43
|
+
const [isShown, setIsShown] = useState(false);
|
|
44
|
+
const stringValue = String(value);
|
|
45
|
+
const valueIsShown = showable && isShown;
|
|
46
|
+
const displayedValue = valueIsShown
|
|
47
|
+
? stringValue
|
|
48
|
+
: formatConfidentialValue(stringValue, maskFormat);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!showable) setIsShown(false);
|
|
52
|
+
}, [showable]);
|
|
53
|
+
|
|
54
|
+
const wrapperStyle = cx(
|
|
55
|
+
NAME,
|
|
56
|
+
css`
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: ${theme.padding.p8};
|
|
60
|
+
`,
|
|
61
|
+
marginsStyle({ m, mv, mh, mt, mb, ml, mr, noMargins }, theme),
|
|
62
|
+
className
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<span className={wrapperStyle}>
|
|
67
|
+
<DisplayField
|
|
68
|
+
{...props}
|
|
69
|
+
ref={ref}
|
|
70
|
+
value={displayedValue}
|
|
71
|
+
numeric={numeric ?? typeof value === 'number'}
|
|
72
|
+
testId={testId}
|
|
73
|
+
noMargins
|
|
74
|
+
/>
|
|
75
|
+
{showable && (
|
|
76
|
+
<IconButton
|
|
77
|
+
type='button'
|
|
78
|
+
aria-label={
|
|
79
|
+
valueIsShown
|
|
80
|
+
? 'Hide confidential value'
|
|
81
|
+
: 'Show confidential value'
|
|
82
|
+
}
|
|
83
|
+
aria-pressed={valueIsShown}
|
|
84
|
+
testId={`${testId}-visibility-toggle`}
|
|
85
|
+
onClick={() => setIsShown((shown) => !shown)}
|
|
86
|
+
>
|
|
87
|
+
{valueIsShown ? (
|
|
88
|
+
<Icon.EyeOff
|
|
89
|
+
aria-hidden='true'
|
|
90
|
+
testId={`${testId}-hide-icon`}
|
|
91
|
+
/>
|
|
92
|
+
) : (
|
|
93
|
+
<Icon.Eye
|
|
94
|
+
aria-hidden='true'
|
|
95
|
+
testId={`${testId}-show-icon`}
|
|
96
|
+
/>
|
|
97
|
+
)}
|
|
98
|
+
</IconButton>
|
|
99
|
+
)}
|
|
100
|
+
</span>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
export default memo(ConfidentialDisplayField);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as ConfidentialDisplayFieldStories from './ConfidentialDisplayField.stories';
|
|
2
|
+
import {
|
|
3
|
+
Meta,
|
|
4
|
+
Title,
|
|
5
|
+
Subtitle,
|
|
6
|
+
Canvas,
|
|
7
|
+
Source,
|
|
8
|
+
ArgTypes,
|
|
9
|
+
} from '@storybook/addon-docs/blocks';
|
|
10
|
+
|
|
11
|
+
<Meta of={ConfidentialDisplayFieldStories} />
|
|
12
|
+
<Title />
|
|
13
|
+
<Subtitle>A DisplayField that masks confidential values</Subtitle>
|
|
14
|
+
|
|
15
|
+
`ConfidentialDisplayField` extends `DisplayField` with the same simple mask
|
|
16
|
+
format used by `ConfidentialInput`. `#` reveals a value character, `*` masks it
|
|
17
|
+
with a `•`, and other characters are displayed as separators.
|
|
18
|
+
|
|
19
|
+
<Source
|
|
20
|
+
code={`<ConfidentialDisplayField
|
|
21
|
+
value='123472'
|
|
22
|
+
maskFormat='****##'
|
|
23
|
+
format='##-##-##'
|
|
24
|
+
showable
|
|
25
|
+
/>`}
|
|
26
|
+
/>
|
|
27
|
+
|
|
28
|
+
## Props
|
|
29
|
+
|
|
30
|
+
- `value`: **string | number** - The original value.
|
|
31
|
+
- `maskFormat`: **string | (value: string) => string** - Required mask template
|
|
32
|
+
or formatter.
|
|
33
|
+
- `showable`: **boolean** - Adds a button that toggles the original value.
|
|
34
|
+
Defaults to `false`.
|
|
35
|
+
- `format`: **string | (value: string) => string** - Formats both the masked
|
|
36
|
+
and visible values using the `DisplayField` format syntax.
|
|
37
|
+
- All `DisplayField` props are supported.
|
|
38
|
+
|
|
39
|
+
<ArgTypes />
|
|
40
|
+
|
|
41
|
+
## Examples
|
|
42
|
+
|
|
43
|
+
<Canvas of={ConfidentialDisplayFieldStories.Default} />
|
|
44
|
+
<Canvas of={ConfidentialDisplayFieldStories.Showable} />
|
|
45
|
+
<Canvas of={ConfidentialDisplayFieldStories.WithSeparators} />
|
|
46
|
+
<Canvas of={ConfidentialDisplayFieldStories.FunctionMask} />
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { createRef } from 'react';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import { ThemeContextProvider } from '../../../theme/useTheme';
|
|
6
|
+
import ConfidentialDisplayField from '../ConfidentialDisplayField';
|
|
7
|
+
|
|
8
|
+
const renderDisplayField = (
|
|
9
|
+
props: React.ComponentProps<typeof ConfidentialDisplayField>
|
|
10
|
+
) =>
|
|
11
|
+
render(
|
|
12
|
+
<ThemeContextProvider>
|
|
13
|
+
<ConfidentialDisplayField {...props} />
|
|
14
|
+
</ThemeContextProvider>
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
describe('ConfidentialDisplayField', () => {
|
|
18
|
+
test('applies a string mask', () => {
|
|
19
|
+
renderDisplayField({
|
|
20
|
+
value: '12345678',
|
|
21
|
+
maskFormat: '******##',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(
|
|
25
|
+
screen.getByTestId('ucl-uikit-confidential-display-field')
|
|
26
|
+
).toHaveTextContent('••••••78');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('applies a function mask', () => {
|
|
30
|
+
renderDisplayField({
|
|
31
|
+
value: 12345678,
|
|
32
|
+
maskFormat: (value) => `**** ending in ${value.slice(-2)}`,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(
|
|
36
|
+
screen.getByTestId('ucl-uikit-confidential-display-field')
|
|
37
|
+
).toHaveTextContent('•••• ending in 78');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('does not add a visibility control by default', () => {
|
|
41
|
+
renderDisplayField({
|
|
42
|
+
value: '12345678',
|
|
43
|
+
maskFormat: '******##',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(
|
|
47
|
+
screen.queryByRole('button', { name: 'Show confidential value' })
|
|
48
|
+
).not.toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('toggles the original value', async () => {
|
|
52
|
+
const user = userEvent.setup();
|
|
53
|
+
renderDisplayField({
|
|
54
|
+
value: '12345678',
|
|
55
|
+
maskFormat: '******##',
|
|
56
|
+
showable: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const output = screen.getByTestId('ucl-uikit-confidential-display-field');
|
|
60
|
+
await user.click(
|
|
61
|
+
screen.getByRole('button', { name: 'Show confidential value' })
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(output).toHaveTextContent('12345678');
|
|
65
|
+
expect(
|
|
66
|
+
screen.getByRole('button', { name: 'Hide confidential value' })
|
|
67
|
+
).toHaveAttribute('aria-pressed', 'true');
|
|
68
|
+
|
|
69
|
+
await user.click(
|
|
70
|
+
screen.getByRole('button', { name: 'Hide confidential value' })
|
|
71
|
+
);
|
|
72
|
+
expect(output).toHaveTextContent('••••••78');
|
|
73
|
+
});
|
|
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
|
+
|
|
94
|
+
test('forwards its ref to the output', () => {
|
|
95
|
+
const ref = createRef<HTMLOutputElement>();
|
|
96
|
+
render(
|
|
97
|
+
<ThemeContextProvider>
|
|
98
|
+
<ConfidentialDisplayField
|
|
99
|
+
ref={ref}
|
|
100
|
+
value='12345678'
|
|
101
|
+
maskFormat='******##'
|
|
102
|
+
/>
|
|
103
|
+
</ThemeContextProvider>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
expect(ref.current).toBe(
|
|
107
|
+
screen.getByTestId('ucl-uikit-confidential-display-field')
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('hides the value when showable changes to false', async () => {
|
|
112
|
+
const user = userEvent.setup();
|
|
113
|
+
const { rerender } = render(
|
|
114
|
+
<ThemeContextProvider>
|
|
115
|
+
<ConfidentialDisplayField
|
|
116
|
+
value='12345678'
|
|
117
|
+
maskFormat='******##'
|
|
118
|
+
showable
|
|
119
|
+
/>
|
|
120
|
+
</ThemeContextProvider>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
await user.click(
|
|
124
|
+
screen.getByRole('button', { name: 'Show confidential value' })
|
|
125
|
+
);
|
|
126
|
+
expect(
|
|
127
|
+
screen.getByTestId('ucl-uikit-confidential-display-field')
|
|
128
|
+
).toHaveTextContent('12345678');
|
|
129
|
+
|
|
130
|
+
rerender(
|
|
131
|
+
<ThemeContextProvider>
|
|
132
|
+
<ConfidentialDisplayField
|
|
133
|
+
value='12345678'
|
|
134
|
+
maskFormat='******##'
|
|
135
|
+
showable={false}
|
|
136
|
+
/>
|
|
137
|
+
</ThemeContextProvider>
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(
|
|
141
|
+
screen.getByTestId('ucl-uikit-confidential-display-field')
|
|
142
|
+
).toHaveTextContent('••••••78');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -11,7 +11,7 @@ const meta = {
|
|
|
11
11
|
args: {
|
|
12
12
|
'aria-label': 'Account number',
|
|
13
13
|
defaultValue: '12345678',
|
|
14
|
-
maskFormat: '
|
|
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: '
|
|
34
|
+
maskFormat: '**-**-##',
|
|
35
35
|
showable: true,
|
|
36
36
|
},
|
|
37
37
|
};
|
|
@@ -16,6 +16,10 @@ import IconButton from '../IconButton';
|
|
|
16
16
|
import Input, { InputProps } from '../Input';
|
|
17
17
|
import { FieldContext } from '../Field';
|
|
18
18
|
import marginsStyle from '../common/marginsStyle';
|
|
19
|
+
import formatConfidentialValue, {
|
|
20
|
+
MaskFormat,
|
|
21
|
+
} from '../common/formatConfidentialValue';
|
|
22
|
+
import formatDisplayValue from '../common/formatDisplayValue';
|
|
19
23
|
|
|
20
24
|
export const NAME = 'ucl-uikit-confidential-input';
|
|
21
25
|
|
|
@@ -23,7 +27,7 @@ export interface ConfidentialInputProps extends Omit<
|
|
|
23
27
|
InputProps,
|
|
24
28
|
'iconButton' | 'type'
|
|
25
29
|
> {
|
|
26
|
-
maskFormat:
|
|
30
|
+
maskFormat: MaskFormat;
|
|
27
31
|
showable?: boolean;
|
|
28
32
|
visibleWhenFocused?: boolean;
|
|
29
33
|
}
|
|
@@ -37,36 +41,13 @@ const getStringValue = (
|
|
|
37
41
|
return Array.isArray(value) ? value.join(',') : String(value);
|
|
38
42
|
};
|
|
39
43
|
|
|
40
|
-
const applyMaskTemplate = (value: string, template: string) => {
|
|
41
|
-
const valueCharacters = Array.from(value);
|
|
42
|
-
let valueIndex = 0;
|
|
43
|
-
let maskedValue = '';
|
|
44
|
-
|
|
45
|
-
for (const templateCharacter of Array.from(template)) {
|
|
46
|
-
if (valueIndex >= valueCharacters.length) break;
|
|
47
|
-
|
|
48
|
-
if (templateCharacter === '1') {
|
|
49
|
-
maskedValue += valueCharacters[valueIndex];
|
|
50
|
-
valueIndex += 1;
|
|
51
|
-
} else if (templateCharacter === '*') {
|
|
52
|
-
maskedValue += '*';
|
|
53
|
-
valueIndex += 1;
|
|
54
|
-
} else {
|
|
55
|
-
maskedValue += templateCharacter;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
maskedValue + '*'.repeat(Math.max(0, valueCharacters.length - valueIndex))
|
|
61
|
-
);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
44
|
const ConfidentialInput = forwardRef<Ref, ConfidentialInputProps>(
|
|
65
45
|
(
|
|
66
46
|
{
|
|
67
47
|
maskFormat,
|
|
68
48
|
showable = false,
|
|
69
49
|
visibleWhenFocused = true,
|
|
50
|
+
format,
|
|
70
51
|
value,
|
|
71
52
|
defaultValue,
|
|
72
53
|
disabled,
|
|
@@ -142,13 +123,18 @@ const ConfidentialInput = forwardRef<Ref, ConfidentialInputProps>(
|
|
|
142
123
|
|
|
143
124
|
const stringValue =
|
|
144
125
|
value === undefined ? uncontrolledValue : getStringValue(value);
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
126
|
+
const displayedMaskedValue = formatConfidentialValue(
|
|
127
|
+
stringValue,
|
|
128
|
+
maskFormat
|
|
129
|
+
);
|
|
130
|
+
const configuredValueIsVisible =
|
|
131
|
+
!isFocused &&
|
|
132
|
+
stringValue.length > 0 &&
|
|
133
|
+
(!valueIsPersistentlyShown || Boolean(format));
|
|
134
|
+
const configuredValue = formatDisplayValue(
|
|
135
|
+
valueIsPersistentlyShown ? stringValue : displayedMaskedValue,
|
|
136
|
+
format
|
|
137
|
+
);
|
|
152
138
|
|
|
153
139
|
const wrapperStyle = cx(
|
|
154
140
|
NAME,
|
|
@@ -254,7 +240,7 @@ const ConfidentialInput = forwardRef<Ref, ConfidentialInputProps>(
|
|
|
254
240
|
iconPosition={iconPosition}
|
|
255
241
|
iconButton={visibilityButton}
|
|
256
242
|
inputClassName={cx(
|
|
257
|
-
|
|
243
|
+
configuredValueIsVisible && hiddenInputStyle,
|
|
258
244
|
inputClassName
|
|
259
245
|
)}
|
|
260
246
|
testId={testId}
|
|
@@ -263,13 +249,13 @@ const ConfidentialInput = forwardRef<Ref, ConfidentialInputProps>(
|
|
|
263
249
|
onFocus={handleFocus}
|
|
264
250
|
onBlur={handleBlur}
|
|
265
251
|
/>
|
|
266
|
-
{
|
|
252
|
+
{configuredValueIsVisible && (
|
|
267
253
|
<span
|
|
268
254
|
className={maskStyle}
|
|
269
255
|
data-testid={`${testId}-mask`}
|
|
270
256
|
aria-hidden='true'
|
|
271
257
|
>
|
|
272
|
-
{
|
|
258
|
+
{configuredValue}
|
|
273
259
|
</span>
|
|
274
260
|
)}
|
|
275
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.
|
|
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='
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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='
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '****',
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import DisplayField from './DisplayField';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Components/DisplayField',
|
|
6
|
+
component: DisplayField,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'centered',
|
|
9
|
+
},
|
|
10
|
+
tags: ['autodocs'],
|
|
11
|
+
args: {
|
|
12
|
+
value: 'Personal savings',
|
|
13
|
+
},
|
|
14
|
+
} satisfies Meta<typeof DisplayField>;
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
|
|
20
|
+
export const Text: Story = {};
|
|
21
|
+
|
|
22
|
+
export const Number: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
value: 12345678,
|
|
25
|
+
},
|
|
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
|
+
};
|