Adding `FormTextField` component (#16497)
* Adding FormTextField component * Adding to index.js * Adding id, label and helptext stories * Removing unneeded htmlFor and fixing accessibility on helpText story * Fixing issues with review suggestions * Fixing lint issue * Adding snapshot testfeature/default_network_editable
parent
e9508b4f7f
commit
5ee7da6afe
@ -0,0 +1,336 @@ |
|||||||
|
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; |
||||||
|
|
||||||
|
import { TextField, TextFieldBase } from '../'; |
||||||
|
import { FormTextField } from './form-text-field'; |
||||||
|
|
||||||
|
# FormTextField |
||||||
|
|
||||||
|
The `FormTextField` is an input component to create forms. It bundles the [TextField](/docs/ui-components-component-library-text-field-text-field-stories-js--default-story), [Label](/docs/ui-components-component-library-label-label-stories-js--default-story) and [HelpText](/docs/ui-components-component-library-help-text-help-text-stories-js--default-story) components together. |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story id="ui-components-component-library-form-text-field-form-text-field-stories-js--default-story" /> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
## Props |
||||||
|
|
||||||
|
The `FormTextField` accepts all props below as well as all [Box](/docs/ui-components-ui-box-box-stories-js--default-story#props) component props |
||||||
|
|
||||||
|
<ArgsTable of={FormTextField} /> |
||||||
|
|
||||||
|
`FormTextField` accepts all [TextField](/docs/ui-components-component-library-text-field-text-field-stories-js--default-story#props) |
||||||
|
component props |
||||||
|
|
||||||
|
<ArgsTable of={TextField} /> |
||||||
|
|
||||||
|
`FormTextField` accepts all [TextFieldBase](/docs/ui-components-component-library-text-field-base-text-field-base-stories-js--default-story#props) |
||||||
|
component props |
||||||
|
|
||||||
|
<ArgsTable of={TextFieldBase} /> |
||||||
|
|
||||||
|
### Id |
||||||
|
|
||||||
|
Use the `id` prop to set the `id` of the `FormTextField` component. This is required for accessibility when the `label` prop is set. It is also used internally to link the `label` and `input` elements using `htmlFor`, so clicking on the `label` will focus the `input`. |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story id="ui-components-component-library-form-text-field-form-text-field-stories-js--id" /> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
```jsx |
||||||
|
import { FormTextField } from '../../component-library'; |
||||||
|
|
||||||
|
<FormTextField |
||||||
|
id="accessible-input-id" |
||||||
|
label="If label prop exists id prop is required for accessibility" |
||||||
|
/>; |
||||||
|
``` |
||||||
|
|
||||||
|
### Label |
||||||
|
|
||||||
|
Use the `label` prop to add a label to the `FormTextField` component. Uses the [Label](/docs/ui-components-component-library-label-label-stories-js--default-story) component. Use the `labelProps` prop to pass props to the `Label` component. To use a custom label component see the [Custom Label or HelpText](#custom-label-or-helptext) story example. |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story id="ui-components-component-library-form-text-field-form-text-field-stories-js--label-story" /> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
```jsx |
||||||
|
import { FormTextField } from '../../component-library'; |
||||||
|
|
||||||
|
<FormTextField id="input-with-label" label="Label content appears here" />; |
||||||
|
``` |
||||||
|
|
||||||
|
### HelpText |
||||||
|
|
||||||
|
Use the `helpText` prop to add help text to the `FormTextField` component. Uses the [HelpText](/docs/ui-components-component-library-helpText-helpText-stories-js--default-story) component. Use the `helpTextProps` prop to pass props to the `HelpText` component. To use a custom help text component see the [Custom Label or HelpText](#custom-helpText-or-helptext) story example. When `error` is true the `helpText` will be rendered as an error message. |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story id="ui-components-component-library-form-text-field-form-text-field-stories-js--help-text-story" /> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
```jsx |
||||||
|
import { FormTextField } from '../../component-library'; |
||||||
|
|
||||||
|
<FormTextField helpText="HelpText content appears here" />; |
||||||
|
<FormTextField |
||||||
|
error |
||||||
|
helpText="When error is true the help text will be rendered as an error message" |
||||||
|
/>; |
||||||
|
``` |
||||||
|
|
||||||
|
### Form Example |
||||||
|
|
||||||
|
An example of a form using the `FormTextField` component. |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story id="ui-components-component-library-form-text-field-form-text-field-stories-js--form-example" /> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
```jsx |
||||||
|
import React, { useState, useEffect } from 'react'; |
||||||
|
import { |
||||||
|
DISPLAY, |
||||||
|
COLORS, |
||||||
|
ALIGN_ITEMS, |
||||||
|
TEXT, |
||||||
|
} from '../../../helpers/constants/design-system'; |
||||||
|
|
||||||
|
import Box from '../../ui/box/box'; |
||||||
|
|
||||||
|
import { |
||||||
|
ButtonPrimary, |
||||||
|
ButtonSecondary, |
||||||
|
FormTextField, |
||||||
|
ICON_NAMES, |
||||||
|
Text, |
||||||
|
} from '../../component-library'; |
||||||
|
|
||||||
|
const FORM_STATE = { |
||||||
|
DEFAULT: 'default', |
||||||
|
SUCCESS: 'success', |
||||||
|
ERROR: 'error', |
||||||
|
}; |
||||||
|
|
||||||
|
const VALIDATED_VALUES = { |
||||||
|
NETWORK_NAME: 'network name', |
||||||
|
NEW_RPC_URL: 'new rpc url', |
||||||
|
CHAIN_ID: 'chain id', |
||||||
|
}; |
||||||
|
|
||||||
|
const ERROR_MESSAGES = { |
||||||
|
NETWORK_NAME: `Please enter "${VALIDATED_VALUES.NETWORK_NAME}"`, |
||||||
|
NEW_RPC_URL: `Please enter "${VALIDATED_VALUES.NEW_RPC_URL}"`, |
||||||
|
CHAIN_ID: `Please enter "${VALIDATED_VALUES.CHAIN_ID}"`, |
||||||
|
}; |
||||||
|
|
||||||
|
const [submitted, setSubmitted] = useState(FORM_STATE.DEFAULT); |
||||||
|
|
||||||
|
const [values, setValues] = useState({ |
||||||
|
networkName: '', |
||||||
|
newRpcUrl: '', |
||||||
|
chainId: '', |
||||||
|
}); |
||||||
|
|
||||||
|
const [errors, setErrors] = useState({ |
||||||
|
networkName: '', |
||||||
|
newRpcUrl: '', |
||||||
|
chainId: '', |
||||||
|
}); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
setErrors({ |
||||||
|
networkName: |
||||||
|
values.networkName && |
||||||
|
values.networkName.toLowerCase() !== VALIDATED_VALUES.NETWORK_NAME |
||||||
|
? ERROR_MESSAGES.NETWORK_NAME |
||||||
|
: '', |
||||||
|
newRpcUrl: |
||||||
|
values.newRpcUrl && |
||||||
|
values.newRpcUrl.toLowerCase() !== VALIDATED_VALUES.NEW_RPC_URL |
||||||
|
? ERROR_MESSAGES.NEW_RPC_URL |
||||||
|
: '', |
||||||
|
chainId: |
||||||
|
values.chainId && |
||||||
|
values.chainId.toLowerCase() !== VALIDATED_VALUES.CHAIN_ID |
||||||
|
? ERROR_MESSAGES.CHAIN_ID |
||||||
|
: '', |
||||||
|
}); |
||||||
|
}, [values]); |
||||||
|
|
||||||
|
const handleClearForm = () => { |
||||||
|
setValues({ networkName: '', newRpcUrl: '', chainId: '' }); |
||||||
|
setErrors({ networkName: '', newRpcUrl: '', chainId: '' }); |
||||||
|
setSubmitted(FORM_STATE.DEFAULT); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleOnChange = (e) => { |
||||||
|
if (submitted === FORM_STATE.ERROR) { |
||||||
|
setErrors({ networkName: '', newRpcUrl: '', chainId: '' }); |
||||||
|
setSubmitted(FORM_STATE.DEFAULT); |
||||||
|
} |
||||||
|
setValues({ |
||||||
|
...values, |
||||||
|
[e.target.name]: e.target.value, |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleOnSubmit = (e) => { |
||||||
|
e.preventDefault(); |
||||||
|
if (errors.networkName || errors.newRpcUrl || errors.chainId) { |
||||||
|
setSubmitted(FORM_STATE.ERROR); |
||||||
|
} else { |
||||||
|
setSubmitted(FORM_STATE.SUCCESS); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<Box |
||||||
|
as="form" |
||||||
|
onSubmit={handleOnSubmit} |
||||||
|
marginBottom={4} |
||||||
|
style={{ width: '100%', maxWidth: '420px' }} |
||||||
|
> |
||||||
|
<FormTextField |
||||||
|
marginBottom={4} |
||||||
|
label="Network name" |
||||||
|
placeholder="Enter 'network name'" |
||||||
|
required |
||||||
|
name="networkName" |
||||||
|
id="networkName" |
||||||
|
onChange={handleOnChange} |
||||||
|
value={values.networkName} |
||||||
|
error={Boolean(submitted === FORM_STATE.ERROR && errors.networkName)} |
||||||
|
helpText={submitted === FORM_STATE.ERROR ? errors.networkName : null} |
||||||
|
/> |
||||||
|
<FormTextField |
||||||
|
marginBottom={4} |
||||||
|
label="New RPC URL" |
||||||
|
placeholder="Enter 'new RPC URL'" |
||||||
|
required |
||||||
|
name="newRpcUrl" |
||||||
|
id="newRpcUrl" |
||||||
|
onChange={handleOnChange} |
||||||
|
value={values.newRpcUrl} |
||||||
|
error={Boolean(submitted === FORM_STATE.ERROR && errors.newRpcUrl)} |
||||||
|
helpText={submitted === FORM_STATE.ERROR ? errors.newRpcUrl : null} |
||||||
|
/> |
||||||
|
<FormTextField |
||||||
|
label="Chain ID" |
||||||
|
marginBottom={4} |
||||||
|
placeholder="Enter 'chain ID'" |
||||||
|
required |
||||||
|
name="chainId" |
||||||
|
id="chainId" |
||||||
|
onChange={handleOnChange} |
||||||
|
value={values.chainId} |
||||||
|
error={Boolean(submitted === FORM_STATE.ERROR && errors.chainId)} |
||||||
|
helpText={submitted === FORM_STATE.ERROR ? errors.chainId : null} |
||||||
|
/> |
||||||
|
<Box display={DISPLAY.FLEX} alignItems={ALIGN_ITEMS.CENTER} gap={1}> |
||||||
|
<ButtonPrimary type="submit">Submit</ButtonPrimary> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
<ButtonSecondary |
||||||
|
icon={ICON_NAMES.CLOSE_OUTLINE} |
||||||
|
onClick={handleClearForm} |
||||||
|
danger |
||||||
|
> |
||||||
|
Clear form |
||||||
|
</ButtonSecondary> |
||||||
|
{submitted === FORM_STATE.SUCCESS && ( |
||||||
|
<Text variant={TEXT.BODY_LG} color={COLORS.SUCCESS_DEFAULT} marginTop={4}> |
||||||
|
Form successfully submitted! |
||||||
|
</Text> |
||||||
|
)} |
||||||
|
</> |
||||||
|
); |
||||||
|
``` |
||||||
|
|
||||||
|
### Custom Label or HelpText |
||||||
|
|
||||||
|
There will be times when you will want to use a custom `Label` or `HelpText`. This can be done by simply not providing `label` or `helpText` props to the `FormTextField` component. You can then use the `Label` and `HelpText` components to create your own custom label or help text. |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story id="ui-components-component-library-form-text-field-form-text-field-stories-js--custom-label-or-help-text" /> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
```jsx |
||||||
|
import { |
||||||
|
SIZES, |
||||||
|
DISPLAY, |
||||||
|
COLORS, |
||||||
|
ALIGN_ITEMS, |
||||||
|
JUSTIFY_CONTENT, |
||||||
|
} from '../../../helpers/constants/design-system'; |
||||||
|
|
||||||
|
import Box from '../../ui/box/box'; |
||||||
|
|
||||||
|
import { |
||||||
|
ButtonLink, |
||||||
|
FormTextField, |
||||||
|
HelpText, |
||||||
|
ICON_NAMES, |
||||||
|
Icon, |
||||||
|
Label, |
||||||
|
TEXT_FIELD_TYPES, |
||||||
|
Text, |
||||||
|
} from '../../component-library'; |
||||||
|
|
||||||
|
<Text marginBottom={4}> |
||||||
|
Examples of how one might customize the Label or HelpText within the |
||||||
|
FormTextField component |
||||||
|
</Text> |
||||||
|
<Box |
||||||
|
display={DISPLAY.FLEX} |
||||||
|
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN} |
||||||
|
alignItems={ALIGN_ITEMS.FLEX_END} |
||||||
|
> |
||||||
|
<Box display={DISPLAY.FLEX} alignItems={ALIGN_ITEMS.CENTER}> |
||||||
|
{/** |
||||||
|
* If you need a custom label |
||||||
|
* or require adding some form of customization |
||||||
|
* import the Label component separately |
||||||
|
*/} |
||||||
|
<Label htmlFor="custom-spending-cap" required> |
||||||
|
Custom spending cap |
||||||
|
</Label> |
||||||
|
<Icon |
||||||
|
name={ICON_NAMES.INFO_FILLED} |
||||||
|
size={SIZES.SM} |
||||||
|
marginLeft={1} |
||||||
|
color={COLORS.ICON_ALTERNATIVE} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
<ButtonLink size={SIZES.AUTO}>Use default</ButtonLink> |
||||||
|
</Box> |
||||||
|
<FormTextField |
||||||
|
id="custom-spending-cap" |
||||||
|
placeholder="Enter a number" |
||||||
|
rightAccessory={<ButtonLink size={SIZES.AUTO}>Max</ButtonLink>} |
||||||
|
marginBottom={4} |
||||||
|
type={TEXT_FIELD_TYPES.NUMBER} |
||||||
|
/> |
||||||
|
<FormTextField |
||||||
|
label="Swap from" |
||||||
|
placeholder="0" |
||||||
|
type={TEXT_FIELD_TYPES.NUMBER} |
||||||
|
/> |
||||||
|
<Box |
||||||
|
display={DISPLAY.FLEX} |
||||||
|
alignItems={ALIGN_ITEMS.FLEX_START} |
||||||
|
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN} |
||||||
|
> |
||||||
|
{/** |
||||||
|
* If you need a custom help text |
||||||
|
* or require adding some form of customization |
||||||
|
* import the HelpText component separately and handle the error |
||||||
|
* logic yourself |
||||||
|
*/} |
||||||
|
<HelpText htmlFor="chainId" required paddingRight={2} marginTop={1}> |
||||||
|
Only enter a number that you're comfortable with the contract accessing |
||||||
|
now or in the future. You can always increase the token limit later. |
||||||
|
</HelpText> |
||||||
|
<ButtonLink size={SIZES.AUTO} marginLeft="auto" marginTop={1}> |
||||||
|
Max |
||||||
|
</ButtonLink> |
||||||
|
</Box> |
||||||
|
``` |
@ -0,0 +1,21 @@ |
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||||
|
|
||||||
|
exports[`FormTextField should render correctly 1`] = ` |
||||||
|
<div> |
||||||
|
<div |
||||||
|
class="box mm-form-text-field box--display-flex box--flex-direction-column" |
||||||
|
> |
||||||
|
<div |
||||||
|
class="box mm-text-field-base mm-text-field-base--size-md mm-text-field-base--truncate mm-text-field mm-form-text-field__text-field box--display-inline-flex box--flex-direction-row box--align-items-center box--background-color-background-default box--rounded-sm box--border-width-1 box--border-style-solid" |
||||||
|
> |
||||||
|
<input |
||||||
|
autocomplete="off" |
||||||
|
class="box text mm-text-field-base__input text--body-md text--color-text-default box--padding-right-4 box--padding-left-4 box--flex-direction-row box--background-color-transparent" |
||||||
|
focused="false" |
||||||
|
type="text" |
||||||
|
value="" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
`; |
@ -0,0 +1,167 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import PropTypes from 'prop-types'; |
||||||
|
import classnames from 'classnames'; |
||||||
|
|
||||||
|
import { |
||||||
|
DISPLAY, |
||||||
|
FLEX_DIRECTION, |
||||||
|
SIZES, |
||||||
|
} from '../../../helpers/constants/design-system'; |
||||||
|
|
||||||
|
import Box from '../../ui/box/box'; |
||||||
|
|
||||||
|
import { TextField } from '../text-field'; |
||||||
|
import { HelpText } from '../help-text'; |
||||||
|
import { Label } from '../label'; |
||||||
|
|
||||||
|
export const FormTextField = ({ |
||||||
|
autoComplete, |
||||||
|
autoFocus, |
||||||
|
className, |
||||||
|
defaultValue, |
||||||
|
disabled, |
||||||
|
error, |
||||||
|
helpText, |
||||||
|
helpTextProps, |
||||||
|
id, |
||||||
|
inputProps, |
||||||
|
inputRef, |
||||||
|
label, |
||||||
|
labelProps, |
||||||
|
leftAccessory, |
||||||
|
maxLength, |
||||||
|
name, |
||||||
|
onBlur, |
||||||
|
onChange, |
||||||
|
onFocus, |
||||||
|
placeholder, |
||||||
|
readOnly, |
||||||
|
required, |
||||||
|
rightAccessory, |
||||||
|
size = SIZES.MD, |
||||||
|
textFieldProps, |
||||||
|
truncate, |
||||||
|
showClearButton, |
||||||
|
clearButtonOnClick, |
||||||
|
clearButtonProps, |
||||||
|
type = 'text', |
||||||
|
value, |
||||||
|
...props |
||||||
|
}) => ( |
||||||
|
<Box |
||||||
|
className={classnames( |
||||||
|
'mm-form-text-field', |
||||||
|
{ 'mm-form-text-field--disabled': disabled }, |
||||||
|
className, |
||||||
|
)} |
||||||
|
display={DISPLAY.FLEX} |
||||||
|
flexDirection={FLEX_DIRECTION.COLUMN} |
||||||
|
{...props} |
||||||
|
> |
||||||
|
{label && ( |
||||||
|
<Label |
||||||
|
htmlFor={id} |
||||||
|
required={required} |
||||||
|
disabled={disabled} |
||||||
|
{...labelProps} |
||||||
|
> |
||||||
|
{label} |
||||||
|
</Label> |
||||||
|
)} |
||||||
|
<TextField |
||||||
|
className={classnames( |
||||||
|
'mm-form-text-field__text-field', |
||||||
|
textFieldProps?.className, |
||||||
|
)} |
||||||
|
id={id} |
||||||
|
{...{ |
||||||
|
autoComplete, |
||||||
|
autoFocus, |
||||||
|
defaultValue, |
||||||
|
disabled, |
||||||
|
error, |
||||||
|
id, |
||||||
|
inputProps, |
||||||
|
inputRef, |
||||||
|
leftAccessory, |
||||||
|
maxLength, |
||||||
|
name, |
||||||
|
onBlur, |
||||||
|
onChange, |
||||||
|
onFocus, |
||||||
|
placeholder, |
||||||
|
readOnly, |
||||||
|
required, |
||||||
|
rightAccessory, |
||||||
|
showClearButton, |
||||||
|
clearButtonOnClick, |
||||||
|
clearButtonProps, |
||||||
|
size, |
||||||
|
truncate, |
||||||
|
type, |
||||||
|
value, |
||||||
|
...textFieldProps, |
||||||
|
}} |
||||||
|
/> |
||||||
|
{helpText && ( |
||||||
|
<HelpText |
||||||
|
className={classnames( |
||||||
|
'mm-form-text-field__help-text', |
||||||
|
helpTextProps?.className, |
||||||
|
)} |
||||||
|
error={error} |
||||||
|
marginTop={1} |
||||||
|
{...helpTextProps} |
||||||
|
> |
||||||
|
{helpText} |
||||||
|
</HelpText> |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
); |
||||||
|
|
||||||
|
FormTextField.propTypes = { |
||||||
|
/** |
||||||
|
* An additional className to apply to the form-text-field |
||||||
|
*/ |
||||||
|
className: PropTypes.string, |
||||||
|
/** |
||||||
|
* The id of the FormTextField |
||||||
|
* Required if label prop exists to ensure accessibility |
||||||
|
* |
||||||
|
* @param {object} props - The props passed to the component. |
||||||
|
* @param {string} propName - The prop name in this case 'id'. |
||||||
|
* @param {string} componentName - The name of the component. |
||||||
|
*/ |
||||||
|
id: (props, propName, componentName) => { |
||||||
|
if (props.label && !props[propName]) { |
||||||
|
return new Error( |
||||||
|
`If a label prop exists you must provide an ${propName} prop for the label's htmlFor attribute for accessibility. Warning coming from ${componentName} ui/components/component-library/form-text-field/form-text-field.js`, |
||||||
|
); |
||||||
|
} |
||||||
|
return null; |
||||||
|
}, |
||||||
|
/** |
||||||
|
* The content of the Label component |
||||||
|
*/ |
||||||
|
label: PropTypes.string, |
||||||
|
/** |
||||||
|
* Props that are applied to the Label component |
||||||
|
*/ |
||||||
|
labelProps: PropTypes.object, |
||||||
|
/** |
||||||
|
* The content of the HelpText component |
||||||
|
*/ |
||||||
|
helpText: PropTypes.string, |
||||||
|
/** |
||||||
|
* Props that are applied to the HelpText component |
||||||
|
*/ |
||||||
|
helpTextProps: PropTypes.object, |
||||||
|
/** |
||||||
|
* Props that are applied to the TextField component |
||||||
|
*/ |
||||||
|
textFieldProps: PropTypes.object, |
||||||
|
/** |
||||||
|
* FormTextField accepts all the props from TextField and Box |
||||||
|
*/ |
||||||
|
...TextField.propTypes, |
||||||
|
}; |
@ -0,0 +1,9 @@ |
|||||||
|
.mm-form-text-field { |
||||||
|
--help-text-opacity-disabled: 0.5; |
||||||
|
|
||||||
|
&--disabled { |
||||||
|
.mm-form-text-field__help-text { |
||||||
|
opacity: var(--help-text-opacity-disabled); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,481 @@ |
|||||||
|
import React, { useState, useEffect } from 'react'; |
||||||
|
import { useArgs } from '@storybook/client-api'; |
||||||
|
|
||||||
|
import { |
||||||
|
SIZES, |
||||||
|
DISPLAY, |
||||||
|
COLORS, |
||||||
|
ALIGN_ITEMS, |
||||||
|
TEXT, |
||||||
|
JUSTIFY_CONTENT, |
||||||
|
} from '../../../helpers/constants/design-system'; |
||||||
|
|
||||||
|
import Box from '../../ui/box/box'; |
||||||
|
|
||||||
|
import { |
||||||
|
ButtonLink, |
||||||
|
ButtonPrimary, |
||||||
|
ButtonSecondary, |
||||||
|
HelpText, |
||||||
|
Icon, |
||||||
|
ICON_NAMES, |
||||||
|
Label, |
||||||
|
Text, |
||||||
|
TEXT_FIELD_SIZES, |
||||||
|
TEXT_FIELD_TYPES, |
||||||
|
} from '..'; |
||||||
|
|
||||||
|
import { FormTextField } from './form-text-field'; |
||||||
|
|
||||||
|
import README from './README.mdx'; |
||||||
|
|
||||||
|
const marginSizeControlOptions = [ |
||||||
|
undefined, |
||||||
|
0, |
||||||
|
1, |
||||||
|
2, |
||||||
|
3, |
||||||
|
4, |
||||||
|
5, |
||||||
|
6, |
||||||
|
7, |
||||||
|
8, |
||||||
|
9, |
||||||
|
10, |
||||||
|
11, |
||||||
|
12, |
||||||
|
'auto', |
||||||
|
]; |
||||||
|
|
||||||
|
export default { |
||||||
|
title: 'Components/ComponentLibrary/FormTextField', |
||||||
|
id: __filename, |
||||||
|
component: FormTextField, |
||||||
|
parameters: { |
||||||
|
docs: { |
||||||
|
page: README, |
||||||
|
}, |
||||||
|
}, |
||||||
|
argTypes: { |
||||||
|
value: { |
||||||
|
control: 'text', |
||||||
|
}, |
||||||
|
onChange: { |
||||||
|
action: 'onChange', |
||||||
|
}, |
||||||
|
labelProps: { |
||||||
|
control: 'object', |
||||||
|
}, |
||||||
|
textFieldProps: { |
||||||
|
control: 'object', |
||||||
|
}, |
||||||
|
helpTextProps: { |
||||||
|
control: 'object', |
||||||
|
}, |
||||||
|
showClearButton: { |
||||||
|
control: 'boolean', |
||||||
|
table: { category: 'text field props' }, |
||||||
|
}, |
||||||
|
clearButtonOnClick: { |
||||||
|
action: 'clearButtonOnClick', |
||||||
|
table: { category: 'text field props' }, |
||||||
|
}, |
||||||
|
clearButtonProps: { |
||||||
|
control: 'object', |
||||||
|
table: { category: 'text field props' }, |
||||||
|
}, |
||||||
|
autoComplete: { |
||||||
|
control: 'boolean', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
autoFocus: { |
||||||
|
control: 'boolean', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
className: { |
||||||
|
control: 'text', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
disabled: { |
||||||
|
control: 'boolean', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
error: { |
||||||
|
control: 'boolean', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
id: { |
||||||
|
control: 'text', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
inputProps: { |
||||||
|
control: 'object', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
leftAccessory: { |
||||||
|
control: 'text', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
maxLength: { |
||||||
|
control: 'number', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
name: { |
||||||
|
control: 'text', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
onBlur: { |
||||||
|
action: 'onBlur', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
onClick: { |
||||||
|
action: 'onClick', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
onFocus: { |
||||||
|
action: 'onFocus', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
onKeyDown: { |
||||||
|
action: 'onKeyDown', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
onKeyUp: { |
||||||
|
action: 'onKeyUp', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
placeholder: { |
||||||
|
control: 'text', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
readOnly: { |
||||||
|
control: 'boolean', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
required: { |
||||||
|
control: 'boolean', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
rightAccessory: { |
||||||
|
control: 'text', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
size: { |
||||||
|
control: 'select', |
||||||
|
options: Object.values(TEXT_FIELD_SIZES), |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
type: { |
||||||
|
control: 'select', |
||||||
|
options: Object.values(TEXT_FIELD_TYPES), |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
truncate: { |
||||||
|
control: 'boolean', |
||||||
|
table: { category: 'text field base props' }, |
||||||
|
}, |
||||||
|
marginTop: { |
||||||
|
options: marginSizeControlOptions, |
||||||
|
control: 'select', |
||||||
|
table: { category: 'box props' }, |
||||||
|
}, |
||||||
|
marginRight: { |
||||||
|
options: marginSizeControlOptions, |
||||||
|
control: 'select', |
||||||
|
table: { category: 'box props' }, |
||||||
|
}, |
||||||
|
marginBottom: { |
||||||
|
options: marginSizeControlOptions, |
||||||
|
control: 'select', |
||||||
|
table: { category: 'box props' }, |
||||||
|
}, |
||||||
|
marginLeft: { |
||||||
|
options: marginSizeControlOptions, |
||||||
|
control: 'select', |
||||||
|
table: { category: 'box props' }, |
||||||
|
}, |
||||||
|
}, |
||||||
|
args: { |
||||||
|
placeholder: 'Form text field', |
||||||
|
label: 'Label', |
||||||
|
id: 'form-text-field', |
||||||
|
helpText: 'Help text', |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
const Template = (args) => { |
||||||
|
const [{ value }, updateArgs] = useArgs(); |
||||||
|
const handleOnChange = (e) => { |
||||||
|
updateArgs({ value: e.target.value }); |
||||||
|
}; |
||||||
|
const handleOnClear = () => { |
||||||
|
updateArgs({ value: '' }); |
||||||
|
}; |
||||||
|
return ( |
||||||
|
<FormTextField |
||||||
|
{...args} |
||||||
|
value={value} |
||||||
|
onChange={handleOnChange} |
||||||
|
clearButtonOnClick={handleOnClear} |
||||||
|
/> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export const DefaultStory = Template.bind({}); |
||||||
|
DefaultStory.storyName = 'Default'; |
||||||
|
|
||||||
|
export const Id = Template.bind({}); |
||||||
|
Id.args = { |
||||||
|
id: 'accessible-input-id', |
||||||
|
label: 'If label prop exists id prop is required for accessibility', |
||||||
|
helpText: '', |
||||||
|
}; |
||||||
|
|
||||||
|
export const LabelStory = Template.bind({}); |
||||||
|
LabelStory.storyName = 'Label'; // Need to use LabelStory to avoid conflict with Label component
|
||||||
|
LabelStory.args = { |
||||||
|
id: 'input-with-label', |
||||||
|
label: 'Label content appears here', |
||||||
|
helpText: '', |
||||||
|
}; |
||||||
|
|
||||||
|
export const HelpTextStory = (args) => { |
||||||
|
const [{ value }, updateArgs] = useArgs(); |
||||||
|
const handleOnChange = (e) => { |
||||||
|
updateArgs({ value: e.target.value }); |
||||||
|
}; |
||||||
|
const handleOnClear = () => { |
||||||
|
updateArgs({ value: '' }); |
||||||
|
}; |
||||||
|
return ( |
||||||
|
<> |
||||||
|
<FormTextField |
||||||
|
{...args} |
||||||
|
id="input-with-help-text" |
||||||
|
value={value} |
||||||
|
onChange={handleOnChange} |
||||||
|
clearButtonOnClick={handleOnClear} |
||||||
|
marginBottom={4} |
||||||
|
/> |
||||||
|
<FormTextField |
||||||
|
{...args} |
||||||
|
id="input-with-help-text-as-error" |
||||||
|
error |
||||||
|
helpText="When error is true the help text will be rendered as an error message" |
||||||
|
value={value} |
||||||
|
onChange={handleOnChange} |
||||||
|
clearButtonOnClick={handleOnClear} |
||||||
|
/> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
||||||
|
HelpTextStory.storyName = 'HelpText'; // Need to use HelpTextStory to avoid conflict with HelpTextStory component
|
||||||
|
HelpTextStory.args = { |
||||||
|
label: '', |
||||||
|
helpText: 'HelpText content appears here', |
||||||
|
}; |
||||||
|
|
||||||
|
export const FormExample = () => { |
||||||
|
const FORM_STATE = { |
||||||
|
DEFAULT: 'default', |
||||||
|
SUCCESS: 'success', |
||||||
|
ERROR: 'error', |
||||||
|
}; |
||||||
|
const VALIDATED_VALUES = { |
||||||
|
NETWORK_NAME: 'network name', |
||||||
|
NEW_RPC_URL: 'new rpc url', |
||||||
|
CHAIN_ID: 'chain id', |
||||||
|
}; |
||||||
|
const ERROR_MESSAGES = { |
||||||
|
NETWORK_NAME: `Please enter "${VALIDATED_VALUES.NETWORK_NAME}"`, |
||||||
|
NEW_RPC_URL: `Please enter "${VALIDATED_VALUES.NEW_RPC_URL}"`, |
||||||
|
CHAIN_ID: `Please enter "${VALIDATED_VALUES.CHAIN_ID}"`, |
||||||
|
}; |
||||||
|
const [submitted, setSubmitted] = useState(FORM_STATE.DEFAULT); |
||||||
|
const [values, setValues] = useState({ |
||||||
|
networkName: '', |
||||||
|
newRpcUrl: '', |
||||||
|
chainId: '', |
||||||
|
}); |
||||||
|
const [errors, setErrors] = useState({ |
||||||
|
networkName: '', |
||||||
|
newRpcUrl: '', |
||||||
|
chainId: '', |
||||||
|
}); |
||||||
|
useEffect(() => { |
||||||
|
setErrors({ |
||||||
|
networkName: |
||||||
|
values.networkName && |
||||||
|
values.networkName.toLowerCase() !== VALIDATED_VALUES.NETWORK_NAME |
||||||
|
? ERROR_MESSAGES.NETWORK_NAME |
||||||
|
: '', |
||||||
|
newRpcUrl: |
||||||
|
values.newRpcUrl && |
||||||
|
values.newRpcUrl.toLowerCase() !== VALIDATED_VALUES.NEW_RPC_URL |
||||||
|
? ERROR_MESSAGES.NEW_RPC_URL |
||||||
|
: '', |
||||||
|
chainId: |
||||||
|
values.chainId && |
||||||
|
values.chainId.toLowerCase() !== VALIDATED_VALUES.CHAIN_ID |
||||||
|
? ERROR_MESSAGES.CHAIN_ID |
||||||
|
: '', |
||||||
|
}); |
||||||
|
}, [ |
||||||
|
values, |
||||||
|
ERROR_MESSAGES.CHAIN_ID, |
||||||
|
ERROR_MESSAGES.NETWORK_NAME, |
||||||
|
ERROR_MESSAGES.NEW_RPC_URL, |
||||||
|
VALIDATED_VALUES.CHAIN_ID, |
||||||
|
VALIDATED_VALUES.NETWORK_NAME, |
||||||
|
VALIDATED_VALUES.NEW_RPC_URL, |
||||||
|
]); |
||||||
|
const handleClearForm = () => { |
||||||
|
setValues({ networkName: '', newRpcUrl: '', chainId: '' }); |
||||||
|
setErrors({ networkName: '', newRpcUrl: '', chainId: '' }); |
||||||
|
setSubmitted(FORM_STATE.DEFAULT); |
||||||
|
}; |
||||||
|
const handleOnChange = (e) => { |
||||||
|
if (submitted === FORM_STATE.ERROR) { |
||||||
|
setErrors({ networkName: '', newRpcUrl: '', chainId: '' }); |
||||||
|
setSubmitted(FORM_STATE.DEFAULT); |
||||||
|
} |
||||||
|
setValues({ |
||||||
|
...values, |
||||||
|
[e.target.name]: e.target.value, |
||||||
|
}); |
||||||
|
}; |
||||||
|
const handleOnSubmit = (e) => { |
||||||
|
e.preventDefault(); |
||||||
|
if (errors.networkName || errors.newRpcUrl || errors.chainId) { |
||||||
|
setSubmitted(FORM_STATE.ERROR); |
||||||
|
} else { |
||||||
|
setSubmitted(FORM_STATE.SUCCESS); |
||||||
|
} |
||||||
|
}; |
||||||
|
return ( |
||||||
|
<> |
||||||
|
<Box |
||||||
|
as="form" |
||||||
|
onSubmit={handleOnSubmit} |
||||||
|
marginBottom={4} |
||||||
|
style={{ width: '100%', maxWidth: '420px' }} |
||||||
|
> |
||||||
|
<FormTextField |
||||||
|
marginBottom={4} |
||||||
|
label="Network name" |
||||||
|
placeholder="Enter 'network name'" |
||||||
|
required |
||||||
|
name="networkName" |
||||||
|
id="networkName" |
||||||
|
onChange={handleOnChange} |
||||||
|
value={values.networkName} |
||||||
|
error={Boolean(submitted === FORM_STATE.ERROR && errors.networkName)} |
||||||
|
helpText={submitted === FORM_STATE.ERROR ? errors.networkName : null} |
||||||
|
/> |
||||||
|
<FormTextField |
||||||
|
marginBottom={4} |
||||||
|
label="New RPC URL" |
||||||
|
placeholder="Enter 'new RPC URL'" |
||||||
|
required |
||||||
|
name="newRpcUrl" |
||||||
|
id="newRpcUrl" |
||||||
|
onChange={handleOnChange} |
||||||
|
value={values.newRpcUrl} |
||||||
|
error={Boolean(submitted === FORM_STATE.ERROR && errors.newRpcUrl)} |
||||||
|
helpText={submitted === FORM_STATE.ERROR ? errors.newRpcUrl : null} |
||||||
|
/> |
||||||
|
<FormTextField |
||||||
|
label="Chain ID" |
||||||
|
marginBottom={4} |
||||||
|
placeholder="Enter 'chain ID'" |
||||||
|
required |
||||||
|
name="chainId" |
||||||
|
id="chainId" |
||||||
|
onChange={handleOnChange} |
||||||
|
value={values.chainId} |
||||||
|
error={Boolean(submitted === FORM_STATE.ERROR && errors.chainId)} |
||||||
|
helpText={submitted === FORM_STATE.ERROR ? errors.chainId : null} |
||||||
|
/> |
||||||
|
<Box display={DISPLAY.FLEX} alignItems={ALIGN_ITEMS.CENTER} gap={1}> |
||||||
|
<ButtonPrimary type="submit">Submit</ButtonPrimary> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
<ButtonSecondary |
||||||
|
icon={ICON_NAMES.CLOSE_OUTLINE} |
||||||
|
onClick={handleClearForm} |
||||||
|
danger |
||||||
|
> |
||||||
|
Clear form |
||||||
|
</ButtonSecondary> |
||||||
|
{submitted === FORM_STATE.SUCCESS && ( |
||||||
|
<Text |
||||||
|
variant={TEXT.BODY_LG} |
||||||
|
color={COLORS.SUCCESS_DEFAULT} |
||||||
|
marginTop={4} |
||||||
|
> |
||||||
|
Form successfully submitted! |
||||||
|
</Text> |
||||||
|
)} |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export const CustomLabelOrHelpText = () => ( |
||||||
|
<> |
||||||
|
<Text marginBottom={4}> |
||||||
|
Examples of how one might customize the Label or HelpText within the |
||||||
|
FormTextField component |
||||||
|
</Text> |
||||||
|
<Box |
||||||
|
display={DISPLAY.FLEX} |
||||||
|
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN} |
||||||
|
alignItems={ALIGN_ITEMS.FLEX_END} |
||||||
|
> |
||||||
|
<Box display={DISPLAY.FLEX} alignItems={ALIGN_ITEMS.CENTER}> |
||||||
|
{/* If you need a custom label |
||||||
|
or require adding some form of customization |
||||||
|
import the Label component separately */} |
||||||
|
<Label htmlFor="custom-spending-cap" required> |
||||||
|
Custom spending cap |
||||||
|
</Label> |
||||||
|
<Icon |
||||||
|
name={ICON_NAMES.INFO_FILLED} |
||||||
|
size={SIZES.SM} |
||||||
|
marginLeft={1} |
||||||
|
color={COLORS.ICON_ALTERNATIVE} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
<ButtonLink size={SIZES.AUTO}>Use default</ButtonLink> |
||||||
|
</Box> |
||||||
|
<FormTextField |
||||||
|
id="custom-spending-cap" |
||||||
|
placeholder="Enter a number" |
||||||
|
rightAccessory={<ButtonLink size={SIZES.AUTO}>Max</ButtonLink>} |
||||||
|
marginBottom={4} |
||||||
|
type={TEXT_FIELD_TYPES.NUMBER} |
||||||
|
/> |
||||||
|
<FormTextField |
||||||
|
label="Swap from" |
||||||
|
placeholder="0" |
||||||
|
type={TEXT_FIELD_TYPES.NUMBER} |
||||||
|
/> |
||||||
|
<Box |
||||||
|
display={DISPLAY.FLEX} |
||||||
|
alignItems={ALIGN_ITEMS.FLEX_START} |
||||||
|
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN} |
||||||
|
> |
||||||
|
{/* If you need a custom help text |
||||||
|
or require adding some form of customization |
||||||
|
import the HelpText component separately and handle the error |
||||||
|
logic yourself */} |
||||||
|
<HelpText paddingRight={2} marginTop={1}> |
||||||
|
Only enter a number that you're comfortable with the contract |
||||||
|
accessing now or in the future. You can always increase the token limit |
||||||
|
later. |
||||||
|
</HelpText> |
||||||
|
<ButtonLink size={SIZES.AUTO} marginLeft="auto" marginTop={1}> |
||||||
|
Max |
||||||
|
</ButtonLink> |
||||||
|
</Box> |
||||||
|
</> |
||||||
|
); |
@ -0,0 +1,349 @@ |
|||||||
|
/* eslint-disable jest/require-top-level-describe */ |
||||||
|
import React from 'react'; |
||||||
|
import { fireEvent, render } from '@testing-library/react'; |
||||||
|
|
||||||
|
import { |
||||||
|
renderControlledInput, |
||||||
|
renderWithUserEvent, |
||||||
|
} from '../../../../test/lib/render-helpers'; |
||||||
|
|
||||||
|
import { SIZES } from '../../../helpers/constants/design-system'; |
||||||
|
|
||||||
|
import { FormTextField } from './form-text-field'; |
||||||
|
|
||||||
|
describe('FormTextField', () => { |
||||||
|
it('should render correctly', () => { |
||||||
|
const { getByRole, container } = render(<FormTextField />); |
||||||
|
expect(getByRole('textbox')).toBeDefined(); |
||||||
|
expect(container).toMatchSnapshot(); |
||||||
|
}); |
||||||
|
// autoComplete
|
||||||
|
it('should render with autoComplete', () => { |
||||||
|
const { getByTestId } = render( |
||||||
|
<FormTextField |
||||||
|
autoComplete |
||||||
|
inputProps={{ 'data-testid': 'form-text-field-auto-complete' }} |
||||||
|
/>, |
||||||
|
); |
||||||
|
expect(getByTestId('form-text-field-auto-complete')).toHaveAttribute( |
||||||
|
'autocomplete', |
||||||
|
'on', |
||||||
|
); |
||||||
|
}); |
||||||
|
// autoFocus
|
||||||
|
it('should render with autoFocus', () => { |
||||||
|
const { getByRole } = render(<FormTextField autoFocus />); |
||||||
|
expect(getByRole('textbox')).toHaveFocus(); |
||||||
|
}); |
||||||
|
// className
|
||||||
|
it('should render with custom className', () => { |
||||||
|
const { getByTestId } = render( |
||||||
|
<FormTextField data-testid="form-text-field" className="test-class" />, |
||||||
|
); |
||||||
|
expect(getByTestId('form-text-field')).toHaveClass('test-class'); |
||||||
|
}); |
||||||
|
// defaultValue
|
||||||
|
it('should render with a defaultValue', () => { |
||||||
|
const { getByRole } = render( |
||||||
|
<FormTextField defaultValue="default value" />, |
||||||
|
); |
||||||
|
expect(getByRole('textbox').value).toBe('default value'); |
||||||
|
}); |
||||||
|
// disabled
|
||||||
|
it('should render in disabled state and not focus or be clickable', async () => { |
||||||
|
const mockOnClick = jest.fn(); |
||||||
|
const mockOnFocus = jest.fn(); |
||||||
|
const { getByRole, user, getByLabelText } = renderWithUserEvent( |
||||||
|
<FormTextField |
||||||
|
label="test label" |
||||||
|
id="test-id" |
||||||
|
disabled |
||||||
|
onFocus={mockOnFocus} |
||||||
|
onClick={mockOnClick} |
||||||
|
/>, |
||||||
|
); |
||||||
|
|
||||||
|
await user.click(getByLabelText('test label')); |
||||||
|
expect(mockOnFocus).toHaveBeenCalledTimes(0); |
||||||
|
await user.type(getByRole('textbox'), 'test value'); |
||||||
|
expect(getByRole('textbox')).not.toHaveValue('test value'); |
||||||
|
|
||||||
|
expect(getByRole('textbox')).toBeDisabled(); |
||||||
|
expect(mockOnClick).toHaveBeenCalledTimes(0); |
||||||
|
expect(mockOnFocus).toHaveBeenCalledTimes(0); |
||||||
|
}); |
||||||
|
// error
|
||||||
|
it('should render with error classNames on TextField and HelpText components when error is true', () => { |
||||||
|
const { getByTestId, getByText } = render( |
||||||
|
<FormTextField |
||||||
|
error |
||||||
|
textFieldProps={{ 'data-testid': 'text-field' }} |
||||||
|
helpText="test help text" |
||||||
|
/>, |
||||||
|
); |
||||||
|
expect(getByTestId('text-field')).toHaveClass('mm-text-field-base--error'); |
||||||
|
expect(getByText('test help text')).toHaveClass( |
||||||
|
'text--color-error-default', |
||||||
|
); |
||||||
|
}); |
||||||
|
// helpText
|
||||||
|
it('should render with helpText', () => { |
||||||
|
const { getByText } = render(<FormTextField helpText="test help text" />); |
||||||
|
expect(getByText('test help text')).toBeDefined(); |
||||||
|
}); |
||||||
|
// helpTextProps
|
||||||
|
it('should render with helpText and helpTextProps', () => { |
||||||
|
const { getByText, getByTestId } = render( |
||||||
|
<FormTextField |
||||||
|
helpText="test help text" |
||||||
|
helpTextProps={{ 'data-testid': 'help-text-test' }} |
||||||
|
/>, |
||||||
|
); |
||||||
|
expect(getByText('test help text')).toBeDefined(); |
||||||
|
expect(getByTestId('help-text-test')).toBeDefined(); |
||||||
|
}); |
||||||
|
// id
|
||||||
|
it('should render the FormTextField with an id and pass it to input and Label as htmlFor. When clicking on Label the input should have focus', async () => { |
||||||
|
const onFocus = jest.fn(); |
||||||
|
const { getByRole, getByLabelText, user } = renderWithUserEvent( |
||||||
|
<FormTextField label="test label" id="test-id" onFocus={onFocus} />, |
||||||
|
); |
||||||
|
expect(getByRole('textbox')).toHaveAttribute('id', 'test-id'); |
||||||
|
await user.click(getByLabelText('test label')); |
||||||
|
expect(onFocus).toHaveBeenCalledTimes(1); |
||||||
|
expect(getByRole('textbox')).toHaveFocus(); |
||||||
|
}); |
||||||
|
// inputProps
|
||||||
|
it('should render with inputProps', () => { |
||||||
|
const { getByTestId } = render( |
||||||
|
<FormTextField inputProps={{ 'data-testid': 'test-id' }} />, |
||||||
|
); |
||||||
|
expect(getByTestId('test-id')).toBeDefined(); |
||||||
|
}); |
||||||
|
// inputRef
|
||||||
|
it('should render with working ref using inputRef prop', () => { |
||||||
|
// Because the 'ref' attribute wont flow down to the DOM
|
||||||
|
// I'm not exactly sure how to test this?
|
||||||
|
const mockRef = jest.fn(); |
||||||
|
const { getByRole } = render(<FormTextField inputRef={mockRef} />); |
||||||
|
expect(getByRole('textbox')).toBeDefined(); |
||||||
|
expect(mockRef).toHaveBeenCalledTimes(1); |
||||||
|
}); |
||||||
|
// label
|
||||||
|
it('should render with a label', () => { |
||||||
|
const { getByLabelText } = render( |
||||||
|
<FormTextField id="test-id" label="test label" />, |
||||||
|
); |
||||||
|
expect(getByLabelText('test label')).toBeDefined(); |
||||||
|
}); |
||||||
|
// labelProps
|
||||||
|
it('should render with a labelProps', () => { |
||||||
|
const { getByTestId, getByLabelText } = render( |
||||||
|
<FormTextField |
||||||
|
label="test label" |
||||||
|
labelProps={{ 'data-testid': 'label-test-id' }} |
||||||
|
id="test-id" |
||||||
|
/>, |
||||||
|
); |
||||||
|
expect(getByLabelText('test label')).toBeDefined(); |
||||||
|
expect(getByTestId('label-test-id')).toBeDefined(); |
||||||
|
}); |
||||||
|
// leftAccessory, // rightAccessory
|
||||||
|
it('should render with right and left accessories', () => { |
||||||
|
const { getByRole, getByText } = render( |
||||||
|
<FormTextField |
||||||
|
leftAccessory={<div>left accessory</div>} |
||||||
|
rightAccessory={<div>right accessory</div>} |
||||||
|
/>, |
||||||
|
); |
||||||
|
expect(getByRole('textbox')).toBeDefined(); |
||||||
|
expect(getByText('left accessory')).toBeDefined(); |
||||||
|
expect(getByText('right accessory')).toBeDefined(); |
||||||
|
}); |
||||||
|
// maxLength;
|
||||||
|
it('should render with maxLength and not allow more than the set characters', async () => { |
||||||
|
const { getByRole, user } = renderWithUserEvent( |
||||||
|
<FormTextField maxLength={5} />, |
||||||
|
); |
||||||
|
const formTextField = getByRole('textbox'); |
||||||
|
await user.type(formTextField, '1234567890'); |
||||||
|
expect(getByRole('textbox')).toBeDefined(); |
||||||
|
expect(formTextField.maxLength).toBe(5); |
||||||
|
expect(formTextField.value).toBe('12345'); |
||||||
|
expect(formTextField.value).toHaveLength(5); |
||||||
|
}); |
||||||
|
// name
|
||||||
|
it('should render with name prop', () => { |
||||||
|
const { getByRole } = render(<FormTextField name="test-name" />); |
||||||
|
expect(getByRole('textbox')).toHaveAttribute('name', 'test-name'); |
||||||
|
}); |
||||||
|
// onBlur, // onFocus
|
||||||
|
it('should render and fire onFocus and onBlur events', async () => { |
||||||
|
const onFocus = jest.fn(); |
||||||
|
const onBlur = jest.fn(); |
||||||
|
const { getByTestId, user } = renderWithUserEvent( |
||||||
|
<FormTextField |
||||||
|
inputProps={{ 'data-testid': 'form-text-field' }} |
||||||
|
onFocus={onFocus} |
||||||
|
onBlur={onBlur} |
||||||
|
/>, |
||||||
|
); |
||||||
|
const formTextField = getByTestId('form-text-field'); |
||||||
|
|
||||||
|
await user.click(formTextField); |
||||||
|
expect(onFocus).toHaveBeenCalledTimes(1); |
||||||
|
fireEvent.blur(formTextField); |
||||||
|
expect(onBlur).toHaveBeenCalledTimes(1); |
||||||
|
}); |
||||||
|
// onChange
|
||||||
|
it('should render and fire onChange event', async () => { |
||||||
|
const onChange = jest.fn(); |
||||||
|
const { user, getByRole } = renderWithUserEvent( |
||||||
|
<FormTextField onChange={onChange} />, |
||||||
|
); |
||||||
|
await user.type(getByRole('textbox'), 'test'); |
||||||
|
expect(onChange).toHaveBeenCalledTimes(4); |
||||||
|
}); |
||||||
|
// placeholder
|
||||||
|
it('should render with placeholder', () => { |
||||||
|
const { getByTestId } = render( |
||||||
|
<FormTextField |
||||||
|
placeholder="test placeholder" |
||||||
|
inputProps={{ 'data-testid': 'form-text-field-auto-complete' }} |
||||||
|
/>, |
||||||
|
); |
||||||
|
expect(getByTestId('form-text-field-auto-complete')).toHaveAttribute( |
||||||
|
'placeholder', |
||||||
|
'test placeholder', |
||||||
|
); |
||||||
|
}); |
||||||
|
// readOnly
|
||||||
|
it('should render with readOnly attr when readOnly is true', async () => { |
||||||
|
const { getByRole, user } = renderWithUserEvent( |
||||||
|
<FormTextField |
||||||
|
readOnly |
||||||
|
value="test value" |
||||||
|
data-testid="read-only" |
||||||
|
inputProps={{ 'data-testid': 'text-field-base-readonly' }} |
||||||
|
/>, |
||||||
|
); |
||||||
|
await user.type(getByRole('textbox'), 'test'); |
||||||
|
expect(getByRole('textbox')).toHaveValue('test value'); |
||||||
|
expect(getByRole('textbox')).toHaveAttribute('readonly', ''); |
||||||
|
}); |
||||||
|
// required
|
||||||
|
it('should render with required asterisk after Label', () => { |
||||||
|
const { getByTestId } = render( |
||||||
|
<FormTextField |
||||||
|
required |
||||||
|
label="test label" |
||||||
|
labelProps={{ 'data-testid': 'label-test-id' }} |
||||||
|
/>, |
||||||
|
); |
||||||
|
expect(getByTestId('label-test-id')).toHaveTextContent('test label*'); |
||||||
|
}); |
||||||
|
// size = SIZES.MD
|
||||||
|
it('should render with different size classes', () => { |
||||||
|
const { getByTestId } = render( |
||||||
|
<> |
||||||
|
<FormTextField |
||||||
|
size={SIZES.SM} |
||||||
|
textFieldProps={{ 'data-testid': 'sm' }} |
||||||
|
/> |
||||||
|
<FormTextField |
||||||
|
size={SIZES.MD} |
||||||
|
textFieldProps={{ 'data-testid': 'md' }} |
||||||
|
/> |
||||||
|
<FormTextField |
||||||
|
size={SIZES.LG} |
||||||
|
textFieldProps={{ 'data-testid': 'lg' }} |
||||||
|
/> |
||||||
|
</>, |
||||||
|
); |
||||||
|
expect(getByTestId('sm')).toHaveClass('mm-text-field-base--size-sm'); |
||||||
|
expect(getByTestId('md')).toHaveClass('mm-text-field-base--size-md'); |
||||||
|
expect(getByTestId('lg')).toHaveClass('mm-text-field-base--size-lg'); |
||||||
|
}); |
||||||
|
// textFieldProps
|
||||||
|
it('should render with textFieldProps', () => { |
||||||
|
const { getByTestId } = render( |
||||||
|
<FormTextField textFieldProps={{ 'data-testid': 'test-text-field' }} />, |
||||||
|
); |
||||||
|
expect(getByTestId('test-text-field')).toBeDefined(); |
||||||
|
}); |
||||||
|
// truncate
|
||||||
|
it('should render with truncate class as true by default and remove it when truncate is false', () => { |
||||||
|
const { getByTestId } = render( |
||||||
|
<> |
||||||
|
<FormTextField textFieldProps={{ 'data-testid': 'truncate' }} /> |
||||||
|
<FormTextField |
||||||
|
truncate={false} |
||||||
|
textFieldProps={{ 'data-testid': 'no-truncate' }} |
||||||
|
/> |
||||||
|
</>, |
||||||
|
); |
||||||
|
expect(getByTestId('truncate')).toHaveClass('mm-text-field-base--truncate'); |
||||||
|
expect(getByTestId('no-truncate')).not.toHaveClass( |
||||||
|
'mm-text-field-base--truncate', |
||||||
|
); |
||||||
|
}); |
||||||
|
// showClearButton
|
||||||
|
it('should render showClearButton button when showClearButton is true and value exists', async () => { |
||||||
|
// As showClearButton is intended to be used with a controlled input we need to use renderControlledInput
|
||||||
|
const { user, getByRole } = renderControlledInput(FormTextField, { |
||||||
|
showClearButton: true, |
||||||
|
}); |
||||||
|
await user.type(getByRole('textbox'), 'test value'); |
||||||
|
expect(getByRole('textbox')).toHaveValue('test value'); |
||||||
|
expect(getByRole('button', { name: /Clear/u })).toBeDefined(); |
||||||
|
}); |
||||||
|
// clearButtonOnClick
|
||||||
|
it('should fire onClick event when passed to clearButtonOnClick when clear button is clicked', async () => { |
||||||
|
// As showClearButton is intended to be used with a controlled input we need to use renderControlledInput
|
||||||
|
const fn = jest.fn(); |
||||||
|
const { user, getByRole } = renderControlledInput(FormTextField, { |
||||||
|
showClearButton: true, |
||||||
|
clearButtonOnClick: fn, |
||||||
|
}); |
||||||
|
await user.type(getByRole('textbox'), 'test value'); |
||||||
|
await user.click(getByRole('button', { name: /Clear/u })); |
||||||
|
expect(fn).toHaveBeenCalledTimes(1); |
||||||
|
}); |
||||||
|
// clearButtonProps,
|
||||||
|
it('should fire onClick event when passed to clearButtonProps.onClick prop', async () => { |
||||||
|
// As showClearButton is intended to be used with a controlled input we need to use renderControlledInput
|
||||||
|
const fn = jest.fn(); |
||||||
|
const { user, getByRole } = renderControlledInput(FormTextField, { |
||||||
|
showClearButton: true, |
||||||
|
clearButtonProps: { onClick: fn }, |
||||||
|
}); |
||||||
|
await user.type(getByRole('textbox'), 'test value'); |
||||||
|
await user.click(getByRole('button', { name: /Clear/u })); |
||||||
|
expect(fn).toHaveBeenCalledTimes(1); |
||||||
|
}); |
||||||
|
// type,
|
||||||
|
it('should render with different types', () => { |
||||||
|
const { getByTestId } = render( |
||||||
|
<> |
||||||
|
<FormTextField inputProps={{ 'data-testid': 'form-text-field-text' }} /> |
||||||
|
<FormTextField |
||||||
|
type="number" |
||||||
|
inputProps={{ 'data-testid': 'form-text-field-number' }} |
||||||
|
/> |
||||||
|
<FormTextField |
||||||
|
type="password" |
||||||
|
inputProps={{ 'data-testid': 'form-text-field-password' }} |
||||||
|
/> |
||||||
|
</>, |
||||||
|
); |
||||||
|
expect(getByTestId('form-text-field-text')).toHaveAttribute('type', 'text'); |
||||||
|
expect(getByTestId('form-text-field-number')).toHaveAttribute( |
||||||
|
'type', |
||||||
|
'number', |
||||||
|
); |
||||||
|
expect(getByTestId('form-text-field-password')).toHaveAttribute( |
||||||
|
'type', |
||||||
|
'password', |
||||||
|
); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1 @@ |
|||||||
|
export { FormTextField } from './form-text-field'; |
Loading…
Reference in new issue