mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
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
This commit is contained in:
parent
6918bff291
commit
055a7c52c0
@ -6,3 +6,4 @@
|
|||||||
@import 'button-primary/button-primary';
|
@import 'button-primary/button-primary';
|
||||||
@import 'icon/icon';
|
@import 'icon/icon';
|
||||||
@import 'text/text';
|
@import 'text/text';
|
||||||
|
@import 'text-field-base/text-field-base';
|
||||||
|
298
ui/components/component-library/text-field-base/README.mdx
Normal file
298
ui/components/component-library/text-field-base/README.mdx
Normal file
@ -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 />;
|
||||||
|
```
|
5
ui/components/component-library/text-field-base/index.js
Normal file
5
ui/components/component-library/text-field-base/index.js
Normal file
@ -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…
Reference in New Issue
Block a user