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