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 />) };
|
||||
}
|
||||
|
||||
// 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-base/text-field-base';
|
||||
@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 { ButtonPrimary } from './button-primary';
|
||||
export { ButtonSecondary } from './button-secondary';
|
||||
export { FormTextField } from './form-text-field';
|
||||
export { HelpText } from './help-text';
|
||||
export { Icon, ICON_NAMES } from './icon';
|
||||
export { Label } from './label';
|
||||
@ -17,7 +18,7 @@ export { PickerNetwork } from './picker-network';
|
||||
export { Tag } from './tag';
|
||||
export { TagUrl } from './tag-url';
|
||||
export { Text } from './text';
|
||||
export { TextField } from './text-field';
|
||||
export { TextField, TEXT_FIELD_TYPES, TEXT_FIELD_SIZES } from './text-field';
|
||||
export {
|
||||
TextFieldBase,
|
||||
TEXT_FIELD_BASE_SIZES,
|
||||
|
@ -1,28 +1,21 @@
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
import React from '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';
|
||||
|
||||
// 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', () => {
|
||||
it('should render correctly', () => {
|
||||
const { getByRole } = render(<TextField />);
|
||||
expect(getByRole('textbox')).toBeDefined();
|
||||
});
|
||||
it('should render and be able to input text', async () => {
|
||||
const { user, getByRole } = setup(<TextField />);
|
||||
const { user, getByRole } = renderWithUserEvent(<TextField />);
|
||||
const textField = getByRole('textbox');
|
||||
await user.type(textField, 'text value');
|
||||
expect(textField).toHaveValue('text value');
|
||||
@ -46,7 +39,7 @@ describe('TextField', () => {
|
||||
});
|
||||
it('should render and fire onChange event', async () => {
|
||||
const onChange = jest.fn();
|
||||
const { user, getByRole } = setup(
|
||||
const { user, getByRole } = renderWithUserEvent(
|
||||
<TextField
|
||||
inputProps={{ 'data-testid': 'text-field' }}
|
||||
onChange={onChange}
|
||||
@ -60,7 +53,7 @@ describe('TextField', () => {
|
||||
});
|
||||
it('should render and fire onClick event', async () => {
|
||||
const onClick = jest.fn();
|
||||
const { user, getByTestId } = setup(
|
||||
const { user, getByTestId } = renderWithUserEvent(
|
||||
<TextField data-testid="text-field" onClick={onClick} />,
|
||||
);
|
||||
await user.click(getByTestId('text-field'));
|
||||
|
Loading…
Reference in New Issue
Block a user