@verifiedinc-public/shared-ui-elements 0.11.6 → 0.12.0
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/README.md +41 -0
- package/package.json +6 -24
- package/src/components/Alert/Alert.tsx +8 -0
- package/src/components/Alert/FullWidthAlert.tsx +27 -0
- package/src/components/Alert/index.ts +2 -0
- package/src/components/Button/index.tsx +8 -0
- package/src/components/CredentialRequestsEditor/CredentialRequestsEditor.context.tsx +98 -0
- package/src/components/CredentialRequestsEditor/components/CredentialRequestsField.tsx +103 -0
- package/src/components/CredentialRequestsEditor/components/DataFieldAccordion.tsx +337 -0
- package/src/components/CredentialRequestsEditor/components/DataFieldDeleteModal.tsx +64 -0
- package/src/components/CredentialRequestsEditor/components/DataFieldDescription.tsx +68 -0
- package/src/components/CredentialRequestsEditor/components/DataFieldMandatory.tsx +84 -0
- package/src/components/CredentialRequestsEditor/components/DataFieldMulti.tsx +74 -0
- package/src/components/CredentialRequestsEditor/components/DataFieldOptionType.tsx +84 -0
- package/src/components/CredentialRequestsEditor/components/DataFieldSection.tsx +48 -0
- package/src/components/CredentialRequestsEditor/components/DataFieldUserInput.tsx +71 -0
- package/src/components/CredentialRequestsEditor/components/RadioOption.tsx +89 -0
- package/src/components/CredentialRequestsEditor/contexts/CredentialRequestFieldContext.tsx +36 -0
- package/src/components/CredentialRequestsEditor/index.tsx +15 -0
- package/src/components/CredentialRequestsEditor/types/compositeCredentialSchema.ts +1 -0
- package/src/components/CredentialRequestsEditor/types/credentialSchemasDto.ts +3 -0
- package/src/components/CredentialRequestsEditor/types/form.ts +28 -0
- package/src/components/CredentialRequestsEditor/types/mandatoryEnum.ts +5 -0
- package/src/components/CredentialRequestsEditor/utils/buildDataFieldValue.ts +65 -0
- package/src/components/CredentialRequestsEditor/utils/prettyField.ts +16 -0
- package/src/components/Image.tsx +10 -0
- package/src/components/QRCodeDisplay/index.tsx +50 -0
- package/src/components/RequiredLabel/index.tsx +15 -0
- package/src/components/TextField/index.tsx +8 -0
- package/src/components/Tip/index.tsx +18 -0
- package/src/components/Typography/index.tsx +8 -0
- package/src/components/When.tsx +28 -0
- package/src/components/form/CountrySelector.tsx +96 -0
- package/src/components/form/DataFieldClearAdornment.tsx +28 -0
- package/src/components/form/DateInput.tsx +117 -0
- package/src/components/form/DefaultInput.tsx +28 -0
- package/src/components/form/InputMask.tsx +41 -0
- package/src/components/form/OTPInput.tsx +246 -0
- package/src/components/form/PhoneInput.tsx +153 -0
- package/src/components/form/SSNInput.tsx +100 -0
- package/src/components/form/SelectInput.tsx +89 -0
- package/src/components/form/TextMaskCustom.tsx +48 -0
- package/src/components/form/index.ts +5 -0
- package/src/components/form/styles/input.ts +24 -0
- package/src/components/index.ts +10 -0
- package/src/components/terms/AcceptTermsNotice.tsx +27 -0
- package/src/components/terms/LegalLink.tsx +22 -0
- package/src/components/verified/VerifiedImage.tsx +272 -0
- package/src/components/verified/VerifiedIncLogo.tsx +11 -0
- package/src/components/verified/index.ts +2 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useCallbackRef.ts +22 -0
- package/src/hooks/useCopyToClipboard.ts +76 -0
- package/src/hooks/useDisclosure.ts +96 -0
- package/src/hooks/useLocalStorage.ts +24 -0
- package/src/hooks/usePrevious.ts +17 -0
- package/src/hooks/useQRCode.ts +62 -0
- package/src/index.ts +5 -0
- package/src/stories/components/Alert.stories.tsx +41 -0
- package/src/stories/components/Button.stories.ts +49 -0
- package/src/stories/components/CredentialRequestsEditor.stories.tsx +98 -0
- package/src/stories/components/QRCodeDisplay.stories.tsx +60 -0
- package/src/stories/components/TextField.stories.ts +59 -0
- package/src/stories/components/Typography.stories.ts +140 -0
- package/src/stories/components/VerifiedImage.stories.tsx +32 -0
- package/src/stories/components/form/DateInput.stories.ts +39 -0
- package/src/stories/components/form/OTPInput.stories.tsx +90 -0
- package/src/stories/components/form/PhoneInput.stories.tsx +34 -0
- package/src/stories/components/form/SSNInput.stories.ts +30 -0
- package/src/stories/components/form/SelectInput.stories.ts +39 -0
- package/src/stories/hooks/useCopyToClipboard.stories.tsx +45 -0
- package/src/styles/colors.ts +34 -0
- package/src/styles/index.ts +2 -0
- package/src/styles/theme.ts +209 -0
- package/src/utils/date.ts +41 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/masks/index.ts +6 -0
- package/src/utils/omitProperty.ts +19 -0
- package/src/utils/phone.ts +76 -0
- package/src/utils/wrapPromise.ts +19 -0
- package/src/validations/birthDate.schema.ts +54 -0
- package/src/validations/date.schema.ts +10 -0
- package/src/validations/description.schema.ts +5 -0
- package/src/validations/email.schema.ts +3 -0
- package/src/validations/field.schema.ts +3 -0
- package/src/validations/index.ts +9 -0
- package/src/validations/phone.schema.ts +6 -0
- package/src/validations/ssn.schema.ts +6 -0
- package/src/validations/state.schema.ts +3 -0
- package/src/validations/unix.schema.ts +11 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
import _ from 'lodash';
|
2
|
+
import { type CompositeCredentialSchema } from '../types/compositeCredentialSchema';
|
3
|
+
import { CredentialRequests } from '../types/form';
|
4
|
+
import { type CredentialSchemaDto } from '../types/credentialSchemasDto';
|
5
|
+
import { MandatoryEnum } from '../types/mandatoryEnum';
|
6
|
+
|
7
|
+
const isComposed = (schema: unknown): schema is CompositeCredentialSchema =>
|
8
|
+
Object.prototype.hasOwnProperty.call(schema || {}, 'anyOf') ||
|
9
|
+
Object.prototype.hasOwnProperty.call(schema || {}, 'allOf');
|
10
|
+
|
11
|
+
function extractTypes(
|
12
|
+
record: any,
|
13
|
+
result: string[] = [],
|
14
|
+
parentKeys: string[] = [],
|
15
|
+
): string[] {
|
16
|
+
_.forOwn(record, (value, key) => {
|
17
|
+
// Check if the current key is $ref or $id and handle accordingly
|
18
|
+
if (key === '$ref' && typeof value === 'string') {
|
19
|
+
result.push(value);
|
20
|
+
} else if (
|
21
|
+
key === '$id' &&
|
22
|
+
typeof value === 'string' &&
|
23
|
+
_.some(parentKeys, (k) => ['allOf', 'anyOf', 'oneOf'].includes(k))
|
24
|
+
) {
|
25
|
+
result.push(value);
|
26
|
+
}
|
27
|
+
|
28
|
+
// If the value is an object or array, recurse into it
|
29
|
+
if (_.isObject(value)) {
|
30
|
+
extractTypes(value, result, [...parentKeys, key]);
|
31
|
+
}
|
32
|
+
});
|
33
|
+
|
34
|
+
return result;
|
35
|
+
}
|
36
|
+
|
37
|
+
export function buildDataFieldValue(
|
38
|
+
type: string,
|
39
|
+
schema: CredentialSchemaDto['schemas'],
|
40
|
+
): CredentialRequests {
|
41
|
+
const selectedSchema = schema[type];
|
42
|
+
const isComposedSchema = isComposed(selectedSchema);
|
43
|
+
|
44
|
+
if (isComposedSchema) {
|
45
|
+
return {
|
46
|
+
type,
|
47
|
+
mandatory: MandatoryEnum.NO,
|
48
|
+
description: '',
|
49
|
+
allowUserInput: true,
|
50
|
+
multi: false,
|
51
|
+
children: extractTypes(selectedSchema).map((item) =>
|
52
|
+
buildDataFieldValue(item, schema),
|
53
|
+
),
|
54
|
+
};
|
55
|
+
}
|
56
|
+
|
57
|
+
return {
|
58
|
+
type,
|
59
|
+
mandatory: MandatoryEnum.NO,
|
60
|
+
description: '',
|
61
|
+
allowUserInput: true,
|
62
|
+
// We want to default to true if is email credential.
|
63
|
+
multi: type === 'EmailCredential',
|
64
|
+
};
|
65
|
+
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
// This pattern will split camel cased string out of the credential,
|
2
|
+
// when a camel word contain a sequence of uppercase character it is kept to maintain consistency of that word.
|
3
|
+
const schemaNamePattern = /([A-Z][a-z0-9]+)/gm;
|
4
|
+
|
5
|
+
// Format the camel cased text to a human-readable.
|
6
|
+
export const prettyField = (field: string): string =>
|
7
|
+
field
|
8
|
+
.split(schemaNamePattern)
|
9
|
+
.map((word) => {
|
10
|
+
if (word === 'Id') return 'ID';
|
11
|
+
if (word === 'Zip') return 'ZIP';
|
12
|
+
if (word === 'Ssn') return 'SSN';
|
13
|
+
return word;
|
14
|
+
})
|
15
|
+
.filter((e) => e !== 'Credential')
|
16
|
+
.join(' ');
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { Box, type BoxProps } from '@mui/material';
|
2
|
+
|
3
|
+
export interface ImageProps extends BoxProps {
|
4
|
+
src: string;
|
5
|
+
alt: string;
|
6
|
+
}
|
7
|
+
|
8
|
+
export const Image = ({ src, alt, ...props }: ImageProps): JSX.Element => {
|
9
|
+
return <Box src={src} alt={alt} {...props} component='img' />;
|
10
|
+
};
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import { Box } from '@mui/material';
|
2
|
+
|
3
|
+
import { useQRCode } from '../../hooks';
|
4
|
+
|
5
|
+
export interface QRCodeDisplayProps {
|
6
|
+
data: string;
|
7
|
+
asset?: string;
|
8
|
+
svgSize?: number;
|
9
|
+
logoSize?: number;
|
10
|
+
fill?: string;
|
11
|
+
}
|
12
|
+
|
13
|
+
export function QRCodeDisplay({
|
14
|
+
data,
|
15
|
+
asset,
|
16
|
+
svgSize = 300,
|
17
|
+
logoSize = 0,
|
18
|
+
fill = '#000000',
|
19
|
+
}: QRCodeDisplayProps) {
|
20
|
+
const svg = useQRCode({
|
21
|
+
data: data,
|
22
|
+
size: svgSize,
|
23
|
+
imageSize: logoSize,
|
24
|
+
fill,
|
25
|
+
});
|
26
|
+
|
27
|
+
return (
|
28
|
+
<Box position='relative'>
|
29
|
+
<Box
|
30
|
+
display='flex'
|
31
|
+
sx={{ '& svg': { width: '100%', height: 'auto', aspectRatio: 1 } }}
|
32
|
+
dangerouslySetInnerHTML={{ __html: svg }}
|
33
|
+
/>
|
34
|
+
<Box
|
35
|
+
component='img'
|
36
|
+
src={asset}
|
37
|
+
sx={{
|
38
|
+
position: 'absolute',
|
39
|
+
width: (logoSize / svgSize) * 100 + '%',
|
40
|
+
maxWidth: logoSize + 'px',
|
41
|
+
height: 'auto',
|
42
|
+
inset: 0,
|
43
|
+
aspectRatio: 1,
|
44
|
+
m: 'auto',
|
45
|
+
p: 0.5,
|
46
|
+
}}
|
47
|
+
/>
|
48
|
+
</Box>
|
49
|
+
);
|
50
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import { type PropsWithChildren } from 'react';
|
2
|
+
import { Box } from '@mui/material';
|
3
|
+
|
4
|
+
export function RequiredLabel(
|
5
|
+
props: Readonly<PropsWithChildren>,
|
6
|
+
): React.JSX.Element {
|
7
|
+
return (
|
8
|
+
<span>
|
9
|
+
{props.children}
|
10
|
+
<Box component='span' color='error.main' sx={{ ml: 0.5 }}>
|
11
|
+
*
|
12
|
+
</Box>
|
13
|
+
</span>
|
14
|
+
);
|
15
|
+
}
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import MUITextField from '@mui/material/TextField';
|
2
|
+
import type { TextFieldProps as MUITextFieldProps } from '@mui/material/TextField';
|
3
|
+
|
4
|
+
export type TextFieldProps = MUITextFieldProps;
|
5
|
+
|
6
|
+
export function TextField(props: TextFieldProps): JSX.Element {
|
7
|
+
return <MUITextField {...props} />;
|
8
|
+
}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import { type PropsWithChildren } from 'react';
|
2
|
+
import { IconButton, Tooltip } from '@mui/material';
|
3
|
+
import { Code } from '@mui/icons-material';
|
4
|
+
|
5
|
+
export function Tip({ children }: PropsWithChildren): React.JSX.Element {
|
6
|
+
return (
|
7
|
+
<Tooltip title={children} arrow enterTouchDelay={0}>
|
8
|
+
<IconButton
|
9
|
+
size='small'
|
10
|
+
onClick={(e) => {
|
11
|
+
e.stopPropagation();
|
12
|
+
}}
|
13
|
+
>
|
14
|
+
<Code />
|
15
|
+
</IconButton>
|
16
|
+
</Tooltip>
|
17
|
+
);
|
18
|
+
}
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import MUITypography from '@mui/material/Typography';
|
2
|
+
import type { TypographyProps as MUITypographyProps } from '@mui/material/Typography';
|
3
|
+
|
4
|
+
export type TypographyProps = MUITypographyProps;
|
5
|
+
|
6
|
+
export function Typography(props: TypographyProps): JSX.Element {
|
7
|
+
return <MUITypography {...props} />;
|
8
|
+
}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import { type ReactNode } from 'react';
|
2
|
+
|
3
|
+
export interface WhenProps<T = unknown> {
|
4
|
+
value: T;
|
5
|
+
children: ReactNode | ((condition: NonNullable<T>) => ReactNode);
|
6
|
+
}
|
7
|
+
|
8
|
+
/**
|
9
|
+
* This component can be used when using value is truthy to render some other component,
|
10
|
+
* instead of using ternary operators you can use this.
|
11
|
+
*
|
12
|
+
* This is particularly useful when you have nested conditional rendering. Also, it allows for safer falsy conditions
|
13
|
+
* while in a ternary you can risk rendering falsy values in the react tree if you use `&&`.
|
14
|
+
* @param value
|
15
|
+
* @param children Children can be direct ReactNode or a function returning ReactNode, when a function,
|
16
|
+
* the value is being passed as param and is guarantee to be non-nullable excluding undefined and null from the inferred type.
|
17
|
+
* @constructor
|
18
|
+
*/
|
19
|
+
export function When<T>({
|
20
|
+
value,
|
21
|
+
children,
|
22
|
+
}: WhenProps<T>): React.JSX.Element | null {
|
23
|
+
if (!value) return null;
|
24
|
+
|
25
|
+
if (typeof children === 'function') return <>{children(value)}</>;
|
26
|
+
|
27
|
+
return <>{children}</>;
|
28
|
+
}
|
@@ -0,0 +1,96 @@
|
|
1
|
+
import { useMemo, useState } from 'react';
|
2
|
+
import { Button, Menu, MenuItem } from '@mui/material';
|
3
|
+
|
4
|
+
import { KeyboardArrowDown } from '@mui/icons-material';
|
5
|
+
import {
|
6
|
+
countries,
|
7
|
+
getPhoneDataByFieldName,
|
8
|
+
sortByCountryName,
|
9
|
+
} from '../../utils/phone';
|
10
|
+
|
11
|
+
interface CountrySelectorProps {
|
12
|
+
value: string;
|
13
|
+
onChange: (value: string) => void;
|
14
|
+
shouldShowOnlyNorthAmericanCountries?: boolean;
|
15
|
+
}
|
16
|
+
|
17
|
+
/**
|
18
|
+
* Component that renders and allows to manage the desired phone country format.
|
19
|
+
* @constructor
|
20
|
+
*/
|
21
|
+
export default function CountrySelector({
|
22
|
+
shouldShowOnlyNorthAmericanCountries = true,
|
23
|
+
...props
|
24
|
+
}: Readonly<CountrySelectorProps>): React.JSX.Element {
|
25
|
+
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
26
|
+
const open = Boolean(anchorEl);
|
27
|
+
const options = useMemo(() => {
|
28
|
+
// Remove Brazil from the list of countries, we allow only North American numbers.
|
29
|
+
let _countries = [
|
30
|
+
...countries.filter((country) => country.countryCode !== 'BR'),
|
31
|
+
];
|
32
|
+
|
33
|
+
// Show all countries in development mode.
|
34
|
+
if (!shouldShowOnlyNorthAmericanCountries) {
|
35
|
+
_countries = [...countries];
|
36
|
+
}
|
37
|
+
|
38
|
+
return [..._countries].sort(sortByCountryName);
|
39
|
+
}, [shouldShowOnlyNorthAmericanCountries]);
|
40
|
+
|
41
|
+
const handleClick = (event: React.MouseEvent<HTMLElement>): void => {
|
42
|
+
setAnchorEl(event.currentTarget);
|
43
|
+
};
|
44
|
+
|
45
|
+
const handleClose = (): void => {
|
46
|
+
setAnchorEl(null);
|
47
|
+
};
|
48
|
+
|
49
|
+
return (
|
50
|
+
<>
|
51
|
+
<Button
|
52
|
+
id='demo-customized-button'
|
53
|
+
aria-controls={open ? 'country-select-button' : undefined}
|
54
|
+
aria-haspopup='true'
|
55
|
+
aria-expanded={open ? 'true' : undefined}
|
56
|
+
variant='text'
|
57
|
+
disableElevation
|
58
|
+
onClick={handleClick}
|
59
|
+
endIcon={<KeyboardArrowDown />}
|
60
|
+
>
|
61
|
+
{getPhoneDataByFieldName('countryCode', props.value)?.emoji}
|
62
|
+
</Button>
|
63
|
+
<Menu
|
64
|
+
anchorEl={anchorEl}
|
65
|
+
open={open}
|
66
|
+
onClose={handleClose}
|
67
|
+
MenuListProps={{
|
68
|
+
'aria-labelledby': 'country-select-button',
|
69
|
+
}}
|
70
|
+
slotProps={{
|
71
|
+
paper: {
|
72
|
+
style: {
|
73
|
+
maxHeight: 48 * 4.5,
|
74
|
+
width: '20ch',
|
75
|
+
minWidth: '300px',
|
76
|
+
},
|
77
|
+
},
|
78
|
+
}}
|
79
|
+
>
|
80
|
+
{/* spread to avoid mutating the original countries array with sort method */}
|
81
|
+
{options.map((country) => (
|
82
|
+
<MenuItem
|
83
|
+
role='menuitem'
|
84
|
+
key={country.countryCode}
|
85
|
+
onClick={() => {
|
86
|
+
props.onChange(country.countryCode);
|
87
|
+
handleClose();
|
88
|
+
}}
|
89
|
+
>
|
90
|
+
{country.emoji} {country.countryName}
|
91
|
+
</MenuItem>
|
92
|
+
))}
|
93
|
+
</Menu>
|
94
|
+
</>
|
95
|
+
);
|
96
|
+
}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import { Close } from '@mui/icons-material';
|
2
|
+
import { InputAdornment, IconButton } from '@mui/material';
|
3
|
+
|
4
|
+
type DataFieldClearAdornmentProps = Readonly<{
|
5
|
+
onClick?: () => void;
|
6
|
+
handleClear: () => void;
|
7
|
+
}>;
|
8
|
+
|
9
|
+
export function DataFieldClearAdornment({
|
10
|
+
onClick,
|
11
|
+
handleClear,
|
12
|
+
}: DataFieldClearAdornmentProps): React.JSX.Element {
|
13
|
+
return (
|
14
|
+
<InputAdornment position='end'>
|
15
|
+
<IconButton
|
16
|
+
aria-label='clear value'
|
17
|
+
edge='end'
|
18
|
+
size='small'
|
19
|
+
onClick={() => {
|
20
|
+
handleClear();
|
21
|
+
onClick?.();
|
22
|
+
}}
|
23
|
+
>
|
24
|
+
<Close fontSize='small' />
|
25
|
+
</IconButton>
|
26
|
+
</InputAdornment>
|
27
|
+
);
|
28
|
+
}
|
@@ -0,0 +1,117 @@
|
|
1
|
+
import { Box, TextField } from '@mui/material';
|
2
|
+
import {
|
3
|
+
forwardRef,
|
4
|
+
useEffect,
|
5
|
+
useState,
|
6
|
+
type ChangeEventHandler,
|
7
|
+
} from 'react';
|
8
|
+
import {
|
9
|
+
formatDateMMDDYYYY,
|
10
|
+
getMaxDateInstance,
|
11
|
+
getMinDateInstance,
|
12
|
+
} from '../../utils/date';
|
13
|
+
import { masks } from '../../utils/masks';
|
14
|
+
import { USDateSchema } from '../../validations';
|
15
|
+
import { type TextFieldProps } from '../TextField';
|
16
|
+
import { InputMask } from './InputMask';
|
17
|
+
import { inputStyle } from './styles/input';
|
18
|
+
|
19
|
+
interface DateInputProps {
|
20
|
+
name?: string;
|
21
|
+
value?: string;
|
22
|
+
label?: string;
|
23
|
+
error?: boolean;
|
24
|
+
helperText?: string;
|
25
|
+
onChange?: (event: { target: { value: string } }) => void;
|
26
|
+
onBlur?: ChangeEventHandler<HTMLInputElement>;
|
27
|
+
disabled?: boolean;
|
28
|
+
allowFutureDates?: boolean;
|
29
|
+
}
|
30
|
+
|
31
|
+
/**
|
32
|
+
* The input with date format.
|
33
|
+
* @constructor
|
34
|
+
*/
|
35
|
+
function DateInputComponent(
|
36
|
+
{
|
37
|
+
label = 'Date of Birth',
|
38
|
+
value = '',
|
39
|
+
error,
|
40
|
+
helperText,
|
41
|
+
onChange,
|
42
|
+
onBlur,
|
43
|
+
disabled,
|
44
|
+
allowFutureDates = true,
|
45
|
+
...rest
|
46
|
+
}: Readonly<DateInputProps>,
|
47
|
+
ref: any,
|
48
|
+
): React.JSX.Element {
|
49
|
+
// Arbitrary value to format the timestamp into human-readable date.
|
50
|
+
const [localValue, setLocalValue] = useState<string>(
|
51
|
+
value ? formatDateMMDDYYYY(value) : '',
|
52
|
+
);
|
53
|
+
|
54
|
+
useEffect(() => {
|
55
|
+
if (value === '') {
|
56
|
+
setLocalValue('');
|
57
|
+
}
|
58
|
+
}, [value]);
|
59
|
+
|
60
|
+
const minDateInstance = getMinDateInstance();
|
61
|
+
const maxDateInstance = getMaxDateInstance(allowFutureDates);
|
62
|
+
|
63
|
+
const textFieldStyle: TextFieldProps = {
|
64
|
+
...inputStyle,
|
65
|
+
label,
|
66
|
+
error,
|
67
|
+
helperText,
|
68
|
+
inputProps: {
|
69
|
+
// Set the input mode to numeric.
|
70
|
+
inputMode: 'numeric',
|
71
|
+
// Tab index for each block.
|
72
|
+
tabIndex: 0,
|
73
|
+
// Mask type date.
|
74
|
+
mask: masks.DOB_MASK,
|
75
|
+
},
|
76
|
+
fullWidth: true,
|
77
|
+
};
|
78
|
+
|
79
|
+
return (
|
80
|
+
<Box width='100%'>
|
81
|
+
<InputMask
|
82
|
+
mask={masks.DOB_MASK}
|
83
|
+
maskPlaceholder={null}
|
84
|
+
disabled={disabled}
|
85
|
+
value={localValue}
|
86
|
+
onBlur={onBlur}
|
87
|
+
onChange={(e) => {
|
88
|
+
const value = e.target.value;
|
89
|
+
const valid = USDateSchema.safeParse(value);
|
90
|
+
|
91
|
+
// Update the facade input value, so it let user input wrong/right values.
|
92
|
+
setLocalValue(value);
|
93
|
+
|
94
|
+
if (!valid.success) {
|
95
|
+
// A way to make sure the data field state is invalid is to empty the value.
|
96
|
+
return onChange?.({ target: { value: '' } });
|
97
|
+
}
|
98
|
+
|
99
|
+
const date = new Date(value);
|
100
|
+
if (date < minDateInstance || date > maxDateInstance) {
|
101
|
+
// A way to make sure the data field state is invalid is to empty the value.
|
102
|
+
return onChange?.({ target: { value: '' } });
|
103
|
+
}
|
104
|
+
|
105
|
+
date.setUTCHours(12);
|
106
|
+
|
107
|
+
// The date is valid in the US date format and is in between the valid min-max date range.
|
108
|
+
onChange?.({ target: { value: String(+date) } });
|
109
|
+
}}
|
110
|
+
>
|
111
|
+
<TextField {...textFieldStyle} inputRef={ref} {...rest} />
|
112
|
+
</InputMask>
|
113
|
+
</Box>
|
114
|
+
);
|
115
|
+
}
|
116
|
+
|
117
|
+
export const DateInput = forwardRef(DateInputComponent);
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import { forwardRef } from 'react';
|
2
|
+
import { inputStyle } from './styles/input';
|
3
|
+
import { TextField, type TextFieldProps } from '../TextField';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* A wrapper around the MUI TextField component to encapsulate some common defaults.
|
7
|
+
* Primarily intended for use creating more specific Input components rather than for direct use.
|
8
|
+
*
|
9
|
+
* @param {TextFieldProps} props DefaultInput takes the same props as the MUI TextField component.
|
10
|
+
* It sets default values for variant ('outlined') and margin ('normal').
|
11
|
+
*/
|
12
|
+
const DefaultInput = (
|
13
|
+
{ variant = 'outlined', margin = 'normal', ...props }: TextFieldProps,
|
14
|
+
ref: any,
|
15
|
+
): React.JSX.Element => {
|
16
|
+
return (
|
17
|
+
<TextField
|
18
|
+
inputRef={ref}
|
19
|
+
{...inputStyle}
|
20
|
+
variant={variant}
|
21
|
+
margin={margin}
|
22
|
+
fullWidth
|
23
|
+
{...props}
|
24
|
+
/>
|
25
|
+
);
|
26
|
+
};
|
27
|
+
|
28
|
+
export default forwardRef(DefaultInput);
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import { type ChangeEventHandler, type ReactNode } from 'react';
|
2
|
+
import RInputMask from '@mona-health/react-input-mask';
|
3
|
+
|
4
|
+
interface Selection {
|
5
|
+
start: string;
|
6
|
+
end: string;
|
7
|
+
}
|
8
|
+
|
9
|
+
interface State {
|
10
|
+
value: string;
|
11
|
+
selection: Selection;
|
12
|
+
}
|
13
|
+
|
14
|
+
interface BeforeMaskedStateChangeOptions {
|
15
|
+
previousState: State;
|
16
|
+
currentState: State;
|
17
|
+
nextState: State;
|
18
|
+
}
|
19
|
+
|
20
|
+
interface InputMaskProps {
|
21
|
+
// Custom render function for integration with other input components.
|
22
|
+
children: ReactNode;
|
23
|
+
// Mask format.
|
24
|
+
mask: string | Array<RegExp | string>;
|
25
|
+
// Input value.
|
26
|
+
value: string;
|
27
|
+
// Change event handler.
|
28
|
+
onChange: ChangeEventHandler<HTMLInputElement>;
|
29
|
+
// Function to modify value and selection before applying mask.
|
30
|
+
onBlur?: ChangeEventHandler<HTMLInputElement>;
|
31
|
+
beforeMaskedStateChange?: (options: BeforeMaskedStateChangeOptions) => void;
|
32
|
+
// Placeholder to cover unfilled parts of the mask, null to remove the default "_" placeholder.
|
33
|
+
maskPlaceholder?: string | null;
|
34
|
+
// Whether mask prefix and placeholder should be displayed when input is empty and has no focus.
|
35
|
+
alwaysShowMask?: boolean;
|
36
|
+
disabled?: boolean;
|
37
|
+
}
|
38
|
+
|
39
|
+
export function InputMask(props: Readonly<InputMaskProps>): React.JSX.Element {
|
40
|
+
return <RInputMask {...props} />;
|
41
|
+
}
|