Adding `TextFieldBase` component (#16043)

* Adding TextInputBase component

* Removing keyup and keydown props, tests and docs

* removing showClear from stories

* removing unneeded css

* simplifying uncontrolled vs controlled to work

* Fortifying maxLength test

* Lint fix for test

* Doc, style and prop updates

* Updating constant names with 'base'

* Adding a background color

* Adding a background color to input
feature/default_network_editable
George Marshall 2 years ago committed by GitHub
parent 6918bff291
commit 055a7c52c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      ui/components/component-library/component-library-components.scss
  2. 298
      ui/components/component-library/text-field-base/README.mdx
  3. 5
      ui/components/component-library/text-field-base/index.js
  4. 12
      ui/components/component-library/text-field-base/text-field-base.constants.js
  5. 250
      ui/components/component-library/text-field-base/text-field-base.js
  6. 52
      ui/components/component-library/text-field-base/text-field-base.scss
  7. 361
      ui/components/component-library/text-field-base/text-field-base.stories.js
  8. 213
      ui/components/component-library/text-field-base/text-field-base.test.js

@ -6,3 +6,4 @@
@import 'button-primary/button-primary';
@import 'icon/icon';
@import 'text/text';
@import 'text-field-base/text-field-base';

@ -0,0 +1,298 @@
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import { TextFieldBase } from './text-field-base';
### This is a base component. It should not be used in your feature code directly but as a "base" for other UI components
# TextFieldBase
The `TextFieldBase` is the base component for all text fields. It should not be used directly. It functions as both a uncontrolled and controlled input.
<Canvas>
<Story id="ui-components-component-library-text-field-base-text-field-base-stories-js--default-story" />
</Canvas>
## Props
The `TextFieldBase` accepts all props below as well as all [Box](/docs/ui-components-ui-box-box-stories-js--default-story#props) component props
<ArgsTable of={TextFieldBase} />
### Size
Use the `size` prop to set the height of the `TextFieldBase`.
Possible sizes include:
- `sm` 32px
- `md` 40px
- `lg` 48px
Defaults to `md`
<Canvas>
<Story id="ui-components-component-library-text-field-base-text-field-base-stories-js--size" />
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/component-library/text-field-base';
import { SIZES } from '../../../helpers/constants/design-system';
<TextFieldBase size={SIZES.SM} />
<TextFieldBase size={SIZES.MD} />
<TextFieldBase size={SIZES.LG} />
```
### Type
Use the `type` prop to change the type of input.
Possible types include:
- `text`
- `number`
- `password`
Defaults to `text`.
<Canvas>
<Story id="ui-components-component-library-text-field-base-text-field-base-stories-js--type" />
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/component-library/text-field-base';
<TextFieldBase type="text" /> // (Default)
<TextFieldBase type="number" />
<TextFieldBase type="password" />
```
### Truncate
Use the `truncate` prop to truncate the text of the the `TextFieldBase`
<Canvas>
<Story id="ui-components-component-library-text-field-base-text-field-base-stories-js--truncate" />
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/component-library/text-field-base';
<TextFieldBase truncate />;
```
### Left Accessory Right Accessory
Use the `leftAccessory` and `rightAccessory` props to add components such as icons or buttons to either side of the `TextFieldBase`.
<Canvas>
<Story id="ui-components-component-library-text-field-base-text-field-base-stories-js--left-accessory-right-accessory" />
</Canvas>
```jsx
import { COLORS, SIZES } from '../../../helpers/constants/design-system';
import { Icon, ICON_NAMES } from '../../ui/component-library/icons';
import { TextFieldBase } from '../../ui/component-library/text-field-base';
<TextFieldBase
placeholder="Search"
leftAccessory={
<Icon
color={COLORS.ICON_ALTERNATIVE}
name={ICON_NAMES.SEARCH_FILLED}
/>
}
/>
<TextFieldBase
placeholder="MetaMask"
rightAccessory={
// TODO: replace with ButtonIcon
<button>
<Icon name={ICON_NAMES.CLOSE_OUTLINE} size={SIZES.SM} />
</button>
}
/>
<TextFieldBase
truncate
leftAccessory={<AvatarToken tokenName="ast" size={SIZES.SM} />}
rightAccessory={
// TODO: replace with ButtonIcon
<button>
<Icon name={ICON_NAMES.CLOSE_OUTLINE} size={SIZES.SM} />
</button>
}
/>
<TextFieldBase
placeholder="Enter amount"
type="number"
leftAccessory={
<AvatarToken
tokenName="ast"
tokenImageUrl="./AST.png"
size={SIZES.SM}
/>
}
rightAccessory={
// TODO: replace with ButtonLink
<button>Max</button>
}
/>
```
### Input Ref
Use the `inputRef` prop to access the ref of the `<input />` html element of `TextFieldBase`. This is useful for focusing the input from a button or other component.
<Canvas>
<Story id="ui-components-component-library-text-field-base-text-field-base-stories-js--input-ref" />
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/component-library/text-field-base';
const inputRef = useRef(null);
const [value, setValue] = useState('');
const handleOnClick = () => {
inputRef.current.focus();
};
const handleOnChange = (e) => {
setValue(e.target.value);
};
<TextFieldBase
inputRef={inputRef}
value={value}
onChange={handleOnChange}
/>
// TODO: replace with Button component
<Box
as="button"
backgroundColor={COLORS.BACKGROUND_ALTERNATIVE}
color={COLORS.TEXT_DEFAULT}
borderColor={COLORS.BORDER_DEFAULT}
borderRadius={SIZES.XL}
marginLeft={1}
paddingLeft={2}
paddingRight={2}
onClick={handleOnClick}
>
Edit
</Box>
```
### Auto Complete
Use the `autoComplete` prop to set the autocomplete html attribute. It allows the browser to predict the value based on earlier typed values.
<Canvas>
<Story id="ui-components-component-library-text-field-base-text-field-base-stories-js--auto-complete" />
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/component-library/text-field-base';
<TextFieldBase type="password" autoComplete />;
```
### Auto Focus
Use the `autoFocus` prop to focus the `TextFieldBase` during the first mount
<Canvas>
<Story id="ui-components-component-library-text-field-base-text-field-base-stories-js--auto-focus" />
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/component-library/text-field-base';
<TextFieldBase autoFocus />;
```
### Default Value
Use the `defaultValue` prop to set the default value of the `TextFieldBase`
<Canvas>
<Story id="ui-components-component-library-text-field-base-text-field-base-stories-js--default-value" />
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/component-library/text-field-base';
<TextFieldBase defaultValue="default value" />;
```
### Disabled
Use the `disabled` prop to set the disabled state of the `TextFieldBase`
<Canvas>
<Story id="ui-components-component-library-text-field-base-text-field-base-stories-js--disabled" />
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/component-library/text-field-base';
<TextFieldBase disabled />;
```
### Error
Use the `error` prop to set the error state of the `TextFieldBase`
<Canvas>
<Story id="ui-components-component-library-text-field-base-text-field-base-stories-js--error-story" />
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/component-library/text-field-base';
<TextFieldBase error />;
```
### Max Length
Use the `maxLength` prop to set the maximum allowed input characters for the `TextFieldBase`
<Canvas>
<Story id="ui-components-component-library-text-field-base-text-field-base-stories-js--max-length" />
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/component-library/text-field-base';
<TextFieldBase maxLength={10} />;
```
### Read Only
Use the `readOnly` prop to set the `TextFieldBase` to read only
<Canvas>
<Story id="ui-components-component-library-text-field-base-text-field-base-stories-js--read-only" />
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/component-library/text-field-base';
<TextFieldBase readOnly />;
```
### Required
Use the `required` prop to set the `TextFieldBase` to required. Currently there is no visual difference to the `TextFieldBase` when required.
<Canvas>
<Story id="ui-components-component-library-text-field-base-text-field-base-stories-js--required" />
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/component-library/text-field-base';
// Currently no visual difference
<TextFieldBase required />;
```

@ -0,0 +1,5 @@
export { TextFieldBase } from './text-field-base';
export {
TEXT_FIELD_BASE_SIZES,
TEXT_FIELD_BASE_TYPES,
} from './text-field-base.constants';

@ -0,0 +1,12 @@
import { SIZES } from '../../../helpers/constants/design-system';
export const TEXT_FIELD_BASE_SIZES = {
SM: SIZES.SM,
MD: SIZES.MD,
LG: SIZES.LG,
};
export const TEXT_FIELD_BASE_TYPES = {
TEXT: 'text',
NUMBER: 'number',
PASSWORD: 'password',
};

@ -0,0 +1,250 @@
import React, { useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import {
DISPLAY,
SIZES,
ALIGN_ITEMS,
TEXT,
COLORS,
} from '../../../helpers/constants/design-system';
import Box from '../../ui/box';
import { Text } from '../text';
import {
TEXT_FIELD_BASE_SIZES,
TEXT_FIELD_BASE_TYPES,
} from './text-field-base.constants';
export const TextFieldBase = ({
autoComplete,
autoFocus,
className,
defaultValue,
disabled,
error,
id,
inputProps,
inputRef,
leftAccessory,
rightAccessory,
maxLength,
name,
onBlur,
onChange,
onClick,
onFocus,
placeholder,
readOnly,
required,
size = SIZES.MD,
type = 'text',
truncate,
value,
...props
}) => {
const internalInputRef = useRef(null);
const [focused, setFocused] = useState(false);
useEffect(() => {
// The blur won't fire when the disabled state is set on a focused input.
// We need to set the focused state manually.
if (disabled) {
setFocused(false);
}
}, [disabled]);
const handleClick = (event) => {
const { current } = internalInputRef;
if (current) {
current.focus();
setFocused(true);
}
if (onClick) {
onClick(event);
}
};
const handleFocus = (event) => {
setFocused(true);
onFocus && onFocus(event);
};
const handleBlur = (event) => {
setFocused(false);
onBlur && onBlur(event);
};
const handleInputRef = (ref) => {
internalInputRef.current = ref;
if (inputRef && inputRef.current !== undefined) {
inputRef.current = ref;
} else if (typeof inputRef === 'function') {
inputRef(ref);
}
};
return (
<Box
className={classnames(
'mm-text-field-base',
`mm-text-field-base--size-${size}`,
{
'mm-text-field-base--focused': focused && !disabled,
'mm-text-field-base--error': error,
'mm-text-field-base--disabled': disabled,
'mm-text-field-base--truncate': truncate,
},
className,
)}
display={DISPLAY.INLINE_FLEX}
backgroundColor={COLORS.BACKGROUND_DEFAULT}
alignItems={ALIGN_ITEMS.CENTER}
borderWidth={1}
borderRadius={SIZES.SM}
paddingLeft={4}
paddingRight={4}
onClick={handleClick}
{...props}
>
{leftAccessory}
<Text
aria-invalid={error}
as="input"
autoComplete={autoComplete ? 'on' : 'off'}
autoFocus={autoFocus}
backgroundColor={COLORS.TRANSPARENT}
defaultValue={defaultValue}
disabled={disabled}
focused={focused.toString()}
id={id}
margin={0}
maxLength={maxLength}
name={name}
onBlur={handleBlur}
onChange={onChange}
onFocus={handleFocus}
padding={0}
paddingLeft={leftAccessory ? 2 : null}
paddingRight={leftAccessory ? 2 : null}
placeholder={placeholder}
readOnly={readOnly}
ref={handleInputRef}
required={required}
value={value}
variant={TEXT.BODY_MD}
type={type}
{...inputProps} // before className so input className isn't overridden
className={classnames(
'mm-text-field-base__input',
inputProps?.className,
)}
/>
{rightAccessory}
</Box>
);
};
TextFieldBase.propTypes = {
/**
* Autocomplete allows the browser to predict the value based on earlier typed values
*/
autoComplete: PropTypes.string,
/**
* If `true`, the input will be focused during the first mount.
*/
autoFocus: PropTypes.bool,
/**
* An additional className to apply to the text-field-base
*/
className: PropTypes.string,
/**
* The default input value, useful when not controlling the component.
*/
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/**
* If `true`, the input will be disabled.
*/
disabled: PropTypes.bool,
/**
* If `true`, the input will indicate an error
*/
error: PropTypes.bool,
/**
* The id of the `input` element.
*/
id: PropTypes.string,
/**
* Attributes applied to the `input` element.
*/
inputProps: PropTypes.object,
/**
* Component to appear on the left side of the input
*/
leftAccessory: PropTypes.node,
/**
* Component to appear on the right side of the input
*/
rightAccessory: PropTypes.node,
/**
* Use inputRef to pass a ref to the html input element.
*/
inputRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
/**
* Max number of characters to allow
*/
maxLength: PropTypes.number,
/**
* Name attribute of the `input` element.
*/
name: PropTypes.string,
/**
* Callback fired on blur
*/
onBlur: PropTypes.func,
/**
* Callback fired when the value is changed.
*/
onChange: PropTypes.func,
/**
* Callback fired on focus
*/
onFocus: PropTypes.func,
/**
* The short hint displayed in the input before the user enters a value.
*/
placeholder: PropTypes.string,
/**
* It prevents the user from changing the value of the field (not from interacting with the field).
*/
readOnly: PropTypes.bool,
/**
* If `true`, the input will be required. Currently no visual difference is shown.
*/
required: PropTypes.bool,
/**
* The size of the text field. Changes the height of the component
* Accepts SM(32px), MD(40px), LG(48px)
*/
size: PropTypes.oneOf(Object.values(TEXT_FIELD_BASE_SIZES)),
/**
* Type of the input element. Can be TEXT_FIELD_BASE_TYPES.TEXT, TEXT_FIELD_BASE_TYPES.PASSWORD, TEXT_FIELD_BASE_TYPES.NUMBER
* Defaults to TEXT_FIELD_BASE_TYPES.TEXT ('text')
*/
type: PropTypes.oneOf(Object.values(TEXT_FIELD_BASE_TYPES)),
/**
* The input value, required for a controlled component.
*/
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/**
* TextFieldBase accepts all the props from Box
*/
...Box.propTypes,
};
TextFieldBase.displayName = 'TextFieldBase';

@ -0,0 +1,52 @@
.mm-text-field-base {
--text-field-base-height: var(--size, 40px);
&--size-sm {
--size: 32px;
}
&--size-md {
--size: 40px;
}
&--size-lg {
--size: 48px;
}
height: var(--text-field-base-height);
border-color: var(--color-border-default);
&--focused {
border-color: var(--color-primary-default);
}
&--error {
border-color: var(--color-error-default);
}
&--disabled {
opacity: 0.5;
border-color: var(--color-border-default);
}
// truncates text with ellipsis
&--truncate .mm-text-field-base__input {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__input {
border: none;
height: 100%;
flex-grow: 1;
box-sizing: content-box;
margin: 0;
padding: 0;
&:focus,
&:focus-visible {
outline: none;
}
}
}

@ -0,0 +1,361 @@
import React, { useState, useRef } from 'react';
import {
SIZES,
DISPLAY,
COLORS,
FLEX_DIRECTION,
} from '../../../helpers/constants/design-system';
import Box from '../../ui/box/box';
import { Icon, ICON_NAMES } from '../icon';
import { AvatarToken } from '../avatar-token';
import {
TEXT_FIELD_BASE_SIZES,
TEXT_FIELD_BASE_TYPES,
} from './text-field-base.constants';
import { TextFieldBase } from './text-field-base';
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/TextFieldBase',
id: __filename,
component: TextFieldBase,
parameters: {
docs: {
page: README,
},
},
argTypes: {
autoComplete: {
control: 'boolean',
},
autoFocus: {
control: 'boolean',
},
className: {
control: 'text',
},
defaultValue: {
control: 'text',
},
disabled: {
control: 'boolean',
},
error: {
control: 'boolean',
},
id: {
control: 'text',
},
inputProps: {
control: 'object',
},
leftAccessory: {
control: 'text',
},
maxLength: {
control: 'number',
},
name: {
control: 'text',
},
onBlur: {
action: 'onBlur',
},
onChange: {
action: 'onChange',
},
onClick: {
action: 'onClick',
},
onFocus: {
action: 'onFocus',
},
placeholder: {
control: 'text',
},
readOnly: {
control: 'boolean',
},
required: {
control: 'boolean',
},
rightAccessory: {
control: 'text',
},
size: {
control: 'select',
options: Object.values(TEXT_FIELD_BASE_SIZES),
},
type: {
control: 'select',
options: Object.values(TEXT_FIELD_BASE_TYPES),
},
value: {
control: 'text',
},
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: 'Placeholder...',
autoFocus: false,
defaultValue: '',
disabled: false,
error: false,
id: '',
readOnly: false,
required: false,
size: SIZES.MD,
type: 'text',
truncate: false,
},
};
const Template = (args) => <TextFieldBase {...args} />;
export const DefaultStory = Template.bind({});
DefaultStory.storyName = 'Default';
export const Size = (args) => {
return (
<Box
display={DISPLAY.INLINE_FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
gap={4}
>
<TextFieldBase
{...args}
placeholder="SIZES.SM (height: 32px)"
size={SIZES.SM}
/>
<TextFieldBase
{...args}
placeholder="SIZES.MD (height: 40px)"
size={SIZES.MD}
/>
<TextFieldBase
{...args}
placeholder="SIZES.LG (height: 48px)"
size={SIZES.LG}
/>
</Box>
);
};
export const Type = (args) => (
<Box
display={DISPLAY.INLINE_FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
gap={4}
>
<TextFieldBase {...args} placeholder="Default" />
<TextFieldBase
{...args}
type={TEXT_FIELD_BASE_TYPES.PASSWORD}
placeholder="Password"
/>
<TextFieldBase
{...args}
type={TEXT_FIELD_BASE_TYPES.NUMBER}
placeholder="Number"
/>
</Box>
);
export const Truncate = Template.bind({});
Truncate.args = {
placeholder: 'Truncate',
value: 'Truncated text when truncate and width is set',
truncate: true,
style: { width: 240 },
};
export const LeftAccessoryRightAccessory = (args) => {
const [value, setValue] = useState({
search: '',
metaMask: '',
address: '0x514910771af9ca656af840dff83e8264ecf986ca',
amount: 1,
});
return (
<Box
display={DISPLAY.INLINE_FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
gap={4}
>
<TextFieldBase
{...args}
placeholder="Search"
value={value.search}
onChange={(e) => setValue({ ...value, search: e.target.value })}
leftAccessory={
<Icon
color={COLORS.ICON_ALTERNATIVE}
name={ICON_NAMES.SEARCH_FILLED}
/>
}
/>
<TextFieldBase
{...args}
value={value.metaMask}
onChange={(e) => setValue({ ...value, metaMask: e.target.value })}
placeholder="MetaMask"
rightAccessory={
<button
style={{
padding: 0,
background: 'transparent',
margin: 0,
display: 'flex',
}}
onClick={() => setValue({ ...value, metaMask: '' })}
>
<Icon name={ICON_NAMES.CLOSE_OUTLINE} size={SIZES.SM} />
</button>
}
/>
<TextFieldBase
{...args}
placeholder="Enter address"
value={value.address}
onChange={(e) => setValue({ ...value, address: e.target.value })}
truncate
leftAccessory={<AvatarToken tokenName="ast" size={SIZES.SM} />}
rightAccessory={
<button
style={{
padding: 0,
background: 'transparent',
margin: 0,
display: 'flex',
}}
onClick={() => setValue({ ...value, address: '' })}
>
<Icon name={ICON_NAMES.CLOSE_OUTLINE} size={SIZES.SM} />
</button>
}
/>
<TextFieldBase
{...args}
placeholder="Enter amount"
value={value.amount}
onChange={(e) => setValue({ ...value, amount: e.target.value })}
type="number"
leftAccessory={
<AvatarToken
tokenName="ast"
tokenImageUrl="./AST.png"
size={SIZES.SM}
/>
}
rightAccessory={
<button onClick={() => setValue({ ...value, amount: 100000 })}>
Max
</button>
}
/>
</Box>
);
};
export const InputRef = (args) => {
const inputRef = useRef(null);
const [value, setValue] = useState('');
const handleOnClick = () => {
inputRef.current.focus();
};
const handleOnChange = (e) => {
setValue(e.target.value);
};
return (
<>
<TextFieldBase
{...args}
inputRef={inputRef}
value={value}
onChange={handleOnChange}
/>
<Box
as="button"
backgroundColor={COLORS.BACKGROUND_ALTERNATIVE}
color={COLORS.TEXT_DEFAULT}
borderColor={COLORS.BORDER_DEFAULT}
borderRadius={SIZES.XL}
marginLeft={1}
paddingLeft={2}
paddingRight={2}
onClick={handleOnClick}
>
Edit
</Box>
</>
);
};
export const AutoComplete = Template.bind({});
AutoComplete.args = {
autoComplete: true,
type: 'password',
placeholder: 'Enter password',
};
export const AutoFocus = Template.bind({});
AutoFocus.args = { autoFocus: true };
export const DefaultValue = Template.bind({});
DefaultValue.args = { defaultValue: 'Default value' };
export const Disabled = Template.bind({});
Disabled.args = { disabled: true };
export const ErrorStory = Template.bind({});
ErrorStory.args = { error: true };
ErrorStory.storyName = 'Error';
export const MaxLength = Template.bind({});
MaxLength.args = { maxLength: 10, placeholder: 'Max length 10' };
export const ReadOnly = Template.bind({});
ReadOnly.args = { readOnly: true, value: 'Read only' };
export const Required = Template.bind({});
Required.args = { required: true, placeholder: 'Required' };

@ -0,0 +1,213 @@
/* 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 { SIZES } from '../../../helpers/constants/design-system';
import { TextFieldBase } from './text-field-base';
describe('TextFieldBase', () => {
it('should render correctly', () => {
const { getByRole } = render(<TextFieldBase />);
expect(getByRole('textbox')).toBeDefined();
});
it('should render and be able to input text', () => {
const { getByTestId } = render(
<TextFieldBase inputProps={{ 'data-testid': 'text-field-base' }} />,
);
const textFieldBase = getByTestId('text-field-base');
expect(textFieldBase.value).toBe(''); // initial value is empty string
fireEvent.change(textFieldBase, { target: { value: 'text value' } });
expect(textFieldBase.value).toBe('text value');
fireEvent.change(textFieldBase, { target: { value: '' } }); // reset value
expect(textFieldBase.value).toBe(''); // value is empty string after reset
});
it('should render and fire onFocus and onBlur events', () => {
const onFocus = jest.fn();
const onBlur = jest.fn();
const { getByTestId } = render(
<TextFieldBase
inputProps={{ 'data-testid': 'text-field-base' }}
onFocus={onFocus}
onBlur={onBlur}
/>,
);
const textFieldBase = getByTestId('text-field-base');
fireEvent.focus(textFieldBase);
expect(onFocus).toHaveBeenCalledTimes(1);
fireEvent.blur(textFieldBase);
expect(onBlur).toHaveBeenCalledTimes(1);
});
it('should render and fire onChange event', () => {
const onChange = jest.fn();
const { getByTestId } = render(
<TextFieldBase
inputProps={{ 'data-testid': 'text-field-base' }}
onChange={onChange}
/>,
);
const textFieldBase = getByTestId('text-field-base');
fireEvent.change(textFieldBase, { target: { value: 'text value' } });
expect(onChange).toHaveBeenCalledTimes(1);
});
it('should render and fire onClick event', () => {
const onClick = jest.fn();
const { getByTestId } = render(
<TextFieldBase
inputProps={{ 'data-testid': 'text-field-base' }}
onClick={onClick}
/>,
);
const textFieldBase = getByTestId('text-field-base');
fireEvent.click(textFieldBase);
expect(onClick).toHaveBeenCalledTimes(1);
});
it('should render with different size classes', () => {
const { getByTestId } = render(
<>
<TextFieldBase size={SIZES.SM} data-testid="sm" />
<TextFieldBase size={SIZES.MD} data-testid="md" />
<TextFieldBase size={SIZES.LG} 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');
});
it('should render with different types', () => {
const { getByTestId } = render(
<>
<TextFieldBase inputProps={{ 'data-testid': 'text-field-base-text' }} />
<TextFieldBase
type="number"
inputProps={{ 'data-testid': 'text-field-base-number' }}
/>
<TextFieldBase
type="password"
inputProps={{ 'data-testid': 'text-field-base-password' }}
/>
</>,
);
expect(getByTestId('text-field-base-text')).toHaveAttribute('type', 'text');
expect(getByTestId('text-field-base-number')).toHaveAttribute(
'type',
'number',
);
expect(getByTestId('text-field-base-password')).toHaveAttribute(
'type',
'password',
);
});
it('should render with truncate class', () => {
const { getByTestId } = render(
<TextFieldBase truncate data-testid="truncate" />,
);
expect(getByTestId('truncate')).toHaveClass('mm-text-field-base--truncate');
});
it('should render with right and left accessories', () => {
const { getByRole, getByText } = render(
<TextFieldBase
leftAccessory={<div>left accessory</div>}
rightAccessory={<div>right accessory</div>}
/>,
);
expect(getByRole('textbox')).toBeDefined();
expect(getByText('left accessory')).toBeDefined();
expect(getByText('right accessory')).toBeDefined();
});
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(<TextFieldBase inputRef={mockRef} />);
expect(getByRole('textbox')).toBeDefined();
expect(mockRef).toHaveBeenCalledTimes(1);
});
it('should render with autoComplete', () => {
const { getByTestId } = render(
<TextFieldBase
autoComplete
inputProps={{ 'data-testid': 'text-field-base-auto-complete' }}
/>,
);
expect(getByTestId('text-field-base-auto-complete')).toHaveAttribute(
'autocomplete',
'on',
);
});
it('should render with autoFocus', () => {
const { getByRole } = render(<TextFieldBase autoFocus />);
expect(getByRole('textbox')).toHaveFocus();
});
it('should render with a defaultValue', () => {
const { getByRole } = render(
<TextFieldBase
defaultValue="default value"
inputProps={{ 'data-testid': 'text-field-base-default-value' }}
/>,
);
expect(getByRole('textbox').value).toBe('default value');
});
it('should render in disabled state and not focus or be clickable', () => {
const mockOnClick = jest.fn();
const mockOnFocus = jest.fn();
const { getByRole } = render(
<TextFieldBase disabled onFocus={mockOnFocus} onClick={mockOnClick} />,
);
getByRole('textbox').focus();
expect(getByRole('textbox')).toBeDisabled();
expect(mockOnClick).toHaveBeenCalledTimes(0);
expect(mockOnFocus).toHaveBeenCalledTimes(0);
});
it('should render with error className when error is true', () => {
const { getByTestId } = render(
<TextFieldBase
error
value="error value"
data-testid="text-field-base-error"
/>,
);
expect(getByTestId('text-field-base-error')).toHaveClass(
'mm-text-field-base--error',
);
});
it('should render with maxLength and not allow more than the set characters', async () => {
const { getByRole } = render(<TextFieldBase maxLength={5} />);
const textFieldBase = getByRole('textbox');
await userEvent.type(textFieldBase, '1234567890');
expect(getByRole('textbox')).toBeDefined();
expect(textFieldBase.maxLength).toBe(5);
expect(textFieldBase.value).toBe('12345');
expect(textFieldBase.value).toHaveLength(5);
});
it('should render with readOnly attr when readOnly is true', () => {
const { getByTestId } = render(
<TextFieldBase
readOnly
inputProps={{ 'data-testid': 'text-field-base-readonly' }}
/>,
);
expect(getByTestId('text-field-base-readonly')).toHaveAttribute(
'readonly',
'',
);
});
it('should render with required attr when required is true', () => {
const { getByTestId } = render(
<TextFieldBase
required
inputProps={{ 'data-testid': 'text-field-base-required' }}
/>,
);
expect(getByTestId('text-field-base-required')).toHaveAttribute(
'required',
'',
);
});
});
Loading…
Cancel
Save