Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NDS-704] Masked TextField [v5] #645

Merged
merged 3 commits into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
"@babel/preset-typescript": "^7.13.0",
"@commitlint/cli": "^11.0.0",
"@commitlint/config-conventional": "^11.0.0",
"@emotion/react": "^11.4.0",
"@emotion/styled": "^11.3.0",
"@emotion/babel-plugin": "^11.10.5",
"@emotion/babel-preset-css-prop": "^11.10.0",
"@emotion/eslint-plugin": "^11.10.0",
"@emotion/jest": "^11.10.5",
"@emotion/react": "^11.4.0",
"@emotion/styled": "^11.3.0",
"@mdx-js/react": "^1.6.22",
"@optimize-lodash/rollup-plugin": "^4.0.1",
"@rollup/plugin-commonjs": "^23.0.7",
Expand Down Expand Up @@ -56,6 +56,7 @@
"@types/react": "^17.0.9",
"@types/react-dom": "^17.0.6",
"@types/react-highlight-words": "^0.16.1",
"@types/react-input-mask": "^3.0.2",
"@types/react-router-dom": "^5.1.5",
"@types/react-test-renderer": "^17.0.1",
"@types/recharts": "^1.8.16",
Expand Down Expand Up @@ -119,6 +120,7 @@
"polished": "^3.4.4",
"react-fast-compare": "^3.2.0",
"react-highlight-words": "^0.17.0",
"react-input-mask": "^2.0.4",
"react-media": "^2.0.0-rc.1",
"react-range": "^1.8.12",
"react-switch": "^6.0.0",
Expand Down
35 changes: 35 additions & 0 deletions src/components/TextField/TextField.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Icon from '../Icon';
import iconSelector from '../Icon/assets/iconSelector';
import Loader from '../Loader';
import TextFieldShowcase from '../../components/storyUtils/TextFieldShowcase/TextFieldShowcase.tsx';
import MaskedTextFieldShowCase from '../../components/storyUtils/MaskedTextFieldShowCase/MaskedTextFieldShowCase.tsx';
import { FIGMA_URL } from '../../utils/common';
import SectionHeader from '../../storybook/SectionHeader';

Expand Down Expand Up @@ -71,6 +72,40 @@ Regular TextField with label options. Label that will float, with or without Pla
</Story>
</Preview>

### Masked TextField

TextField with mask option. When used, placeholder prop is not available.
The mask rules are the following:

<UsageGuidelines
guidelines={[
" '9' => [0-9] (Numeric)",
" 'a' => [A-Za-z] (Alphabetical)",
" '*' => [A-Za-z0-9] (Alphanumeric)",
]}
/>

<Tip>
In case number 9 is needed as a constant inside the mask, use '\9'. For example Georgian phone
number format would be +\9\9\9 999 999999 (example: +999 123 123456){' '}
</Tip>

<Preview>
<Story name="Masked TextField (DontTest)">
<Stack>
<MaskedTextFieldShowCase
mask={text('Mask', '+(999) 999')}
isDisabled={boolean('isDisabled', false)}
suffix={select('Suffix icon name', ['', ...Object.keys(iconSelector)], 'info')}
status={{
type: select('Status', ['error', 'normal', 'read-only'], 'normal'),
hintMessage: text('Hint/Error message', 'Message in Text Field'),
}}
/>
</Stack>
</Story>
</Preview>

### TextField with Icons

TextField with icon options right of the text, either as external image or one of the available ictinus Icons.
Expand Down
27 changes: 26 additions & 1 deletion src/components/TextField/TextField.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import userEvent from '@testing-library/user-event';
import React from 'react';

import { render, screen } from '../../test';
import { fireEvent, render, screen } from '../../test';
import TextFieldShowCase from '../storyUtils/TextFieldShowcase/TextFieldShowcase';
import MaskedTextFieldShowCase from '../storyUtils/MaskedTextFieldShowCase/MaskedTextFieldShowCase';

export const values = ['Value 1', 'Value 2'];

Expand Down Expand Up @@ -47,3 +48,27 @@ describe('Multi TextField', () => {
expect(screen.queryByTestId('chip-chip_1')).not.toBeInTheDocument();
});
});

describe('Masked TextField', () => {
let input: HTMLInputElement;

beforeEach(() => {
render(<MaskedTextFieldShowCase mask={'+(999)'} />);
});

beforeEach(() => {
input = screen.getByTestId('input') as HTMLInputElement;
});

it('shows the mask as a placeholder when focused', async () => {
input.focus();

expect(input.value).toEqual('+( )');
});

it('formats the input correctly according to the mask', async () => {
fireEvent.change(input, { target: { value: '123' } });

expect(input.value).toEqual('+(123)');
});
});
42 changes: 28 additions & 14 deletions src/components/TextField/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import useTheme from 'hooks/useTheme';
import { omit } from 'lodash';
import React, { InputHTMLAttributes, useMemo } from 'react';
import isEqual from 'react-fast-compare';
import InputMask from 'react-input-mask';
import { generateUniqueID } from 'utils/helpers';

import { suffixContainerStyle } from './TextField.style';
Expand All @@ -20,6 +21,8 @@ export type InputProps = Partial<
Omit<InputHTMLAttributes<HTMLInputElement>, 'readOnly' | 'disabled'>
>;

type MaskProps = { mask?: never } | { mask?: string | (string | RegExp)[]; placeholder?: never };

export type TextFieldProps = {
/** The id of the text field that will be used as for in label too */
id?: string;
Expand All @@ -44,6 +47,7 @@ export type TextFieldProps = {
/** [For MultiTextField] A callback for when all values are deleted */
onMultiValueClearAll?: () => void;
} & TextInputBaseProps &
MaskProps &
InputProps &
TestProps;

Expand All @@ -62,6 +66,7 @@ const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>((props, ref
tags = [],
onMultiValueDelete,
onMultiValueClearAll = () => null,
mask,
...rest
} = props;
const theme = useTheme();
Expand Down Expand Up @@ -109,6 +114,24 @@ const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>((props, ref
[suffixContent, tokens]
);

const inputProps = {
// eslint-disable-next-line @typescript-eslint/naming-convention
readOnly: isLocked || isReadOnly,
css: inputStyle({ label, placeholder }),
placeholder: placeholder ? `${placeholder} ${isRequired ? '*' : ''}` : label,
// eslint-disable-next-line @typescript-eslint/naming-convention
required: isRequired,
id: id,
// eslint-disable-next-line @typescript-eslint/naming-convention
disabled: isDisabled || isLocked,
onInput: onInput,
'data-testid': rest.dataTestId ? `input_${rest.dataTestId}` : 'input',
// eslint-disable-next-line @typescript-eslint/naming-convention
'aria-invalid': status?.type === 'error',
'aria-describedby': hintMessageId,
...omit(rest, 'dataTestId'),
};

return (
<div onClick={handleClick}>
{isMulti ? (
Expand All @@ -128,20 +151,11 @@ const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>((props, ref
) : (
<TextInputBase {...props} status={{ ...status, id: hintMessageId }} sx={textInputBaseSx}>
<div css={{ width: '100% ' }}>
<input
readOnly={isLocked || isReadOnly}
css={inputStyle({ label, placeholder })}
placeholder={placeholder ? `${placeholder} ${isRequired ? '*' : ''}` : label}
required={isRequired}
id={id}
disabled={isDisabled || isLocked}
onInput={onInput}
data-testid={rest.dataTestId ? `input_${rest.dataTestId}` : 'input'}
aria-invalid={status?.type === 'error'}
aria-describedby={hintMessageId}
{...omit(rest, 'dataTestId')}
ref={combinedRefs}
/>
{mask ? (
<InputMask {...inputProps} mask={mask} maskChar={' '} inputRef={combinedRefs} />
) : (
<input {...inputProps} ref={combinedRefs} />
)}
<Label
htmlFor={id}
label={label}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { FC, useState } from 'react';

import TextField from 'components/TextField/TextField';
import { TextInputBaseProps } from 'components/TextInputBase';

type Props = {
mask: string;
} & Partial<Pick<TextInputBaseProps, 'status' | 'isRequired' | 'isDisabled' | 'suffix'>>;

const MaskedTextFieldShowCase: FC<Props> = ({
mask,
status = { type: 'normal' },
isDisabled = false,
suffix,
isRequired = false,
}) => {
const [value, setValue] = useState('');

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) =>
setValue((event.target as HTMLInputElement).value);

return (
<div style={{ width: '500px' }}>
<TextField
label={'Masked TextField'}
value={value}
isDisabled={isDisabled}
isRequired={isRequired}
onChange={handleChange}
suffix={suffix}
mask={mask}
status={status}
/>
</div>
);
};

export default MaskedTextFieldShowCase;
15 changes: 15 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5453,6 +5453,13 @@
dependencies:
"@types/react" "*"

"@types/react-input-mask@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/react-input-mask/-/react-input-mask-3.0.2.tgz#60df645cdb2415c97a8f97316011eb3ede78dc1e"
integrity sha512-WTli3kUyvUqqaOLYG/so2pLqUvRb+n4qnx2He5klfqZDiQmRyD07jVIt/bco/1BrcErkPMtpOm+bHii4Oed6cQ==
dependencies:
"@types/react" "*"

"@types/react-router-dom@^5.1.5":
version "5.1.8"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.8.tgz#bf3e1c8149b3d62eaa206d58599de82df0241192"
Expand Down Expand Up @@ -15462,6 +15469,14 @@ react-input-autosize@^3.0.0:
dependencies:
prop-types "^15.5.8"

react-input-mask@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-input-mask/-/react-input-mask-2.0.4.tgz#9ade5cf8196f4a856dbf010820fe75a795f3eb14"
integrity sha512-1hwzMr/aO9tXfiroiVCx5EtKohKwLk/NT8QlJXHQ4N+yJJFyUuMT+zfTpLBwX/lK3PkuMlievIffncpMZ3HGRQ==
dependencies:
invariant "^2.2.4"
warning "^4.0.2"

react-inspector@^5.1.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-5.1.1.tgz#58476c78fde05d5055646ed8ec02030af42953c8"
Expand Down