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 inputfeature/default_network_editable
parent
6918bff291
commit
055a7c52c0
@ -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…
Reference in new issue