mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
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 test
This commit is contained in:
parent
e9508b4f7f
commit
5ee7da6afe
@ -137,3 +137,12 @@ export function renderControlledInput(InputComponent, props) {
|
|||||||
};
|
};
|
||||||
return { user: userEvent.setup(), ...render(<ControlledWrapper />) };
|
return { user: userEvent.setup(), ...render(<ControlledWrapper />) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// userEvent setup function as per testing-library docs
|
||||||
|
// https://testing-library.com/docs/user-event/intr
|
||||||
|
export function renderWithUserEvent(jsx) {
|
||||||
|
return {
|
||||||
|
user: userEvent.setup(),
|
||||||
|
...render(jsx),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -26,3 +26,4 @@
|
|||||||
@import 'text-field/text-field';
|
@import 'text-field/text-field';
|
||||||
@import 'text-field-base/text-field-base';
|
@import 'text-field-base/text-field-base';
|
||||||
@import 'text-field-search/text-field-search';
|
@import 'text-field-search/text-field-search';
|
||||||
|
@import 'form-text-field/form-text-field';
|
||||||
|
336
ui/components/component-library/form-text-field/README.mdx
Normal file
336
ui/components/component-library/form-text-field/README.mdx
Normal file
@ -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',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
1
ui/components/component-library/form-text-field/index.js
Normal file
1
ui/components/component-library/form-text-field/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { FormTextField } from './form-text-field';
|
@ -10,6 +10,7 @@ export { ButtonIcon } from './button-icon';
|
|||||||
export { ButtonLink } from './button-link';
|
export { ButtonLink } from './button-link';
|
||||||
export { ButtonPrimary } from './button-primary';
|
export { ButtonPrimary } from './button-primary';
|
||||||
export { ButtonSecondary } from './button-secondary';
|
export { ButtonSecondary } from './button-secondary';
|
||||||
|
export { FormTextField } from './form-text-field';
|
||||||
export { HelpText } from './help-text';
|
export { HelpText } from './help-text';
|
||||||
export { Icon, ICON_NAMES } from './icon';
|
export { Icon, ICON_NAMES } from './icon';
|
||||||
export { Label } from './label';
|
export { Label } from './label';
|
||||||
@ -17,7 +18,7 @@ export { PickerNetwork } from './picker-network';
|
|||||||
export { Tag } from './tag';
|
export { Tag } from './tag';
|
||||||
export { TagUrl } from './tag-url';
|
export { TagUrl } from './tag-url';
|
||||||
export { Text } from './text';
|
export { Text } from './text';
|
||||||
export { TextField } from './text-field';
|
export { TextField, TEXT_FIELD_TYPES, TEXT_FIELD_SIZES } from './text-field';
|
||||||
export {
|
export {
|
||||||
TextFieldBase,
|
TextFieldBase,
|
||||||
TEXT_FIELD_BASE_SIZES,
|
TEXT_FIELD_BASE_SIZES,
|
||||||
|
@ -1,28 +1,21 @@
|
|||||||
/* eslint-disable jest/require-top-level-describe */
|
/* eslint-disable jest/require-top-level-describe */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { fireEvent, render } from '@testing-library/react';
|
import { fireEvent, render } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
|
||||||
|
|
||||||
import { renderControlledInput } from '../../../../test/lib/render-helpers';
|
import {
|
||||||
|
renderControlledInput,
|
||||||
|
renderWithUserEvent,
|
||||||
|
} from '../../../../test/lib/render-helpers';
|
||||||
|
|
||||||
import { TextField } from './text-field';
|
import { TextField } from './text-field';
|
||||||
|
|
||||||
// userEvent setup function as per testing-library docs
|
|
||||||
// https://testing-library.com/docs/user-event/intr
|
|
||||||
function setup(jsx) {
|
|
||||||
return {
|
|
||||||
user: userEvent.setup(),
|
|
||||||
...render(jsx),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('TextField', () => {
|
describe('TextField', () => {
|
||||||
it('should render correctly', () => {
|
it('should render correctly', () => {
|
||||||
const { getByRole } = render(<TextField />);
|
const { getByRole } = render(<TextField />);
|
||||||
expect(getByRole('textbox')).toBeDefined();
|
expect(getByRole('textbox')).toBeDefined();
|
||||||
});
|
});
|
||||||
it('should render and be able to input text', async () => {
|
it('should render and be able to input text', async () => {
|
||||||
const { user, getByRole } = setup(<TextField />);
|
const { user, getByRole } = renderWithUserEvent(<TextField />);
|
||||||
const textField = getByRole('textbox');
|
const textField = getByRole('textbox');
|
||||||
await user.type(textField, 'text value');
|
await user.type(textField, 'text value');
|
||||||
expect(textField).toHaveValue('text value');
|
expect(textField).toHaveValue('text value');
|
||||||
@ -46,7 +39,7 @@ describe('TextField', () => {
|
|||||||
});
|
});
|
||||||
it('should render and fire onChange event', async () => {
|
it('should render and fire onChange event', async () => {
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const { user, getByRole } = setup(
|
const { user, getByRole } = renderWithUserEvent(
|
||||||
<TextField
|
<TextField
|
||||||
inputProps={{ 'data-testid': 'text-field' }}
|
inputProps={{ 'data-testid': 'text-field' }}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@ -60,7 +53,7 @@ describe('TextField', () => {
|
|||||||
});
|
});
|
||||||
it('should render and fire onClick event', async () => {
|
it('should render and fire onClick event', async () => {
|
||||||
const onClick = jest.fn();
|
const onClick = jest.fn();
|
||||||
const { user, getByTestId } = setup(
|
const { user, getByTestId } = renderWithUserEvent(
|
||||||
<TextField data-testid="text-field" onClick={onClick} />,
|
<TextField data-testid="text-field" onClick={onClick} />,
|
||||||
);
|
);
|
||||||
await user.click(getByTestId('text-field'));
|
await user.click(getByTestId('text-field'));
|
||||||
|
Loading…
Reference in New Issue
Block a user