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 'icon/icon';
|
||||
@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