mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Updating TextField component (#17732)
* Removing TextFieldBase and updating TextField and TextFieldSearch * Removing autoFocus from MDX so it's not annoying
This commit is contained in:
parent
cdc1bce688
commit
f6ee35b6e3
@ -23,7 +23,7 @@
|
||||
// Molecules
|
||||
@import 'picker-network/picker-network';
|
||||
@import 'tag-url/tag-url';
|
||||
@import 'text-field-base/text-field-base';
|
||||
@import 'text-field/text-field';
|
||||
@import 'text-field-search/text-field-search';
|
||||
@import 'form-text-field/form-text-field';
|
||||
@import 'banner-alert/banner-alert';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
|
||||
|
||||
import { TextField, TextFieldBase } from '../';
|
||||
import { TextField } from '../';
|
||||
import { FormTextField } from './form-text-field';
|
||||
|
||||
# FormTextField
|
||||
@ -22,10 +22,10 @@ component props
|
||||
|
||||
<ArgsTable of={TextField} />
|
||||
|
||||
`FormTextField` accepts all [TextFieldBase](/docs/components-componentlibrary-textfield--default-story#props)
|
||||
`FormTextField` accepts all [TextField](/docs/components-componentlibrary-textfield--default-story#props)
|
||||
component props
|
||||
|
||||
<ArgsTable of={TextFieldBase} />
|
||||
<ArgsTable of={TextField} />
|
||||
|
||||
### Id
|
||||
|
||||
@ -305,7 +305,7 @@ import {
|
||||
<FormTextField
|
||||
id="custom-spending-cap"
|
||||
placeholder="Enter a number"
|
||||
rightAccessory={<ButtonLink>Max</ButtonLink>}
|
||||
endAccessory={<ButtonLink>Max</ButtonLink>}
|
||||
marginBottom={4}
|
||||
type={TEXT_FIELD_TYPES.NUMBER}
|
||||
/>
|
||||
|
@ -6,11 +6,11 @@ exports[`FormTextField should render correctly 1`] = `
|
||||
class="box mm-form-text-field box--display-flex box--flex-direction-column"
|
||||
>
|
||||
<div
|
||||
class="box mm-text-field-base mm-text-field-base--size-md mm-text-field-base--truncate mm-text-field mm-form-text-field__text-field box--display-inline-flex box--flex-direction-row box--align-items-center box--background-color-background-default box--rounded-sm box--border-width-1 box--border-style-solid"
|
||||
class="box mm-text-field mm-text-field--size-md mm-text-field--truncate mm-form-text-field__text-field box--display-inline-flex box--flex-direction-row box--align-items-center box--background-color-background-default box--rounded-sm box--border-width-1 box--border-style-solid"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class="box mm-text mm-text-field-base__input mm-text--body-md mm-text--color-text-default box--padding-right-4 box--padding-left-4 box--flex-direction-row box--background-color-transparent"
|
||||
class="box mm-text mm-text-field__input mm-text--body-md mm-text--color-text-default box--padding-right-4 box--padding-left-4 box--flex-direction-row box--background-color-transparent"
|
||||
focused="false"
|
||||
type="text"
|
||||
value=""
|
||||
|
@ -28,7 +28,7 @@ export const FormTextField = ({
|
||||
inputRef,
|
||||
label,
|
||||
labelProps,
|
||||
leftAccessory,
|
||||
startAccessory,
|
||||
maxLength,
|
||||
name,
|
||||
onBlur,
|
||||
@ -37,13 +37,10 @@ export const FormTextField = ({
|
||||
placeholder,
|
||||
readOnly,
|
||||
required,
|
||||
rightAccessory,
|
||||
endAccessory,
|
||||
size = Size.MD,
|
||||
textFieldProps,
|
||||
truncate,
|
||||
showClearButton,
|
||||
clearButtonOnClick,
|
||||
clearButtonProps,
|
||||
type = 'text',
|
||||
value,
|
||||
...props
|
||||
@ -86,7 +83,7 @@ export const FormTextField = ({
|
||||
id,
|
||||
inputProps,
|
||||
inputRef,
|
||||
leftAccessory,
|
||||
startAccessory,
|
||||
maxLength,
|
||||
name,
|
||||
onBlur,
|
||||
@ -95,10 +92,7 @@ export const FormTextField = ({
|
||||
placeholder,
|
||||
readOnly,
|
||||
required,
|
||||
rightAccessory,
|
||||
showClearButton,
|
||||
clearButtonOnClick,
|
||||
clearButtonProps,
|
||||
endAccessory,
|
||||
size,
|
||||
truncate,
|
||||
type,
|
||||
|
@ -73,18 +73,6 @@ export default {
|
||||
helpTextProps: {
|
||||
control: 'object',
|
||||
},
|
||||
showClearButton: {
|
||||
control: 'boolean',
|
||||
table: { category: 'text field props' },
|
||||
},
|
||||
clearButtonOnClick: {
|
||||
action: 'clearButtonOnClick',
|
||||
table: { category: 'text field props' },
|
||||
},
|
||||
clearButtonProps: {
|
||||
control: 'object',
|
||||
table: { category: 'text field props' },
|
||||
},
|
||||
autoComplete: {
|
||||
control: 'boolean',
|
||||
table: { category: 'text field base props' },
|
||||
@ -113,7 +101,7 @@ export default {
|
||||
control: 'object',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
leftAccessory: {
|
||||
startAccessory: {
|
||||
control: 'text',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
@ -157,7 +145,7 @@ export default {
|
||||
control: 'boolean',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
rightAccessory: {
|
||||
endAccessory: {
|
||||
control: 'text',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
@ -209,17 +197,7 @@ const Template = (args) => {
|
||||
const handleOnChange = (e) => {
|
||||
updateArgs({ value: e.target.value });
|
||||
};
|
||||
const handleOnClear = () => {
|
||||
updateArgs({ value: '' });
|
||||
};
|
||||
return (
|
||||
<FormTextField
|
||||
{...args}
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
clearButtonOnClick={handleOnClear}
|
||||
/>
|
||||
);
|
||||
return <FormTextField {...args} value={value} onChange={handleOnChange} />;
|
||||
};
|
||||
|
||||
export const DefaultStory = Template.bind({});
|
||||
@ -245,9 +223,6 @@ export const HelpTextStory = (args) => {
|
||||
const handleOnChange = (e) => {
|
||||
updateArgs({ value: e.target.value });
|
||||
};
|
||||
const handleOnClear = () => {
|
||||
updateArgs({ value: '' });
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<FormTextField
|
||||
@ -255,7 +230,6 @@ export const HelpTextStory = (args) => {
|
||||
id="input-with-help-text"
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
clearButtonOnClick={handleOnClear}
|
||||
marginBottom={4}
|
||||
/>
|
||||
<FormTextField
|
||||
@ -265,7 +239,6 @@ export const HelpTextStory = (args) => {
|
||||
helpText="When error is true the help text will be rendered as an error message"
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
clearButtonOnClick={handleOnClear}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@ -447,7 +420,7 @@ export const CustomLabelOrHelpText = () => (
|
||||
<FormTextField
|
||||
id="custom-spending-cap"
|
||||
placeholder="Enter a number"
|
||||
rightAccessory={<ButtonLink>Max</ButtonLink>}
|
||||
endAccessory={<ButtonLink>Max</ButtonLink>}
|
||||
marginBottom={4}
|
||||
type={TEXT_FIELD_TYPES.NUMBER}
|
||||
/>
|
||||
|
@ -2,10 +2,7 @@
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
|
||||
import {
|
||||
renderControlledInput,
|
||||
renderWithUserEvent,
|
||||
} from '../../../../test/lib/render-helpers';
|
||||
import { renderWithUserEvent } from '../../../../test/lib/render-helpers';
|
||||
|
||||
import { Size } from '../../../helpers/constants/design-system';
|
||||
|
||||
@ -81,7 +78,7 @@ describe('FormTextField', () => {
|
||||
helpText="test help text"
|
||||
/>,
|
||||
);
|
||||
expect(getByTestId('text-field')).toHaveClass('mm-text-field-base--error');
|
||||
expect(getByTestId('text-field')).toHaveClass('mm-text-field--error');
|
||||
expect(getByText('test help text')).toHaveClass(
|
||||
'mm-text--color-error-default',
|
||||
);
|
||||
@ -154,17 +151,17 @@ describe('FormTextField', () => {
|
||||
'mm-form-text-field__label test',
|
||||
);
|
||||
});
|
||||
// leftAccessory, // rightAccessory
|
||||
// startAccessory, // endAccessory
|
||||
it('should render with right and left accessories', () => {
|
||||
const { getByRole, getByText } = render(
|
||||
<FormTextField
|
||||
leftAccessory={<div>left accessory</div>}
|
||||
rightAccessory={<div>right accessory</div>}
|
||||
startAccessory={<div>start accessory</div>}
|
||||
endAccessory={<div>end accessory</div>}
|
||||
/>,
|
||||
);
|
||||
expect(getByRole('textbox')).toBeDefined();
|
||||
expect(getByText('left accessory')).toBeDefined();
|
||||
expect(getByText('right accessory')).toBeDefined();
|
||||
expect(getByText('start accessory')).toBeDefined();
|
||||
expect(getByText('end accessory')).toBeDefined();
|
||||
});
|
||||
// maxLength;
|
||||
it('should render with maxLength and not allow more than the set characters', async () => {
|
||||
@ -230,7 +227,7 @@ describe('FormTextField', () => {
|
||||
readOnly
|
||||
value="test value"
|
||||
data-testid="read-only"
|
||||
inputProps={{ 'data-testid': 'text-field-base-readonly' }}
|
||||
inputProps={{ 'data-testid': 'text-field-readonly' }}
|
||||
/>,
|
||||
);
|
||||
await user.type(getByRole('textbox'), 'test');
|
||||
@ -266,9 +263,9 @@ describe('FormTextField', () => {
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
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');
|
||||
expect(getByTestId('sm')).toHaveClass('mm-text-field--size-sm');
|
||||
expect(getByTestId('md')).toHaveClass('mm-text-field--size-md');
|
||||
expect(getByTestId('lg')).toHaveClass('mm-text-field--size-lg');
|
||||
});
|
||||
// textFieldProps
|
||||
it('should render with textFieldProps', () => {
|
||||
@ -288,45 +285,11 @@ describe('FormTextField', () => {
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
expect(getByTestId('truncate')).toHaveClass('mm-text-field-base--truncate');
|
||||
expect(getByTestId('truncate')).toHaveClass('mm-text-field--truncate');
|
||||
expect(getByTestId('no-truncate')).not.toHaveClass(
|
||||
'mm-text-field-base--truncate',
|
||||
'mm-text-field--truncate',
|
||||
);
|
||||
});
|
||||
// showClearButton
|
||||
it('should render showClearButton button when showClearButton is true and value exists', async () => {
|
||||
// As showClearButton is intended to be used with a controlled input we need to use renderControlledInput
|
||||
const { user, getByRole } = renderControlledInput(FormTextField, {
|
||||
showClearButton: true,
|
||||
});
|
||||
await user.type(getByRole('textbox'), 'test value');
|
||||
expect(getByRole('textbox')).toHaveValue('test value');
|
||||
expect(getByRole('button', { name: /Clear/u })).toBeDefined();
|
||||
});
|
||||
// clearButtonOnClick
|
||||
it('should fire onClick event when passed to clearButtonOnClick when clear button is clicked', async () => {
|
||||
// As showClearButton is intended to be used with a controlled input we need to use renderControlledInput
|
||||
const fn = jest.fn();
|
||||
const { user, getByRole } = renderControlledInput(FormTextField, {
|
||||
showClearButton: true,
|
||||
clearButtonOnClick: fn,
|
||||
});
|
||||
await user.type(getByRole('textbox'), 'test value');
|
||||
await user.click(getByRole('button', { name: /Clear/u }));
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
// clearButtonProps,
|
||||
it('should fire onClick event when passed to clearButtonProps.onClick prop', async () => {
|
||||
// As showClearButton is intended to be used with a controlled input we need to use renderControlledInput
|
||||
const fn = jest.fn();
|
||||
const { user, getByRole } = renderControlledInput(FormTextField, {
|
||||
showClearButton: true,
|
||||
clearButtonProps: { onClick: fn },
|
||||
});
|
||||
await user.type(getByRole('textbox'), 'test value');
|
||||
await user.click(getByRole('button', { name: /Clear/u }));
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
// type,
|
||||
it('should render with different types', () => {
|
||||
const { getByTestId } = render(
|
||||
|
@ -180,7 +180,7 @@ export const DefaultStory = (args) => {
|
||||
textAlign: TEXT_ALIGN.CENTER,
|
||||
}}
|
||||
backgroundColor={BackgroundColor.backgroundAlternative}
|
||||
rightAccessory={
|
||||
endAccessory={
|
||||
<ButtonIcon
|
||||
iconName={ICON_NAMES.COPY}
|
||||
size={Size.SM}
|
||||
|
@ -28,11 +28,6 @@ export { Tag } from './tag';
|
||||
export { TagUrl } from './tag-url';
|
||||
export { Text, TEXT_DIRECTIONS } from './text';
|
||||
export { TextField, TEXT_FIELD_TYPES, TEXT_FIELD_SIZES } from './text-field';
|
||||
export {
|
||||
TextFieldBase,
|
||||
TEXT_FIELD_BASE_SIZES,
|
||||
TEXT_FIELD_BASE_TYPES,
|
||||
} from './text-field-base';
|
||||
export { TextFieldSearch } from './text-field-search';
|
||||
|
||||
// Molecules
|
||||
|
@ -62,7 +62,7 @@ Use the `htmlFor` prop to allow the `Label` to focus on an input with the same i
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { Label, TextFieldBase } from '../../component-library';
|
||||
import { Label, TextField } from '../../component-library';
|
||||
|
||||
<Label htmlFor="add-network">Add network</Label>
|
||||
<TextField id="add-network" placeholder="Enter network name" />
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { Icon, ICON_NAMES } from '../icon';
|
||||
import { TextFieldBase } from '../text-field-base';
|
||||
import { TextField } from '../text-field';
|
||||
|
||||
import { Label } from './label';
|
||||
|
||||
@ -31,7 +31,7 @@ describe('label', () => {
|
||||
const { getByText, getByRole } = render(
|
||||
<>
|
||||
<Label htmlFor="input">label</Label>
|
||||
<TextFieldBase id="input" />
|
||||
<TextField id="input" />
|
||||
</>,
|
||||
);
|
||||
const input = getByRole('textbox');
|
||||
@ -46,7 +46,7 @@ describe('label', () => {
|
||||
<>
|
||||
<Label>
|
||||
Label text
|
||||
<TextFieldBase />
|
||||
<TextField />
|
||||
</Label>
|
||||
</>,
|
||||
);
|
||||
|
@ -1,17 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TextFieldBase should render correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="box mm-text-field-base mm-text-field-base--size-md mm-text-field-base--truncate box--display-inline-flex box--flex-direction-row box--align-items-center box--background-color-background-default box--rounded-sm box--border-width-1 box--border-style-solid"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class="box mm-text mm-text-field-base__input mm-text--body-md mm-text--color-text-default box--padding-right-4 box--padding-left-4 box--flex-direction-row box--background-color-transparent"
|
||||
focused="false"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -1,5 +0,0 @@
|
||||
export { TextFieldBase } from './text-field-base';
|
||||
export {
|
||||
TEXT_FIELD_BASE_SIZES,
|
||||
TEXT_FIELD_BASE_TYPES,
|
||||
} from './text-field-base.constants';
|
@ -1,13 +0,0 @@
|
||||
import { Size } from '../../../helpers/constants/design-system';
|
||||
|
||||
export const TEXT_FIELD_BASE_SIZES = {
|
||||
SM: Size.SM,
|
||||
MD: Size.MD,
|
||||
LG: Size.LG,
|
||||
};
|
||||
export const TEXT_FIELD_BASE_TYPES = {
|
||||
TEXT: 'text',
|
||||
NUMBER: 'number',
|
||||
PASSWORD: 'password',
|
||||
SEARCH: 'search',
|
||||
};
|
@ -1,266 +0,0 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import {
|
||||
DISPLAY,
|
||||
Size,
|
||||
AlignItems,
|
||||
TextVariant,
|
||||
BorderRadius,
|
||||
BackgroundColor,
|
||||
} 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 = Size.MD,
|
||||
type = 'text',
|
||||
truncate = true,
|
||||
value,
|
||||
InputComponent = Text,
|
||||
...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 && !disabled) {
|
||||
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={BackgroundColor.backgroundDefault}
|
||||
alignItems={AlignItems.center}
|
||||
borderWidth={1}
|
||||
borderRadius={BorderRadius.SM}
|
||||
paddingLeft={leftAccessory ? 4 : 0}
|
||||
paddingRight={rightAccessory ? 4 : 0}
|
||||
onClick={handleClick}
|
||||
{...props}
|
||||
>
|
||||
{leftAccessory}
|
||||
<InputComponent
|
||||
aria-invalid={error}
|
||||
as="input"
|
||||
autoComplete={autoComplete ? 'on' : 'off'}
|
||||
autoFocus={autoFocus}
|
||||
backgroundColor={BackgroundColor.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 : 4}
|
||||
paddingRight={rightAccessory ? 2 : 4}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
ref={handleInputRef}
|
||||
required={required}
|
||||
value={value}
|
||||
variant={TextVariant.bodyMd}
|
||||
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.bool,
|
||||
/**
|
||||
* 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,
|
||||
/**
|
||||
* The the component that is rendered as the input
|
||||
* Defaults to the Text component
|
||||
*/
|
||||
InputComponent: PropTypes.elementType,
|
||||
/**
|
||||
* 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 when the TextField is clicked on
|
||||
*/
|
||||
onClick: 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)),
|
||||
/**
|
||||
* If true will ellipse the text of the input
|
||||
* Defaults to true
|
||||
*/
|
||||
truncate: PropTypes.bool,
|
||||
/**
|
||||
* 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';
|
@ -1,504 +0,0 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useArgs } from '@storybook/client-api';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
DISPLAY,
|
||||
FLEX_DIRECTION,
|
||||
AlignItems,
|
||||
TextVariant,
|
||||
IconColor,
|
||||
BackgroundColor,
|
||||
TextColor,
|
||||
Size,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import Box from '../../ui/box/box';
|
||||
|
||||
import {
|
||||
AvatarAccount,
|
||||
AvatarToken,
|
||||
Button,
|
||||
ButtonIcon,
|
||||
ICON_NAMES,
|
||||
Icon,
|
||||
Text,
|
||||
} from '..';
|
||||
|
||||
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',
|
||||
|
||||
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...',
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args) => {
|
||||
const [{ value }, updateArgs] = useArgs();
|
||||
const handleOnChange = (e) => {
|
||||
updateArgs({ value: e.target.value });
|
||||
};
|
||||
return <TextFieldBase {...args} value={value} onChange={handleOnChange} />;
|
||||
};
|
||||
|
||||
export const DefaultStory = Template.bind({});
|
||||
DefaultStory.storyName = 'Default';
|
||||
|
||||
export const SizeStory = (args) => {
|
||||
return (
|
||||
<Box
|
||||
display={DISPLAY.INLINE_FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
gap={4}
|
||||
>
|
||||
<TextFieldBase
|
||||
{...args}
|
||||
placeholder="Size.SM (height: 32px)"
|
||||
size={Size.SM}
|
||||
/>
|
||||
<TextFieldBase
|
||||
{...args}
|
||||
placeholder="Size.MD (height: 40px)"
|
||||
size={Size.MD}
|
||||
/>
|
||||
<TextFieldBase
|
||||
{...args}
|
||||
placeholder="Size.LG (height: 48px)"
|
||||
size={Size.LG}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
SizeStory.storyName = 'Size';
|
||||
|
||||
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: '',
|
||||
address: '',
|
||||
amount: 1,
|
||||
accountAddress: '0x514910771af9ca656af840dff83e8264ecf986ca',
|
||||
});
|
||||
const handleOnChange = (e) => {
|
||||
setValue({ ...value, [e.target.name]: e.target.value });
|
||||
};
|
||||
const handleTokenPrice = (tokenAmount, priceUSD) => {
|
||||
return tokenAmount * priceUSD;
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
display={DISPLAY.INLINE_FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
gap={4}
|
||||
>
|
||||
<TextFieldBase
|
||||
{...args}
|
||||
placeholder="Search"
|
||||
value={value.search}
|
||||
name="search"
|
||||
onChange={handleOnChange}
|
||||
leftAccessory={
|
||||
<Icon color={IconColor.iconAlternative} name={ICON_NAMES.SEARCH} />
|
||||
}
|
||||
/>
|
||||
<TextFieldBase
|
||||
{...args}
|
||||
placeholder="Public address (0x), or ENS"
|
||||
value={value.address}
|
||||
name="address"
|
||||
onChange={handleOnChange}
|
||||
rightAccessory={
|
||||
<ButtonIcon
|
||||
iconName={ICON_NAMES.SCAN_BARCODE}
|
||||
ariaLabel="Scan QR code"
|
||||
iconProps={{ color: IconColor.primaryDefault }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<TextFieldBase
|
||||
{...args}
|
||||
placeholder="Enter amount"
|
||||
value={value.amount}
|
||||
name="amount"
|
||||
onChange={handleOnChange}
|
||||
type="number"
|
||||
truncate
|
||||
leftAccessory={
|
||||
<Box
|
||||
as="button"
|
||||
style={{ padding: 0 }}
|
||||
backgroundColor={BackgroundColor.transparent}
|
||||
gap={1}
|
||||
display={DISPLAY.FLEX}
|
||||
alignItems={AlignItems.center}
|
||||
>
|
||||
<AvatarToken
|
||||
tokenName="eth"
|
||||
tokenImageUrl="./images/eth_logo.svg"
|
||||
size={Size.SM}
|
||||
/>
|
||||
<Text>ETH</Text>
|
||||
<Icon
|
||||
name={ICON_NAMES.ARROW_DOWN}
|
||||
color={IconColor.iconDefault}
|
||||
size={Size.SM}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
rightAccessory={
|
||||
<Text
|
||||
variant={TextVariant.bodySm}
|
||||
color={TextColor.textAlternative}
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
= ${handleTokenPrice(value.amount, 1173.58)}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<TextFieldBase
|
||||
{...args}
|
||||
placeholder="Public address (0x), or ENS"
|
||||
value={value.accountAddress}
|
||||
name="accountAddress"
|
||||
onChange={handleOnChange}
|
||||
truncate
|
||||
leftAccessory={
|
||||
value.accountAddress && (
|
||||
<AvatarAccount size={Size.SM} address={value.accountAddress} />
|
||||
)
|
||||
}
|
||||
rightAccessory={
|
||||
value.accountAddress.length === 42 && (
|
||||
<Icon name={ICON_NAMES.CHECK} color={IconColor.successDefault} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</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 (
|
||||
<Box display={DISPLAY.FLEX}>
|
||||
<TextFieldBase
|
||||
{...args}
|
||||
inputRef={inputRef}
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
<Button marginLeft={1} onClick={handleOnClick}>
|
||||
Edit
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomInputComponent = React.forwardRef(
|
||||
(
|
||||
{
|
||||
as,
|
||||
autoComplete,
|
||||
autoFocus,
|
||||
defaultValue,
|
||||
disabled,
|
||||
focused,
|
||||
id,
|
||||
inputProps,
|
||||
inputRef,
|
||||
maxLength,
|
||||
name,
|
||||
onBlur,
|
||||
onChange,
|
||||
onFocus,
|
||||
padding,
|
||||
paddingLeft,
|
||||
paddingRight,
|
||||
placeholder,
|
||||
readOnly,
|
||||
required,
|
||||
value,
|
||||
variant,
|
||||
type,
|
||||
className,
|
||||
'aria-invalid': ariaInvalid,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => (
|
||||
<Box
|
||||
display={DISPLAY.FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
ref={ref}
|
||||
{...{ padding, paddingLeft, paddingRight, ...props }}
|
||||
>
|
||||
<Box display={DISPLAY.INLINE_FLEX}>
|
||||
<Text
|
||||
style={{ padding: 0 }}
|
||||
aria-invalid={ariaInvalid}
|
||||
ref={inputRef}
|
||||
{...{
|
||||
className,
|
||||
as,
|
||||
autoComplete,
|
||||
autoFocus,
|
||||
defaultValue,
|
||||
disabled,
|
||||
focused,
|
||||
id,
|
||||
maxLength,
|
||||
name,
|
||||
onBlur,
|
||||
onChange,
|
||||
onFocus,
|
||||
placeholder,
|
||||
readOnly,
|
||||
required,
|
||||
value,
|
||||
variant,
|
||||
type,
|
||||
...inputProps,
|
||||
}}
|
||||
/>
|
||||
<Text variant={TextVariant.bodyXs} color={TextColor.textAlternative}>
|
||||
GoerliETH
|
||||
</Text>
|
||||
</Box>
|
||||
<Text variant={TextVariant.bodyXs}>No conversion rate available</Text>
|
||||
</Box>
|
||||
),
|
||||
);
|
||||
|
||||
CustomInputComponent.propTypes = {
|
||||
/**
|
||||
* The custom input component should accepts all props that the
|
||||
* InputComponent accepts in ./text-field-base.js
|
||||
*/
|
||||
autoFocus: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
disabled: PropTypes.bool,
|
||||
id: PropTypes.string,
|
||||
inputProps: PropTypes.object,
|
||||
inputRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
|
||||
maxLength: PropTypes.number,
|
||||
name: PropTypes.string,
|
||||
onBlur: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
readOnly: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
type: PropTypes.oneOf(Object.values(TEXT_FIELD_BASE_TYPES)),
|
||||
/**
|
||||
* Because we manipulate the type in TextFieldBase so the html element
|
||||
* receives the correct attribute we need to change the autoComplete
|
||||
* propType to a string
|
||||
*/
|
||||
autoComplete: PropTypes.string,
|
||||
/**
|
||||
* The custom input component should also accept all the props from Box
|
||||
*/
|
||||
...Box.propTypes,
|
||||
};
|
||||
|
||||
CustomInputComponent.displayName = 'CustomInputComponent';
|
||||
|
||||
export const InputComponent = (args) => (
|
||||
<TextFieldBase
|
||||
{...args}
|
||||
placeholder="0"
|
||||
type="number"
|
||||
size={Size.LG}
|
||||
InputComponent={CustomInputComponent}
|
||||
leftAccessory={
|
||||
<Icon color={IconColor.iconAlternative} name={ICON_NAMES.WALLET} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
InputComponent.args = { autoComplete: true };
|
||||
|
||||
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' };
|
@ -1,258 +0,0 @@
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { renderWithUserEvent } from '../../../../test/lib/render-helpers';
|
||||
|
||||
import { Size } from '../../../helpers/constants/design-system';
|
||||
|
||||
import Box from '../../ui/box';
|
||||
|
||||
import { TextFieldBase } from './text-field-base';
|
||||
|
||||
describe('TextFieldBase', () => {
|
||||
it('should render correctly', () => {
|
||||
const { getByRole, container } = render(<TextFieldBase />);
|
||||
expect(getByRole('textbox')).toBeDefined();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
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 with focused state when clicked', async () => {
|
||||
const { getByTestId, user } = renderWithUserEvent(
|
||||
<TextFieldBase
|
||||
data-testid="text-field-base"
|
||||
inputProps={{ 'data-testid': 'input' }}
|
||||
/>,
|
||||
);
|
||||
const textFieldBase = getByTestId('input');
|
||||
|
||||
await user.click(textFieldBase);
|
||||
expect(getByTestId('input')).toHaveFocus();
|
||||
expect(getByTestId('text-field-base')).toHaveClass(
|
||||
'mm-text-field-base--focused ',
|
||||
);
|
||||
});
|
||||
it('should render and fire onFocus and onBlur events', async () => {
|
||||
const onFocus = jest.fn();
|
||||
const onBlur = jest.fn();
|
||||
const { getByTestId, user } = renderWithUserEvent(
|
||||
<TextFieldBase
|
||||
inputProps={{ 'data-testid': 'text-field-base' }}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>,
|
||||
);
|
||||
|
||||
const textFieldBase = getByTestId('text-field-base');
|
||||
await user.click(textFieldBase);
|
||||
expect(onFocus).toHaveBeenCalledTimes(1);
|
||||
fireEvent.blur(textFieldBase);
|
||||
expect(onBlur).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should render and fire onChange event', async () => {
|
||||
const onChange = jest.fn();
|
||||
const { getByTestId, user } = renderWithUserEvent(
|
||||
<TextFieldBase
|
||||
inputProps={{ 'data-testid': 'text-field-base' }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
const textFieldBase = getByTestId('text-field-base');
|
||||
await user.type(textFieldBase, '123');
|
||||
expect(textFieldBase).toHaveValue('123');
|
||||
expect(onChange).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
it('should render and fire onClick event', async () => {
|
||||
const onClick = jest.fn();
|
||||
const { getByTestId, user } = renderWithUserEvent(
|
||||
<TextFieldBase
|
||||
inputProps={{ 'data-testid': 'text-field-base' }}
|
||||
onClick={onClick}
|
||||
/>,
|
||||
);
|
||||
const textFieldBase = getByTestId('text-field-base');
|
||||
|
||||
await user.click(textFieldBase);
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should render with different size classes', () => {
|
||||
const { getByTestId } = render(
|
||||
<>
|
||||
<TextFieldBase size={Size.SM} data-testid="sm" />
|
||||
<TextFieldBase size={Size.MD} data-testid="md" />
|
||||
<TextFieldBase size={Size.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 as true by default and remove it when truncate is false', () => {
|
||||
const { getByTestId } = render(
|
||||
<>
|
||||
<TextFieldBase data-testid="truncate" />
|
||||
<TextFieldBase truncate={false} data-testid="no-truncate" />
|
||||
</>,
|
||||
);
|
||||
expect(getByTestId('truncate')).toHaveClass('mm-text-field-base--truncate');
|
||||
expect(getByTestId('no-truncate')).not.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', async () => {
|
||||
const mockOnClick = jest.fn();
|
||||
const mockOnFocus = jest.fn();
|
||||
const { getByRole, getByTestId, user } = renderWithUserEvent(
|
||||
<TextFieldBase
|
||||
disabled
|
||||
onFocus={mockOnFocus}
|
||||
onClick={mockOnClick}
|
||||
data-testid="text-field-base"
|
||||
/>,
|
||||
);
|
||||
|
||||
const textFieldBase = getByTestId('text-field-base');
|
||||
|
||||
await user.click(textFieldBase);
|
||||
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 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, user } = renderWithUserEvent(
|
||||
<TextFieldBase maxLength={5} />,
|
||||
);
|
||||
const textFieldBase = getByRole('textbox');
|
||||
await user.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', async () => {
|
||||
const { getByTestId, getByRole, user } = renderWithUserEvent(
|
||||
<TextFieldBase readOnly data-testid="read-only" />,
|
||||
);
|
||||
const textFieldBase = getByTestId('read-only');
|
||||
await user.type(textFieldBase, '1234567890');
|
||||
expect(getByRole('textbox').value).toBe('');
|
||||
expect(getByRole('textbox')).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',
|
||||
'',
|
||||
);
|
||||
});
|
||||
it('should render with a custom input and still work', async () => {
|
||||
const CustomInputComponent = React.forwardRef((props, ref) => (
|
||||
<Box ref={ref} as="input" {...props} />
|
||||
));
|
||||
CustomInputComponent.displayName = 'CustomInputComponent'; // fixes eslint error
|
||||
const { getByTestId, user } = renderWithUserEvent(
|
||||
<TextFieldBase
|
||||
InputComponent={CustomInputComponent}
|
||||
inputProps={{ 'data-testid': 'text-field-base', className: 'test' }}
|
||||
/>,
|
||||
);
|
||||
const textFieldBase = getByTestId('text-field-base');
|
||||
|
||||
expect(textFieldBase.value).toBe(''); // initial value is empty string
|
||||
await user.type(textFieldBase, '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
|
||||
});
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
|
||||
|
||||
import { TextFieldBase, TextField } from '..';
|
||||
import { TextField } from '..';
|
||||
|
||||
import { TextFieldSearch } from './text-field-search';
|
||||
|
||||
@ -23,11 +23,6 @@ component props
|
||||
|
||||
<ArgsTable of={TextField} />
|
||||
|
||||
`TextFieldSearch` accepts all [TextFieldBase](/docs/components-componentlibrary-textfieldbase--default-story#props)
|
||||
component props
|
||||
|
||||
<ArgsTable of={TextFieldBase} />
|
||||
|
||||
### Clear Button On Click
|
||||
|
||||
`TextFieldSearch` displays a clear button when text is entered into the input. Use the `clearButtonOnClick` prop to pass an `onClick` event handler to clear the value of the input. To hide the clear button, pass `false` to the `showClearButton` prop.
|
||||
|
@ -3,7 +3,7 @@
|
||||
exports[`TextFieldSearch should render correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="box mm-text-field-base mm-text-field-base--size-md mm-text-field-base--truncate mm-text-field mm-text-field-search box--padding-left-4 box--display-inline-flex box--flex-direction-row box--align-items-center box--background-color-background-default box--rounded-sm box--border-width-1 box--border-style-solid"
|
||||
class="box mm-text-field mm-text-field--size-md mm-text-field--truncate mm-text-field-search box--padding-left-4 box--display-inline-flex box--flex-direction-row box--align-items-center box--background-color-background-default box--rounded-sm box--border-width-1 box--border-style-solid"
|
||||
>
|
||||
<div
|
||||
class="box mm-icon mm-icon--size-sm box--flex-direction-row box--color-inherit"
|
||||
@ -11,7 +11,7 @@ exports[`TextFieldSearch should render correctly 1`] = `
|
||||
/>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class="box mm-text mm-text-field-base__input mm-text--body-md mm-text--color-text-default box--margin-right-6 box--padding-right-4 box--padding-left-2 box--flex-direction-row box--background-color-transparent"
|
||||
class="box mm-text mm-text-field__input mm-text--body-md mm-text--color-text-default box--margin-right-6 box--padding-right-4 box--padding-left-2 box--flex-direction-row box--background-color-transparent"
|
||||
focused="false"
|
||||
type="search"
|
||||
value=""
|
||||
|
@ -6,26 +6,46 @@ import { Size } from '../../../helpers/constants/design-system';
|
||||
|
||||
import { ButtonIcon } from '../button-icon';
|
||||
import { Icon, ICON_NAMES } from '../icon';
|
||||
import { TextFieldBase, TEXT_FIELD_BASE_TYPES } from '../text-field-base';
|
||||
import { TextField } from '../text-field';
|
||||
import { TextField, TEXT_FIELD_TYPES } from '../text-field';
|
||||
|
||||
export const TextFieldSearch = ({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
showClearButton = true, // only works with a controlled input
|
||||
clearButtonOnClick,
|
||||
clearButtonProps,
|
||||
className,
|
||||
endAccessory,
|
||||
inputProps,
|
||||
value,
|
||||
onChange,
|
||||
...props
|
||||
}) => (
|
||||
<TextField
|
||||
className={classnames('mm-text-field-search', className)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
type={TEXT_FIELD_BASE_TYPES.SEARCH}
|
||||
leftAccessory={<Icon name={ICON_NAMES.SEARCH} size={Size.SM} />}
|
||||
showClearButton
|
||||
clearButtonOnClick={clearButtonOnClick}
|
||||
clearButtonProps={clearButtonProps}
|
||||
type={TEXT_FIELD_TYPES.SEARCH}
|
||||
endAccessory={
|
||||
value && showClearButton ? (
|
||||
<>
|
||||
<ButtonIcon
|
||||
className="mm-text-field__button-clear"
|
||||
ariaLabel="Clear" // TODO: i18n
|
||||
iconName={ICON_NAMES.CLOSE}
|
||||
size={Size.SM}
|
||||
onClick={clearButtonOnClick}
|
||||
{...clearButtonProps}
|
||||
/>
|
||||
{endAccessory}
|
||||
</>
|
||||
) : (
|
||||
endAccessory
|
||||
)
|
||||
}
|
||||
startAccessory={<Icon name={ICON_NAMES.SEARCH} size={Size.SM} />}
|
||||
inputProps={{
|
||||
marginRight: showClearButton ? 6 : 0,
|
||||
...inputProps,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@ -34,11 +54,16 @@ TextFieldSearch.propTypes = {
|
||||
/**
|
||||
* The value of the TextFieldSearch
|
||||
*/
|
||||
value: TextFieldBase.propTypes.value,
|
||||
value: TextField.propTypes.value,
|
||||
/**
|
||||
* The onChange handler of the TextFieldSearch
|
||||
*/
|
||||
onChange: TextFieldBase.propTypes.onChange,
|
||||
onChange: TextField.propTypes.onChange,
|
||||
/**
|
||||
* The clear button for the TextFieldSearch.
|
||||
* Defaults to true
|
||||
*/
|
||||
showClearButton: PropTypes.bool,
|
||||
/**
|
||||
* The onClick handler for the clear button
|
||||
* Required unless showClearButton is false
|
||||
@ -66,6 +91,18 @@ TextFieldSearch.propTypes = {
|
||||
* An additional className to apply to the TextFieldSearch
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
/**
|
||||
* Component to appear on the right side of the input
|
||||
*/
|
||||
endAccessory: PropTypes.node,
|
||||
/**
|
||||
* Attributes applied to the `input` element.
|
||||
*/
|
||||
inputProps: PropTypes.object,
|
||||
/**
|
||||
* FormTextField accepts all the props from TextField and Box
|
||||
*/
|
||||
...TextField.propTypes,
|
||||
};
|
||||
|
||||
TextFieldSearch.displayName = 'TextFieldSearch';
|
||||
|
@ -80,7 +80,7 @@ export default {
|
||||
control: 'object',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
leftAccessory: {
|
||||
startAccessory: {
|
||||
control: 'text',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
@ -124,7 +124,7 @@ export default {
|
||||
control: 'boolean',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
rightAccessory: {
|
||||
endAccessory: {
|
||||
control: 'text',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
|
@ -31,20 +31,20 @@ describe('TextFieldSearch', () => {
|
||||
expect(getByRole('searchbox')).toHaveValue('test value');
|
||||
expect(getByRole('button', { name: /Clear/u })).toBeDefined();
|
||||
});
|
||||
it('should still render with the rightAccessory if it exists', async () => {
|
||||
it('should still render with the endAccessory if it exists', async () => {
|
||||
const fn = jest.fn();
|
||||
// As showClearButton is intended to be used with a controlled input we need to use renderControlledInput
|
||||
const { user, getByRole, getByText } = renderControlledInput(
|
||||
TextFieldSearch,
|
||||
{
|
||||
clearButtonOnClick: fn,
|
||||
rightAccessory: <div>right-accessory</div>,
|
||||
endAccessory: <div>end-accessory</div>,
|
||||
},
|
||||
);
|
||||
await user.type(getByRole('searchbox'), 'test value');
|
||||
expect(getByRole('searchbox')).toHaveValue('test value');
|
||||
expect(getByRole('button', { name: /Clear/u })).toBeDefined();
|
||||
expect(getByText('right-accessory')).toBeDefined();
|
||||
expect(getByText('end-accessory')).toBeDefined();
|
||||
});
|
||||
it('should fire onClick event when passed to clearButtonOnClick when clear button is clicked', async () => {
|
||||
// As showClearButton is intended to be used with a controlled input we need to use renderControlledInput
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
|
||||
|
||||
import { TextFieldBase } from '../text-field-base';
|
||||
|
||||
import { TextField } from './text-field';
|
||||
|
||||
# TextField
|
||||
|
||||
The `TextField` component lets users enter and edit text as well as adding a show clear button option. It wraps `TextFieldBase` and functions only as a controlled input.
|
||||
`TextField` lets user enter a text data into a boxed field. It can sometimes contain related information or controls.
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-textfield--default-story" />
|
||||
@ -18,79 +16,318 @@ The `TextField` accepts all props below as well as all [Box](/docs/components-ui
|
||||
|
||||
<ArgsTable of={TextField} />
|
||||
|
||||
`TextField` accepts all [TextFieldBase](/docs/components-componentlibrary-textfieldbase--default-story#props)
|
||||
component props
|
||||
### Size
|
||||
|
||||
<ArgsTable of={TextFieldBase} />
|
||||
Use the `size` prop to set the height of the `TextField`.
|
||||
|
||||
### Show Clear Button
|
||||
Possible sizes include:
|
||||
|
||||
Use the `showClearButton` prop to display a clear button when `TextField` has a value. Use the `clearButtonOnClick` prop to pass an `onClick` event handler to clear the value of the input. Otherwise the clear button will not do anything.
|
||||
- `sm` 32px
|
||||
- `md` 40px
|
||||
- `lg` 48px
|
||||
|
||||
The clear button uses [ButtonIcon](/docs/components-componentlibrary-buttonicon--default-story) and accepts all `ButtonIcon` props.
|
||||
|
||||
**NOTE: The `showClearButton` only works with a controlled input.**
|
||||
Defaults to `md`
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-textfield--show-clear-button" />
|
||||
<Story id="components-componentlibrary-textfield--size-story" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { Size } from '../../../helpers/constants/design-system';
|
||||
import { TextField } from '../../component-library';
|
||||
|
||||
<TextField size={Size.SM} />
|
||||
<TextField size={Size.MD} />
|
||||
<TextField size={Size.LG} />
|
||||
```
|
||||
|
||||
### Type
|
||||
|
||||
Use the `type` prop to change the type of input.
|
||||
|
||||
Possible types include:
|
||||
|
||||
- `text`
|
||||
- `number`
|
||||
- `password`
|
||||
|
||||
Defaults to `text`.
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-textfield--type" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { TextField } from '../../component-library';
|
||||
|
||||
const [value, setValue] = useState('show clear');
|
||||
|
||||
const handleOnChange = (e) => {
|
||||
setValue(e.target.value);
|
||||
};
|
||||
|
||||
const handleOnClear = () => {
|
||||
setValue('');
|
||||
};
|
||||
|
||||
<TextField
|
||||
placeholder="Enter text to show clear"
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
showClearButton
|
||||
clearButtonOnClick={handleOnClear}
|
||||
/>;
|
||||
<TextField type="text" /> // (Default)
|
||||
<TextField type="number" />
|
||||
<TextField type="password" />
|
||||
```
|
||||
|
||||
### Clear Button Props
|
||||
### Truncate
|
||||
|
||||
Use the `clearButtonProps` to access other props of the clear button.
|
||||
Use the `truncate` prop to truncate the text of the the `TextField`. Defaults to `true`.
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-textfield--clear-button-props" />
|
||||
<Story id="components-componentlibrary-textfield--truncate" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import React, { useState } from 'react';
|
||||
import { Color, BorderRadius } from '../../../helpers/constants/design-system';
|
||||
|
||||
import { TextField } from '../../component-library';
|
||||
|
||||
const [value, setValue] = useState('show clear');
|
||||
<TextField truncate />; // truncate is set to `true` by default
|
||||
<TextField truncate={false} />;
|
||||
```
|
||||
|
||||
### Start accessory End Accessory
|
||||
|
||||
Use the `startAccessory` and `endAccessory` props to add components such as icons or buttons to either side of the `TextField`.
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-textfield--start-accessory-end-accessory" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { COLORS, SIZES, DISPLAY } from '../../../helpers/constants/design-system';
|
||||
import { ButtonIcon, Icon, ICON_NAMES, TextField } from '../../component-library';
|
||||
|
||||
<TextField
|
||||
placeholder="Search"
|
||||
startAccessory={
|
||||
<Icon
|
||||
color={COLORS.ICON_ALTERNATIVE}
|
||||
name={ICON_NAMES.SEARCH}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
placeholder="Public address (0x), or ENS"
|
||||
endAccessory={
|
||||
<ButtonIcon
|
||||
iconName={ICON_NAMES.SCAN_BARCODE}
|
||||
ariaLabel="Scan QR code"
|
||||
iconProps={{ color: COLORS.PRIMARY_DEFAULT }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
placeholder="Enter amount"
|
||||
type="number"
|
||||
truncate
|
||||
startAccessory={<SelectTokenComponent />}
|
||||
endAccessory={<TokenValueInUSDComponent />}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
placeholder="Public address (0x), or ENS"
|
||||
truncate
|
||||
startAccessory={<AvatarAccount />}
|
||||
endAccessory={
|
||||
isAddressValid && (
|
||||
<Icon
|
||||
name={ICON_NAMES.CHECK}
|
||||
color={COLORS.SUCCESS_DEFAULT}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
```
|
||||
|
||||
### Input Ref
|
||||
|
||||
Use the `inputRef` prop to access the ref of the `<input />` html element of `TextField`. This is useful for focusing the input from a button or other component.
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-textfield--input-ref" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { Button, TextField } from '../../component-library';
|
||||
|
||||
const inputRef = useRef(null);
|
||||
const [value, setValue] = useState('');
|
||||
const handleOnClick = () => {
|
||||
inputRef.current.focus();
|
||||
};
|
||||
const handleOnChange = (e) => {
|
||||
setValue(e.target.value);
|
||||
};
|
||||
|
||||
const handleOnClear = () => {
|
||||
setValue('');
|
||||
};
|
||||
|
||||
<TextField
|
||||
placeholder="Enter text to show clear"
|
||||
inputRef={inputRef}
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
showClearButton
|
||||
clearButtonOnClick={handleOnClear}
|
||||
clearButtonProps={{
|
||||
backgroundColor: Color.backgroundAlternative,
|
||||
borderRadius: BorderRadius.XS,
|
||||
'data-testid': 'clear-button',
|
||||
}}
|
||||
/>;
|
||||
/>
|
||||
<Button marginLeft={1} onClick={handleOnClick}>
|
||||
Edit
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Input Component
|
||||
|
||||
Use the `InputComponent` prop change the component used for the input element. This is useful for replacing the base input with a custom input while retaining the functionality of the `TextField`.
|
||||
|
||||
Defaults to the [Text](/docs/components-componentlibrary-text--default-story) component
|
||||
|
||||
To function fully the custom component should accept the following props:
|
||||
|
||||
- `aria-invalid`
|
||||
- `as`
|
||||
- `autoComplete`
|
||||
- `autoFocus`
|
||||
- `backgroundColor`
|
||||
- `defaultValue`
|
||||
- `disabled`
|
||||
- `focused`
|
||||
- `id`
|
||||
- `margin`
|
||||
- `maxLength`
|
||||
- `name`
|
||||
- `onBlur`
|
||||
- `onChange`
|
||||
- `onFocus`
|
||||
- `padding`
|
||||
- `paddingLeft`
|
||||
- `paddingRight`
|
||||
- `placeholder`
|
||||
- `readOnly`
|
||||
- `ref`
|
||||
- `required`
|
||||
- `value`
|
||||
- `variant`
|
||||
- `type`
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-textfield--input-component" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { TextField, Icon, ICON_NAMES } from '../../component-library';
|
||||
|
||||
// should map the props to the custom input component
|
||||
const CustomInputComponent = () => <div>{/* Custom input component */}</div>;
|
||||
|
||||
const TextFieldCustomInput = (args) => (
|
||||
<TextField
|
||||
size={SIZES.LG}
|
||||
InputComponent={CustomInputComponent}
|
||||
startAccessory={
|
||||
<Icon color={COLORS.ICON_ALTERNATIVE} name={ICON_NAMES.WALLET} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
### 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="components-componentlibrary-textfield--auto-complete" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { TextField } from '../../component-library';
|
||||
|
||||
<TextField type="password" autoComplete />;
|
||||
```
|
||||
|
||||
### Auto Focus
|
||||
|
||||
Use the `autoFocus` prop to focus the `TextField` during the first mount
|
||||
|
||||
To view story see [Canvas tab](/story/components-componentlibrary-textfield--auto-complete). Removing it from docs because created annoying reading experience 😁
|
||||
|
||||
```jsx
|
||||
import { TextField } from '../../component-library';
|
||||
|
||||
<TextField autoFocus />;
|
||||
```
|
||||
|
||||
### Default Value
|
||||
|
||||
Use the `defaultValue` prop to set the default value of the `TextField`
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-textfield--default-value" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { TextField } from '../../component-library';
|
||||
|
||||
<TextField defaultValue="default value" />;
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` prop to set the disabled state of the `TextField`
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-textfield--disabled" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { TextField } from '../../component-library';
|
||||
|
||||
<TextField disabled />;
|
||||
```
|
||||
|
||||
### Error
|
||||
|
||||
Use the `error` prop to set the error state of the `TextField`
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-textfield--error-story" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { TextField } from '../../component-library';
|
||||
|
||||
<TextField error />;
|
||||
```
|
||||
|
||||
### Max Length
|
||||
|
||||
Use the `maxLength` prop to set the maximum allowed input characters for the `TextField`
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-textfield--max-length" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { TextField } from '../../component-library';
|
||||
|
||||
<TextField maxLength={10} />;
|
||||
```
|
||||
|
||||
### Read Only
|
||||
|
||||
Use the `readOnly` prop to set the `TextField` to read only. When `readOnly` is true `TextField` will not have a focus state.
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-textfield--read-only" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { TextField } from '../../component-library';
|
||||
|
||||
<TextField readOnly />;
|
||||
```
|
||||
|
||||
### Required
|
||||
|
||||
Use the `required` prop to set the `TextField` to required. Currently there is no visual difference to the `TextField` when required.
|
||||
|
||||
<Canvas>
|
||||
<Story id="components-componentlibrary-textfield--required" />
|
||||
</Canvas>
|
||||
|
||||
```jsx
|
||||
import { TextField } from '../../component-library';
|
||||
|
||||
// Currently no visual difference
|
||||
<TextField required />;
|
||||
```
|
||||
|
@ -3,11 +3,11 @@
|
||||
exports[`TextField should render correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="box mm-text-field-base mm-text-field-base--size-md mm-text-field-base--truncate mm-text-field box--display-inline-flex box--flex-direction-row box--align-items-center box--background-color-background-default box--rounded-sm box--border-width-1 box--border-style-solid"
|
||||
class="box mm-text-field mm-text-field--size-md mm-text-field--truncate box--display-inline-flex box--flex-direction-row box--align-items-center box--background-color-background-default box--rounded-sm box--border-width-1 box--border-style-solid"
|
||||
>
|
||||
<input
|
||||
autocomplete="off"
|
||||
class="box mm-text mm-text-field-base__input mm-text--body-md mm-text--color-text-default box--padding-right-4 box--padding-left-4 box--flex-direction-row box--background-color-transparent"
|
||||
class="box mm-text mm-text-field__input mm-text--body-md mm-text--color-text-default box--padding-right-4 box--padding-left-4 box--flex-direction-row box--background-color-transparent"
|
||||
focused="false"
|
||||
type="text"
|
||||
value=""
|
||||
|
@ -1,7 +1,13 @@
|
||||
import {
|
||||
TEXT_FIELD_BASE_SIZES,
|
||||
TEXT_FIELD_BASE_TYPES,
|
||||
} from '../text-field-base/text-field-base.constants';
|
||||
import { Size } from '../../../helpers/constants/design-system';
|
||||
|
||||
export const TEXT_FIELD_SIZES = TEXT_FIELD_BASE_SIZES;
|
||||
export const TEXT_FIELD_TYPES = TEXT_FIELD_BASE_TYPES;
|
||||
export const TEXT_FIELD_SIZES = {
|
||||
SM: Size.SM,
|
||||
MD: Size.MD,
|
||||
LG: Size.LG,
|
||||
};
|
||||
export const TEXT_FIELD_TYPES = {
|
||||
TEXT: 'text',
|
||||
NUMBER: 'number',
|
||||
PASSWORD: 'password',
|
||||
SEARCH: 'search',
|
||||
};
|
||||
|
@ -1,81 +1,260 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { Size } from '../../../helpers/constants/design-system';
|
||||
import {
|
||||
DISPLAY,
|
||||
Size,
|
||||
AlignItems,
|
||||
TextVariant,
|
||||
BorderRadius,
|
||||
BackgroundColor,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
|
||||
import { ICON_NAMES } from '../icon';
|
||||
import { ButtonIcon } from '../button-icon';
|
||||
import Box from '../../ui/box';
|
||||
|
||||
import { TextFieldBase } from '../text-field-base';
|
||||
import { Text } from '../text';
|
||||
|
||||
import { TEXT_FIELD_SIZES, TEXT_FIELD_TYPES } from './text-field.constants';
|
||||
|
||||
export const TextField = ({
|
||||
autoComplete,
|
||||
autoFocus,
|
||||
className,
|
||||
showClearButton, // only works with a controlled input
|
||||
clearButtonOnClick,
|
||||
clearButtonProps,
|
||||
rightAccessory,
|
||||
defaultValue,
|
||||
disabled,
|
||||
error,
|
||||
id,
|
||||
inputProps,
|
||||
value,
|
||||
inputRef,
|
||||
startAccessory,
|
||||
endAccessory,
|
||||
maxLength,
|
||||
name,
|
||||
onBlur,
|
||||
onChange,
|
||||
onClick,
|
||||
onFocus,
|
||||
placeholder,
|
||||
readOnly,
|
||||
required,
|
||||
size = Size.MD,
|
||||
type = 'text',
|
||||
truncate = true,
|
||||
value,
|
||||
InputComponent = Text,
|
||||
...props
|
||||
}) => (
|
||||
<TextFieldBase
|
||||
className={classnames('mm-text-field', className)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
rightAccessory={
|
||||
value && showClearButton ? (
|
||||
<>
|
||||
<ButtonIcon
|
||||
className="mm-text-field__button-clear"
|
||||
ariaLabel="Clear" // TODO: i18n
|
||||
iconName={ICON_NAMES.CLOSE}
|
||||
size={Size.SM}
|
||||
onClick={clearButtonOnClick}
|
||||
{...clearButtonProps}
|
||||
/>
|
||||
{rightAccessory}
|
||||
</>
|
||||
) : (
|
||||
rightAccessory
|
||||
)
|
||||
}) => {
|
||||
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);
|
||||
}
|
||||
inputProps={{
|
||||
marginRight: showClearButton ? 6 : 0,
|
||||
...inputProps,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}, [disabled]);
|
||||
|
||||
const handleClick = (event) => {
|
||||
const { current } = internalInputRef;
|
||||
|
||||
if (current) {
|
||||
current.focus();
|
||||
setFocused(true);
|
||||
}
|
||||
|
||||
if (onClick && !disabled) {
|
||||
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',
|
||||
`mm-text-field--size-${size}`,
|
||||
{
|
||||
'mm-text-field--focused': focused && !disabled,
|
||||
'mm-text-field--error': error,
|
||||
'mm-text-field--disabled': disabled,
|
||||
'mm-text-field--truncate': truncate,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
display={DISPLAY.INLINE_FLEX}
|
||||
backgroundColor={BackgroundColor.backgroundDefault}
|
||||
alignItems={AlignItems.center}
|
||||
borderWidth={1}
|
||||
borderRadius={BorderRadius.SM}
|
||||
paddingLeft={startAccessory ? 4 : 0}
|
||||
paddingRight={endAccessory ? 4 : 0}
|
||||
onClick={handleClick}
|
||||
{...props}
|
||||
>
|
||||
{startAccessory}
|
||||
<InputComponent
|
||||
aria-invalid={error}
|
||||
as="input"
|
||||
autoComplete={autoComplete ? 'on' : 'off'}
|
||||
autoFocus={autoFocus}
|
||||
backgroundColor={BackgroundColor.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={startAccessory ? 2 : 4}
|
||||
paddingRight={endAccessory ? 2 : 4}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
ref={handleInputRef}
|
||||
required={required}
|
||||
value={value}
|
||||
variant={TextVariant.bodyMd}
|
||||
type={type}
|
||||
{...inputProps} // before className so input className isn't overridden
|
||||
className={classnames('mm-text-field__input', inputProps?.className)}
|
||||
/>
|
||||
{endAccessory}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
TextField.propTypes = {
|
||||
/**
|
||||
* The value af the TextField
|
||||
* Autocomplete allows the browser to predict the value based on earlier typed values
|
||||
*/
|
||||
value: TextFieldBase.propTypes.value,
|
||||
autoComplete: PropTypes.bool,
|
||||
/**
|
||||
* The onChange handler af the TextField
|
||||
* If `true`, the input will be focused during the first mount.
|
||||
*/
|
||||
onChange: TextFieldBase.propTypes.onChange,
|
||||
autoFocus: PropTypes.bool,
|
||||
/**
|
||||
* An additional className to apply to the text-field
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
/**
|
||||
* Show a clear button to clear the input
|
||||
* The default input value, useful when not controlling the component.
|
||||
*/
|
||||
showClearButton: PropTypes.bool,
|
||||
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
/**
|
||||
* The onClick handler for the clear button
|
||||
* If `true`, the input will be disabled.
|
||||
*/
|
||||
clearButtonOnClick: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
/**
|
||||
* The props to pass to the clear button
|
||||
* If `true`, the input will indicate an error
|
||||
*/
|
||||
clearButtonProps: PropTypes.shape(ButtonIcon.PropTypes),
|
||||
error: PropTypes.bool,
|
||||
/**
|
||||
* TextField accepts all the props from TextFieldBase and Box
|
||||
* The id of the `input` element.
|
||||
*/
|
||||
...TextFieldBase.propTypes,
|
||||
id: PropTypes.string,
|
||||
/**
|
||||
* The the component that is rendered as the input
|
||||
* Defaults to the Text component
|
||||
*/
|
||||
InputComponent: PropTypes.elementType,
|
||||
/**
|
||||
* Attributes applied to the `input` element.
|
||||
*/
|
||||
inputProps: PropTypes.object,
|
||||
/**
|
||||
* Component to appear on the left side of the input
|
||||
*/
|
||||
startAccessory: PropTypes.node,
|
||||
/**
|
||||
* Component to appear on the right side of the input
|
||||
*/
|
||||
endAccessory: 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 when the TextField is clicked on
|
||||
*/
|
||||
onClick: 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_SIZES)),
|
||||
/**
|
||||
* Type of the input element. Can be TEXT_FIELD_TYPES.TEXT, TEXT_FIELD_TYPES.PASSWORD, TEXT_FIELD_TYPES.NUMBER
|
||||
* Defaults to TEXT_FIELD_TYPES.TEXT ('text')
|
||||
*/
|
||||
type: PropTypes.oneOf(Object.values(TEXT_FIELD_TYPES)),
|
||||
/**
|
||||
* If true will ellipse the text of the input
|
||||
* Defaults to true
|
||||
*/
|
||||
truncate: PropTypes.bool,
|
||||
/**
|
||||
* The input value, required for a controlled component.
|
||||
*/
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
/**
|
||||
* TextField accepts all the props from Box
|
||||
*/
|
||||
...Box.propTypes,
|
||||
};
|
||||
|
||||
TextField.displayName = 'TextField';
|
||||
|
@ -1,5 +1,5 @@
|
||||
.mm-text-field-base {
|
||||
--text-field-base-height: var(--size, 40px);
|
||||
.mm-text-field {
|
||||
--text-field-height: var(--size, 40px);
|
||||
|
||||
&--size-sm {
|
||||
--size: 32px;
|
||||
@ -13,7 +13,7 @@
|
||||
--size: 48px;
|
||||
}
|
||||
|
||||
height: var(--text-field-base-height);
|
||||
height: var(--text-field-height);
|
||||
border-color: var(--color-border-default);
|
||||
|
||||
&--focused {
|
||||
@ -31,7 +31,7 @@
|
||||
}
|
||||
|
||||
// truncates text with ellipsis
|
||||
&--truncate .mm-text-field-base__input {
|
||||
&--truncate .mm-text-field__input {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
@ -1,11 +1,28 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useArgs } from '@storybook/client-api';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
Size,
|
||||
BorderRadius,
|
||||
DISPLAY,
|
||||
FLEX_DIRECTION,
|
||||
AlignItems,
|
||||
TextVariant,
|
||||
IconColor,
|
||||
BackgroundColor,
|
||||
TextColor,
|
||||
Size,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import Box from '../../ui/box/box';
|
||||
|
||||
import {
|
||||
AvatarAccount,
|
||||
AvatarToken,
|
||||
Button,
|
||||
ButtonIcon,
|
||||
ICON_NAMES,
|
||||
Icon,
|
||||
Text,
|
||||
} from '..';
|
||||
|
||||
import { TEXT_FIELD_SIZES, TEXT_FIELD_TYPES } from './text-field.constants';
|
||||
import { TextField } from './text-field';
|
||||
@ -40,110 +57,73 @@ export default {
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
value: {
|
||||
autoComplete: {
|
||||
control: 'boolean',
|
||||
},
|
||||
autoFocus: {
|
||||
control: 'boolean',
|
||||
},
|
||||
className: {
|
||||
control: 'text',
|
||||
},
|
||||
defaultValue: {
|
||||
control: 'text',
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
},
|
||||
error: {
|
||||
control: 'boolean',
|
||||
},
|
||||
id: {
|
||||
control: 'text',
|
||||
},
|
||||
inputProps: {
|
||||
control: 'object',
|
||||
},
|
||||
startAccessory: {
|
||||
control: 'text',
|
||||
},
|
||||
maxLength: {
|
||||
control: 'number',
|
||||
},
|
||||
name: {
|
||||
control: 'text',
|
||||
},
|
||||
onBlur: {
|
||||
action: 'onBlur',
|
||||
},
|
||||
onChange: {
|
||||
action: 'onChange',
|
||||
},
|
||||
showClearButton: {
|
||||
control: 'boolean',
|
||||
},
|
||||
clearButtonOnClick: {
|
||||
action: 'clearButtonOnClick',
|
||||
},
|
||||
clearButtonProps: {
|
||||
control: 'object',
|
||||
},
|
||||
autoComplete: {
|
||||
control: 'boolean',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
autoFocus: {
|
||||
control: 'boolean',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
className: {
|
||||
control: 'text',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
error: {
|
||||
control: 'boolean',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
id: {
|
||||
control: 'text',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
inputProps: {
|
||||
control: 'object',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
leftAccessory: {
|
||||
control: 'text',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
maxLength: {
|
||||
control: 'number',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
name: {
|
||||
control: 'text',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
onBlur: {
|
||||
action: 'onBlur',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
onClick: {
|
||||
action: 'onClick',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
onFocus: {
|
||||
action: 'onFocus',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
onKeyDown: {
|
||||
action: 'onKeyDown',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
onKeyUp: {
|
||||
action: 'onKeyUp',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
placeholder: {
|
||||
control: 'text',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
readOnly: {
|
||||
control: 'boolean',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
required: {
|
||||
control: 'boolean',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
rightAccessory: {
|
||||
endAccessory: {
|
||||
control: 'text',
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: Object.values(TEXT_FIELD_SIZES),
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
type: {
|
||||
control: 'select',
|
||||
options: Object.values(TEXT_FIELD_TYPES),
|
||||
table: { category: 'text field base props' },
|
||||
},
|
||||
truncate: {
|
||||
control: 'boolean',
|
||||
table: { category: 'text field base props' },
|
||||
value: {
|
||||
control: 'text',
|
||||
},
|
||||
marginTop: {
|
||||
options: marginSizeControlOptions,
|
||||
@ -167,18 +147,7 @@ export default {
|
||||
},
|
||||
},
|
||||
args: {
|
||||
showClearButton: false,
|
||||
placeholder: 'Placeholder...',
|
||||
autoFocus: false,
|
||||
disabled: false,
|
||||
error: false,
|
||||
id: '',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
size: Size.MD,
|
||||
type: 'text',
|
||||
truncate: false,
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
|
||||
@ -187,36 +156,342 @@ const Template = (args) => {
|
||||
const handleOnChange = (e) => {
|
||||
updateArgs({ value: e.target.value });
|
||||
};
|
||||
const handleOnClear = () => {
|
||||
updateArgs({ value: '' });
|
||||
};
|
||||
return (
|
||||
<TextField
|
||||
{...args}
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
clearButtonOnClick={handleOnClear}
|
||||
/>
|
||||
);
|
||||
return <TextField {...args} value={value} onChange={handleOnChange} />;
|
||||
};
|
||||
|
||||
export const DefaultStory = Template.bind({});
|
||||
DefaultStory.storyName = 'Default';
|
||||
|
||||
export const ShowClearButton = Template.bind({});
|
||||
export const SizeStory = (args) => {
|
||||
return (
|
||||
<Box
|
||||
display={DISPLAY.INLINE_FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
gap={4}
|
||||
>
|
||||
<TextField
|
||||
{...args}
|
||||
placeholder="Size.SM (height: 32px)"
|
||||
size={Size.SM}
|
||||
/>
|
||||
<TextField
|
||||
{...args}
|
||||
placeholder="Size.MD (height: 40px)"
|
||||
size={Size.MD}
|
||||
/>
|
||||
<TextField
|
||||
{...args}
|
||||
placeholder="Size.LG (height: 48px)"
|
||||
size={Size.LG}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
SizeStory.storyName = 'Size';
|
||||
|
||||
ShowClearButton.args = {
|
||||
placeholder: 'Enter text to show clear',
|
||||
showClearButton: true,
|
||||
export const Type = (args) => (
|
||||
<Box
|
||||
display={DISPLAY.INLINE_FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
gap={4}
|
||||
>
|
||||
<TextField {...args} placeholder="Default" />
|
||||
<TextField
|
||||
{...args}
|
||||
type={TEXT_FIELD_TYPES.PASSWORD}
|
||||
placeholder="Password"
|
||||
/>
|
||||
<TextField {...args} type={TEXT_FIELD_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 ClearButtonProps = Template.bind({});
|
||||
ClearButtonProps.args = {
|
||||
value: 'clear button props',
|
||||
size: Size.LG,
|
||||
showClearButton: true,
|
||||
clearButtonProps: {
|
||||
backgroundColor: BackgroundColor.backgroundAlternative,
|
||||
borderRadius: BorderRadius.XS,
|
||||
},
|
||||
export const StartAccessoryEndAccessory = (args) => {
|
||||
const [value, setValue] = useState({
|
||||
search: '',
|
||||
address: '',
|
||||
amount: 1,
|
||||
accountAddress: '0x514910771af9ca656af840dff83e8264ecf986ca',
|
||||
});
|
||||
const handleOnChange = (e) => {
|
||||
setValue({ ...value, [e.target.name]: e.target.value });
|
||||
};
|
||||
const handleTokenPrice = (tokenAmount, priceUSD) => {
|
||||
return tokenAmount * priceUSD;
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
display={DISPLAY.INLINE_FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
gap={4}
|
||||
>
|
||||
<TextField
|
||||
{...args}
|
||||
placeholder="Search"
|
||||
value={value.search}
|
||||
name="search"
|
||||
onChange={handleOnChange}
|
||||
startAccessory={
|
||||
<Icon color={IconColor.iconAlternative} name={ICON_NAMES.SEARCH} />
|
||||
}
|
||||
/>
|
||||
<TextField
|
||||
{...args}
|
||||
placeholder="Public address (0x), or ENS"
|
||||
value={value.address}
|
||||
name="address"
|
||||
onChange={handleOnChange}
|
||||
endAccessory={
|
||||
<ButtonIcon
|
||||
iconName={ICON_NAMES.SCAN_BARCODE}
|
||||
ariaLabel="Scan QR code"
|
||||
iconProps={{ color: IconColor.primaryDefault }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<TextField
|
||||
{...args}
|
||||
placeholder="Enter amount"
|
||||
value={value.amount}
|
||||
name="amount"
|
||||
onChange={handleOnChange}
|
||||
type="number"
|
||||
truncate
|
||||
startAccessory={
|
||||
<Box
|
||||
as="button"
|
||||
style={{ padding: 0 }}
|
||||
backgroundColor={BackgroundColor.transparent}
|
||||
gap={1}
|
||||
display={DISPLAY.FLEX}
|
||||
alignItems={AlignItems.center}
|
||||
>
|
||||
<AvatarToken
|
||||
name="eth"
|
||||
src="./images/eth_logo.svg"
|
||||
size={Size.SM}
|
||||
/>
|
||||
<Text>ETH</Text>
|
||||
<Icon
|
||||
name={ICON_NAMES.ARROW_DOWN}
|
||||
color={IconColor.iconDefault}
|
||||
size={Size.SM}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
endAccessory={
|
||||
<Text
|
||||
variant={TextVariant.bodySm}
|
||||
color={TextColor.textAlternative}
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
= ${handleTokenPrice(value.amount, 1173.58)}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<TextField
|
||||
{...args}
|
||||
placeholder="Public address (0x), or ENS"
|
||||
value={value.accountAddress}
|
||||
name="accountAddress"
|
||||
onChange={handleOnChange}
|
||||
truncate
|
||||
startAccessory={
|
||||
value.accountAddress && (
|
||||
<AvatarAccount size={Size.SM} address={value.accountAddress} />
|
||||
)
|
||||
}
|
||||
endAccessory={
|
||||
value.accountAddress.length === 42 && (
|
||||
<Icon name={ICON_NAMES.CHECK} color={IconColor.successDefault} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</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 (
|
||||
<Box display={DISPLAY.FLEX}>
|
||||
<TextField
|
||||
{...args}
|
||||
inputRef={inputRef}
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
<Button marginLeft={1} onClick={handleOnClick}>
|
||||
Edit
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomInputComponent = React.forwardRef(
|
||||
(
|
||||
{
|
||||
as,
|
||||
autoComplete,
|
||||
autoFocus,
|
||||
defaultValue,
|
||||
disabled,
|
||||
focused,
|
||||
id,
|
||||
inputProps,
|
||||
inputRef,
|
||||
maxLength,
|
||||
name,
|
||||
onBlur,
|
||||
onChange,
|
||||
onFocus,
|
||||
padding,
|
||||
paddingLeft,
|
||||
paddingRight,
|
||||
placeholder,
|
||||
readOnly,
|
||||
required,
|
||||
value,
|
||||
variant,
|
||||
type,
|
||||
className,
|
||||
'aria-invalid': ariaInvalid,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => (
|
||||
<Box
|
||||
display={DISPLAY.FLEX}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
ref={ref}
|
||||
{...{ padding, paddingLeft, paddingRight, ...props }}
|
||||
>
|
||||
<Box display={DISPLAY.INLINE_FLEX}>
|
||||
<Text
|
||||
style={{ padding: 0 }}
|
||||
aria-invalid={ariaInvalid}
|
||||
ref={inputRef}
|
||||
{...{
|
||||
className,
|
||||
as,
|
||||
autoComplete,
|
||||
autoFocus,
|
||||
defaultValue,
|
||||
disabled,
|
||||
focused,
|
||||
id,
|
||||
maxLength,
|
||||
name,
|
||||
onBlur,
|
||||
onChange,
|
||||
onFocus,
|
||||
placeholder,
|
||||
readOnly,
|
||||
required,
|
||||
value,
|
||||
variant,
|
||||
type,
|
||||
...inputProps,
|
||||
}}
|
||||
/>
|
||||
<Text variant={TextVariant.bodyXs} color={TextColor.textAlternative}>
|
||||
GoerliETH
|
||||
</Text>
|
||||
</Box>
|
||||
<Text variant={TextVariant.bodyXs}>No conversion rate available</Text>
|
||||
</Box>
|
||||
),
|
||||
);
|
||||
|
||||
CustomInputComponent.propTypes = {
|
||||
/**
|
||||
* The custom input component should accepts all props that the
|
||||
* InputComponent accepts in ./text-field.js
|
||||
*/
|
||||
autoFocus: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
disabled: PropTypes.bool,
|
||||
id: PropTypes.string,
|
||||
inputProps: PropTypes.object,
|
||||
inputRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
|
||||
maxLength: PropTypes.number,
|
||||
name: PropTypes.string,
|
||||
onBlur: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
readOnly: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
type: PropTypes.oneOf(Object.values(TEXT_FIELD_TYPES)),
|
||||
/**
|
||||
* Because we manipulate the type in TextField so the html element
|
||||
* receives the correct attribute we need to change the autoComplete
|
||||
* propType to a string
|
||||
*/
|
||||
autoComplete: PropTypes.string,
|
||||
/**
|
||||
* The custom input component should also accept all the props from Box
|
||||
*/
|
||||
...Box.propTypes,
|
||||
};
|
||||
|
||||
CustomInputComponent.displayName = 'CustomInputComponent';
|
||||
|
||||
export const InputComponent = (args) => (
|
||||
<TextField
|
||||
{...args}
|
||||
placeholder="0"
|
||||
type="number"
|
||||
size={Size.LG}
|
||||
InputComponent={CustomInputComponent}
|
||||
startAccessory={
|
||||
<Icon color={IconColor.iconAlternative} name={ICON_NAMES.WALLET} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
InputComponent.args = { autoComplete: true };
|
||||
|
||||
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' };
|
||||
|
@ -1,11 +1,11 @@
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { renderWithUserEvent } from '../../../../test/lib/render-helpers';
|
||||
|
||||
import {
|
||||
renderControlledInput,
|
||||
renderWithUserEvent,
|
||||
} from '../../../../test/lib/render-helpers';
|
||||
import { Size } from '../../../helpers/constants/design-system';
|
||||
|
||||
import Box from '../../ui/box';
|
||||
|
||||
import { TextField } from './text-field';
|
||||
|
||||
@ -15,103 +15,234 @@ describe('TextField', () => {
|
||||
expect(getByRole('textbox')).toBeDefined();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
it('should render and be able to input text', async () => {
|
||||
const { user, getByRole } = renderWithUserEvent(<TextField />);
|
||||
const textField = getByRole('textbox');
|
||||
await user.type(textField, 'text value');
|
||||
expect(textField).toHaveValue('text value');
|
||||
it('should render and be able to input text', () => {
|
||||
const { getByTestId } = render(
|
||||
<TextField inputProps={{ 'data-testid': 'text-field' }} />,
|
||||
);
|
||||
const textField = getByTestId('text-field');
|
||||
|
||||
expect(textField.value).toBe(''); // initial value is empty string
|
||||
fireEvent.change(textField, { target: { value: 'text value' } });
|
||||
expect(textField.value).toBe('text value');
|
||||
fireEvent.change(textField, { target: { value: '' } }); // reset value
|
||||
expect(textField.value).toBe(''); // value is empty string after reset
|
||||
});
|
||||
it('should render and fire onFocus and onBlur events', () => {
|
||||
it('should render with focused state when clicked', async () => {
|
||||
const { getByTestId, user } = renderWithUserEvent(
|
||||
<TextField
|
||||
data-testid="text-field"
|
||||
inputProps={{ 'data-testid': 'input' }}
|
||||
/>,
|
||||
);
|
||||
const textField = getByTestId('input');
|
||||
|
||||
await user.click(textField);
|
||||
expect(getByTestId('input')).toHaveFocus();
|
||||
expect(getByTestId('text-field')).toHaveClass('mm-text-field--focused ');
|
||||
});
|
||||
it('should render and fire onFocus and onBlur events', async () => {
|
||||
const onFocus = jest.fn();
|
||||
const onBlur = jest.fn();
|
||||
const { getByTestId } = render(
|
||||
const { getByTestId, user } = renderWithUserEvent(
|
||||
<TextField
|
||||
inputProps={{ 'data-testid': 'text-field' }}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>,
|
||||
);
|
||||
const textField = getByTestId('text-field');
|
||||
|
||||
fireEvent.focus(textField);
|
||||
const textField = getByTestId('text-field');
|
||||
await user.click(textField);
|
||||
expect(onFocus).toHaveBeenCalledTimes(1);
|
||||
fireEvent.blur(textField);
|
||||
expect(onBlur).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should render and fire onChange event', async () => {
|
||||
const onChange = jest.fn();
|
||||
const { user, getByRole } = renderWithUserEvent(
|
||||
const { getByTestId, user } = renderWithUserEvent(
|
||||
<TextField
|
||||
inputProps={{ 'data-testid': 'text-field' }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
const textField = getByRole('textbox');
|
||||
|
||||
const textField = getByTestId('text-field');
|
||||
await user.type(textField, '123');
|
||||
expect(textField).toHaveValue('123');
|
||||
expect(onChange).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
it('should render and fire onClick event', async () => {
|
||||
const onClick = jest.fn();
|
||||
const { user, getByTestId } = renderWithUserEvent(
|
||||
<TextField data-testid="text-field" onClick={onClick} />,
|
||||
const { getByTestId, user } = renderWithUserEvent(
|
||||
<TextField
|
||||
inputProps={{ 'data-testid': 'text-field' }}
|
||||
onClick={onClick}
|
||||
/>,
|
||||
);
|
||||
await user.click(getByTestId('text-field'));
|
||||
const textField = getByTestId('text-field');
|
||||
|
||||
await user.click(textField);
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should render with the rightAccessory', () => {
|
||||
const { getByText } = render(
|
||||
<TextField rightAccessory={<div>right-accessory</div>} />,
|
||||
);
|
||||
expect(getByText('right-accessory')).toBeDefined();
|
||||
});
|
||||
it('should render showClearButton button when showClearButton is true and value exists', async () => {
|
||||
// As showClearButton is intended to be used with a controlled input we need to use renderControlledInput
|
||||
const { user, getByRole } = renderControlledInput(TextField, {
|
||||
showClearButton: true,
|
||||
});
|
||||
await user.type(getByRole('textbox'), 'test value');
|
||||
expect(getByRole('textbox')).toHaveValue('test value');
|
||||
expect(getByRole('button', { name: /Clear/u })).toBeDefined();
|
||||
});
|
||||
it('should still render with the rightAccessory when showClearButton is true', async () => {
|
||||
// As showClearButton is intended to be used with a controlled input we need to use renderControlledInput
|
||||
const { user, getByRole, getByText } = renderControlledInput(TextField, {
|
||||
showClearButton: true,
|
||||
rightAccessory: <div>right-accessory</div>,
|
||||
});
|
||||
await user.type(getByRole('textbox'), 'test value');
|
||||
expect(getByRole('textbox')).toHaveValue('test value');
|
||||
expect(getByRole('button', { name: /Clear/u })).toBeDefined();
|
||||
expect(getByText('right-accessory')).toBeDefined();
|
||||
});
|
||||
it('should fire onClick event when passed to clearButtonOnClick when clear button is clicked', async () => {
|
||||
// As showClearButton is intended to be used with a controlled input we need to use renderControlledInput
|
||||
const fn = jest.fn();
|
||||
const { user, getByRole } = renderControlledInput(TextField, {
|
||||
showClearButton: true,
|
||||
clearButtonOnClick: fn,
|
||||
});
|
||||
await user.type(getByRole('textbox'), 'test value');
|
||||
await user.click(getByRole('button', { name: /Clear/u }));
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should fire onClick event when passed to clearButtonProps.onClick prop', async () => {
|
||||
// As showClearButton is intended to be used with a controlled input we need to use renderControlledInput
|
||||
const fn = jest.fn();
|
||||
const { user, getByRole } = renderControlledInput(TextField, {
|
||||
showClearButton: true,
|
||||
clearButtonProps: { onClick: fn },
|
||||
});
|
||||
await user.type(getByRole('textbox'), 'test value');
|
||||
await user.click(getByRole('button', { name: /Clear/u }));
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should be able to accept inputProps', () => {
|
||||
it('should render with different size classes', () => {
|
||||
const { getByTestId } = render(
|
||||
<TextField inputProps={{ 'data-testid': 'text-field' }} />,
|
||||
<>
|
||||
<TextField size={Size.SM} data-testid="sm" />
|
||||
<TextField size={Size.MD} data-testid="md" />
|
||||
<TextField size={Size.LG} data-testid="lg" />
|
||||
</>,
|
||||
);
|
||||
expect(getByTestId('text-field')).toBeDefined();
|
||||
expect(getByTestId('sm')).toHaveClass('mm-text-field--size-sm');
|
||||
expect(getByTestId('md')).toHaveClass('mm-text-field--size-md');
|
||||
expect(getByTestId('lg')).toHaveClass('mm-text-field--size-lg');
|
||||
});
|
||||
it('should render with different types', () => {
|
||||
const { getByTestId } = render(
|
||||
<>
|
||||
<TextField inputProps={{ 'data-testid': 'text-field-text' }} />
|
||||
<TextField
|
||||
type="number"
|
||||
inputProps={{ 'data-testid': 'text-field-number' }}
|
||||
/>
|
||||
<TextField
|
||||
type="password"
|
||||
inputProps={{ 'data-testid': 'text-field-password' }}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
expect(getByTestId('text-field-text')).toHaveAttribute('type', 'text');
|
||||
expect(getByTestId('text-field-number')).toHaveAttribute('type', 'number');
|
||||
expect(getByTestId('text-field-password')).toHaveAttribute(
|
||||
'type',
|
||||
'password',
|
||||
);
|
||||
});
|
||||
it('should render with truncate class as true by default and remove it when truncate is false', () => {
|
||||
const { getByTestId } = render(
|
||||
<>
|
||||
<TextField data-testid="truncate" />
|
||||
<TextField truncate={false} data-testid="no-truncate" />
|
||||
</>,
|
||||
);
|
||||
expect(getByTestId('truncate')).toHaveClass('mm-text-field--truncate');
|
||||
expect(getByTestId('no-truncate')).not.toHaveClass(
|
||||
'mm-text-field--truncate',
|
||||
);
|
||||
});
|
||||
it('should render with right and left accessories', () => {
|
||||
const { getByRole, getByText } = render(
|
||||
<TextField
|
||||
startAccessory={<div>start accessory</div>}
|
||||
endAccessory={<div>end accessory</div>}
|
||||
/>,
|
||||
);
|
||||
expect(getByRole('textbox')).toBeDefined();
|
||||
expect(getByText('start accessory')).toBeDefined();
|
||||
expect(getByText('end 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(<TextField inputRef={mockRef} />);
|
||||
expect(getByRole('textbox')).toBeDefined();
|
||||
expect(mockRef).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should render with autoComplete', () => {
|
||||
const { getByTestId } = render(
|
||||
<TextField
|
||||
autoComplete
|
||||
inputProps={{ 'data-testid': 'text-field-auto-complete' }}
|
||||
/>,
|
||||
);
|
||||
expect(getByTestId('text-field-auto-complete')).toHaveAttribute(
|
||||
'autocomplete',
|
||||
'on',
|
||||
);
|
||||
});
|
||||
it('should render with autoFocus', () => {
|
||||
const { getByRole } = render(<TextField autoFocus />);
|
||||
expect(getByRole('textbox')).toHaveFocus();
|
||||
});
|
||||
it('should render with a defaultValue', () => {
|
||||
const { getByRole } = render(
|
||||
<TextField
|
||||
defaultValue="default value"
|
||||
inputProps={{ 'data-testid': 'text-field-default-value' }}
|
||||
/>,
|
||||
);
|
||||
expect(getByRole('textbox').value).toBe('default value');
|
||||
});
|
||||
it('should render in disabled state and not focus or be clickable', async () => {
|
||||
const mockOnClick = jest.fn();
|
||||
const mockOnFocus = jest.fn();
|
||||
const { getByRole, getByTestId, user } = renderWithUserEvent(
|
||||
<TextField
|
||||
disabled
|
||||
onFocus={mockOnFocus}
|
||||
onClick={mockOnClick}
|
||||
data-testid="text-field"
|
||||
/>,
|
||||
);
|
||||
|
||||
const textField = getByTestId('text-field');
|
||||
|
||||
await user.click(textField);
|
||||
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(
|
||||
<TextField error data-testid="text-field-error" />,
|
||||
);
|
||||
expect(getByTestId('text-field-error')).toHaveClass('mm-text-field--error');
|
||||
});
|
||||
it('should render with maxLength and not allow more than the set characters', async () => {
|
||||
const { getByRole, user } = renderWithUserEvent(
|
||||
<TextField maxLength={5} />,
|
||||
);
|
||||
const textField = getByRole('textbox');
|
||||
await user.type(textField, '1234567890');
|
||||
expect(getByRole('textbox')).toBeDefined();
|
||||
expect(textField.maxLength).toBe(5);
|
||||
expect(textField.value).toBe('12345');
|
||||
expect(textField.value).toHaveLength(5);
|
||||
});
|
||||
it('should render with readOnly attr when readOnly is true', async () => {
|
||||
const { getByTestId, getByRole, user } = renderWithUserEvent(
|
||||
<TextField readOnly data-testid="read-only" />,
|
||||
);
|
||||
const textField = getByTestId('read-only');
|
||||
await user.type(textField, '1234567890');
|
||||
expect(getByRole('textbox').value).toBe('');
|
||||
expect(getByRole('textbox')).toHaveAttribute('readonly', '');
|
||||
});
|
||||
it('should render with required attr when required is true', () => {
|
||||
const { getByTestId } = render(
|
||||
<TextField
|
||||
required
|
||||
inputProps={{ 'data-testid': 'text-field-required' }}
|
||||
/>,
|
||||
);
|
||||
expect(getByTestId('text-field-required')).toHaveAttribute('required', '');
|
||||
});
|
||||
it('should render with a custom input and still work', async () => {
|
||||
const CustomInputComponent = React.forwardRef((props, ref) => (
|
||||
<Box ref={ref} as="input" {...props} />
|
||||
));
|
||||
CustomInputComponent.displayName = 'CustomInputComponent'; // fixes eslint error
|
||||
const { getByTestId, user } = renderWithUserEvent(
|
||||
<TextField
|
||||
InputComponent={CustomInputComponent}
|
||||
inputProps={{ 'data-testid': 'text-field', className: 'test' }}
|
||||
/>,
|
||||
);
|
||||
const textField = getByTestId('text-field');
|
||||
|
||||
expect(textField.value).toBe(''); // initial value is empty string
|
||||
await user.type(textField, 'text value');
|
||||
expect(textField.value).toBe('text value');
|
||||
fireEvent.change(textField, { target: { value: '' } }); // reset value
|
||||
expect(textField.value).toBe(''); // value is empty string after reset
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user