-
Notifications
You must be signed in to change notification settings - Fork 72
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
[BD-46] feat: added FormControl input mask #2626
Changes from 6 commits
87699d6
662c5e6
aa37c4f
98726d2
c1c1a1e
e565967
e06aa95
d6dc0e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -43,7 +43,6 @@ or [select attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element | |
} | ||
``` | ||
|
||
|
||
## Input types | ||
|
||
```jsx live | ||
|
@@ -163,6 +162,160 @@ or [select attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element | |
} | ||
``` | ||
|
||
## Input masks | ||
Paragon uses the [react-imask](https://www.npmjs.com/package/react-imask) library, | ||
which allows you to add masks of different types for inputs. | ||
To create your own mask, you need to pass the required mask pattern (`+{1}(000)000-00-00`) to the `inputMask` property. <br /> | ||
See [react-imask](https://imask.js.org) for documentation on available props. | ||
|
||
```jsx live | ||
() => { | ||
{/* start example state */} | ||
const [maskType, setMaskType] = useState('phone'); | ||
{/* end example state */} | ||
|
||
const inputsWithMask = { | ||
phone: ( | ||
<> | ||
<h3>Phone</h3> | ||
<Form.Group> | ||
<Form.Control | ||
inputMask="+{1}(000)000-00-00" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The US-based example phone number mask should likely be |
||
value={value} | ||
onChange={handleChange} | ||
leadingElement={<Icon src={FavoriteBorder} />} | ||
floatingLabel="What is your phone number?" | ||
/> | ||
</Form.Group> | ||
</> | ||
), | ||
creditCard: (<> | ||
<h3>Credit card</h3> | ||
<Form.Group> | ||
<Form.Control | ||
inputMask="0000 0000 0000 0000" | ||
value={value} | ||
onChange={handleChange} | ||
leadingElement={<Icon src={FavoriteBorder} />} | ||
floatingLabel="What is your credit card number?" | ||
/> | ||
</Form.Group> | ||
</>), | ||
date: (<> | ||
<h3>Date</h3> | ||
<Form.Group> | ||
<Form.Control | ||
inputMask={Date} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A bit curious about this one; it appears it doesn't let users input months without leading zeros? For example, when trying to type in June 10, I type Beyond this, I'm also wondering if we should keep the date input mask example of these examples given the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems to me that this is a good solution if the mask requires entering a zero before the month. |
||
value={value} | ||
onChange={handleChange} | ||
leadingElement={<Icon src={FavoriteBorder} />} | ||
floatingLabel="What is your date of birth?" | ||
/> | ||
</Form.Group> | ||
</>), | ||
securePassword: (<> | ||
<h3>Secure password</h3> | ||
<Form.Group> | ||
<Form.Control | ||
inputMask="XXX-XX-0000" | ||
value={value} | ||
definitions={{ | ||
X: { | ||
mask: '0', | ||
displayChar: 'X', | ||
placeholderChar: '#', | ||
}, | ||
}} | ||
onChange={handleChange} | ||
leadingElement={<Icon src={FavoriteBorder} />} | ||
floatingLabel="What is your password?" | ||
/> | ||
</Form.Group> | ||
</>), | ||
OTPpassword: (<> | ||
<h3>OTP password</h3> | ||
<Form.Group> | ||
<Form.Control | ||
inputMask="G-00000" | ||
value={value} | ||
onChange={handleChange} | ||
leadingElement={<Icon src={FavoriteBorder} />} | ||
floatingLabel="What is your OPT password?" | ||
/> | ||
</Form.Group> | ||
</>), | ||
price: ( | ||
<> | ||
<h3>Course prise</h3> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: "Price" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Corrected |
||
<Form.Group> | ||
<Form.Control | ||
inputMask="$num" | ||
blocks={{ | ||
num: { | ||
// nested masks are available! | ||
mask: Number, | ||
thousandsSeparator: ' ' | ||
} | ||
}} | ||
value={value} | ||
onChange={handleChange} | ||
leadingElement={<Icon src={FavoriteBorder} />} | ||
floatingLabel="What is the price of this course?" | ||
/> | ||
</Form.Group> | ||
</> | ||
), | ||
}; | ||
|
||
const [value, setValue] = useState(''); | ||
|
||
const handleChange = (e) => setValue(e.target.value); | ||
|
||
return ( | ||
<> | ||
{/* start example form block */} | ||
<ExamplePropsForm | ||
inputs={[ | ||
{ value: maskType, setValue: setMaskType, options: Object.keys(inputsWithMask), name: 'Mask variants' }, | ||
]} | ||
/> | ||
{/* end example form block */} | ||
|
||
{inputsWithMask[maskType]} | ||
</> | ||
); | ||
} | ||
``` | ||
|
||
## Input masks with clear value | ||
To get a value without a mask, you need to use `onChange` instead of `onAccept` to handle changes. | ||
|
||
```jsx live | ||
() => { | ||
const [value, setValue] = useState(''); | ||
|
||
return ( | ||
<> | ||
<Form.Group> | ||
<Form.Control | ||
inputMask="+{1}(000)000-00-00" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The US-based example phone number mask should likely be |
||
leadingElement={<Icon src={FavoriteBorder} />} | ||
trailingElement={<Icon src={Verified} />} | ||
floatingLabel="What is your phone number?" | ||
value={value} | ||
// depending on prop above first argument is | ||
// `value` if `unmask=false`, | ||
// `unmaskedValue` if `unmask=true`, | ||
// `typedValue` if `unmask='typed'` | ||
onAccept={(_, mask) => setValue(mask._unmaskedValue)} | ||
/> | ||
</Form.Group> | ||
Unmasked value: {JSON.stringify(value)} | ||
</> | ||
); | ||
} | ||
``` | ||
|
||
## Textarea autoResize | ||
|
||
`autoResize` prop allows input to be resized according to the content height. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
import React from 'react'; | ||
import { render, screen } from '@testing-library/react'; | ||
import React, { useState } from 'react'; | ||
import { | ||
render, screen, act, fireEvent, | ||
} from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
|
||
import FormControl from '../FormControl'; | ||
|
@@ -8,6 +10,22 @@ const ref = { | |
current: null, | ||
}; | ||
|
||
// eslint-disable-next-line react/prop-types | ||
function Component({ isClearValue }) { | ||
const onChangeFunc = jest.fn(); | ||
const [inputValue, setInputValue] = useState(''); | ||
|
||
return ( | ||
<FormControl | ||
inputMask="+{1}(000)000-00-00" | ||
value={inputValue} | ||
onChange={isClearValue ? onChangeFunc : (e) => setInputValue(e.target.value)} | ||
onAccept={isClearValue ? onChangeFunc : (value) => setInputValue(value)} | ||
data-testid="form-control-with-mask" | ||
/> | ||
); | ||
} | ||
|
||
describe('FormControl', () => { | ||
it('textarea changes its height with autoResize prop', async () => { | ||
const useReferenceSpy = jest.spyOn(React, 'useRef').mockReturnValue(ref); | ||
|
@@ -31,4 +49,21 @@ describe('FormControl', () => { | |
expect(onChangeFunc).toHaveBeenCalledTimes(inputText.length); | ||
expect(ref.current.style.height).toEqual(`${ref.current.scrollHeight + ref.current.offsetHeight}px`); | ||
}); | ||
|
||
it('should apply and accept input mask for phone numbers', () => { | ||
render(<Component />); | ||
|
||
act(() => { | ||
const input = screen.getByTestId('form-control-with-mask'); | ||
userEvent.type(input, '1234567890'); | ||
expect(input.value).toBe('+1(234)567-89-0'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [curious] generally, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @adamstankiewicz thank you for bringing this issue to our attention 👍 At the moment we have a lot of similar examples of wrapping in
My solution is based on Is wrapping with act now required?. As it turned out, we have differences in the We have several options for further action:
An article about dependency overriding, in which we can make sure that our intentions are correct:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for digging into, this @PKulkoRaccoonGang! TIL that |
||
}); | ||
}); | ||
it('should be cleared from the mask elements value', () => { | ||
render(<Component isClearValue />); | ||
|
||
const input = screen.getByTestId('form-control-with-mask'); | ||
fireEvent.change(input, { target: { value: '1234567890' } }); | ||
expect(input.value).toBe('1234567890'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [curious] Why use |
||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The US-based example phone number mask should likely be
+{1} (000) 000-0000
so that phone numbers are formatted like+1 (555) 555-5555
, which is typical for a US-based phone number.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Corrected, thanks