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
feature/default_network_editable
George Marshall 2 years ago committed by GitHub
parent e9508b4f7f
commit 5ee7da6afe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      test/lib/render-helpers.js
  2. 1
      ui/components/component-library/component-library-components.scss
  3. 336
      ui/components/component-library/form-text-field/README.mdx
  4. 21
      ui/components/component-library/form-text-field/__snapshots__/form-text-field.test.js.snap
  5. 167
      ui/components/component-library/form-text-field/form-text-field.js
  6. 9
      ui/components/component-library/form-text-field/form-text-field.scss
  7. 481
      ui/components/component-library/form-text-field/form-text-field.stories.js
  8. 349
      ui/components/component-library/form-text-field/form-text-field.test.js
  9. 1
      ui/components/component-library/form-text-field/index.js
  10. 3
      ui/components/component-library/index.js
  11. 21
      ui/components/component-library/text-field/text-field.test.js

@ -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';

@ -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&apos;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';

@ -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…
Cancel
Save