diff --git a/test/env.js b/test/env.js
index 0fb3e9378..b932b2a42 100644
--- a/test/env.js
+++ b/test/env.js
@@ -1,9 +1,10 @@
process.env.METAMASK_ENV = 'test';
/**
- * Used for testing components that use the Icon component
+ * Used for testing components that use the Icon component
* 'ui/components/component-library/icon/icon.js'
*/
process.env.ICON_NAMES = {
LOADING_FILLED: 'loading-filled',
+ CLOSE_OUTLINE: 'close-outline',
};
diff --git a/ui/components/component-library/component-library-components.scss b/ui/components/component-library/component-library-components.scss
index a4d49cc93..af5003d1e 100644
--- a/ui/components/component-library/component-library-components.scss
+++ b/ui/components/component-library/component-library-components.scss
@@ -12,4 +12,5 @@
@import 'icon/icon';
@import 'tag/tag';
@import 'text/text';
+@import 'text-field/text-field';
@import 'text-field-base/text-field-base';
diff --git a/ui/components/component-library/text-field/README.mdx b/ui/components/component-library/text-field/README.mdx
new file mode 100644
index 000000000..7a9bc8cd6
--- /dev/null
+++ b/ui/components/component-library/text-field/README.mdx
@@ -0,0 +1,73 @@
+import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
+
+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.
+
+
+
+## Props
+
+The `TextField` accepts all props below as well as all [Box](/docs/ui-components-ui-box-box-stories-js--default-story#props) and [TextFieldBase](/docs/ui-components-component-library-text-field-base-text-field-base-stories-js--default-story#props) component props
+
+
+
+### Show Clear
+
+Use the `showClear` prop to display a clear button when `TextField` has a value. Clicking the button will clear the value.
+You can also attach an `onClear` handler to the `TextField` to perform additional actions when the clear button is clicked.
+
+
+
+```jsx
+import { TextField } from '../../ui/component-library/text-field';
+
+;
+```
+
+### On Clear
+
+Use the `onClear` prop to perform additional actions when the clear button is clicked.
+
+
+
+```jsx
+import { TextField } from '../../ui/component-library/text-field';
+
+ console.log('cleared input')} />;
+```
+
+### Clear Button Props and Clear Button Icon Props
+
+Use the `clearButtonProps` and `clearButtonIconProps` props to pass props to the clear button and clear button icon respectively.
+
+
+
+```jsx
+import {
+ SIZES,
+ COLORS,
+ BORDER_RADIUS,
+} from '../../../helpers/constants/design-system';
+import { TextField } from '../../ui/component-library/text-field';
+
+;
+```
diff --git a/ui/components/component-library/text-field/index.js b/ui/components/component-library/text-field/index.js
new file mode 100644
index 000000000..a680c752d
--- /dev/null
+++ b/ui/components/component-library/text-field/index.js
@@ -0,0 +1,2 @@
+export { TextField } from './text-field';
+export { TEXT_FIELD_SIZES, TEXT_FIELD_TYPES } from './text-field.constants';
diff --git a/ui/components/component-library/text-field/text-field.constants.js b/ui/components/component-library/text-field/text-field.constants.js
new file mode 100644
index 000000000..8047344e5
--- /dev/null
+++ b/ui/components/component-library/text-field/text-field.constants.js
@@ -0,0 +1,7 @@
+import {
+ TEXT_FIELD_BASE_SIZES,
+ TEXT_FIELD_BASE_TYPES,
+} from '../text-field-base/text-field-base.constants';
+
+export const TEXT_FIELD_SIZES = TEXT_FIELD_BASE_SIZES;
+export const TEXT_FIELD_TYPES = TEXT_FIELD_BASE_TYPES;
diff --git a/ui/components/component-library/text-field/text-field.js b/ui/components/component-library/text-field/text-field.js
new file mode 100644
index 000000000..ddf351776
--- /dev/null
+++ b/ui/components/component-library/text-field/text-field.js
@@ -0,0 +1,108 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+
+import {
+ SIZES,
+ DISPLAY,
+ JUSTIFY_CONTENT,
+ ALIGN_ITEMS,
+ COLORS,
+} from '../../../helpers/constants/design-system';
+
+import Box from '../../ui/box';
+
+import { Icon, ICON_NAMES } from '../icon';
+
+import { TextFieldBase } from '../text-field-base';
+
+export const TextField = ({
+ className,
+ showClear,
+ clearButtonIconProps,
+ clearButtonProps,
+ rightAccessory,
+ value: valueProp,
+ onChange,
+ onClear,
+ inputProps,
+ ...props
+}) => {
+ const [value, setValue] = useState(valueProp || '');
+ const handleOnChange = (e) => {
+ setValue(e.target.value);
+ onChange?.(e);
+ };
+ const handleClear = (e) => {
+ setValue('');
+ clearButtonProps?.onClick?.(e);
+ onClear?.(e);
+ };
+ return (
+
+ {/* replace with ButtonIcon */}
+
+
+
+ {rightAccessory}
+ >
+ ) : (
+ rightAccessory
+ )
+ }
+ inputProps={{
+ marginRight: showClear ? 6 : 0,
+ ...inputProps,
+ }}
+ {...props}
+ />
+ );
+};
+
+TextField.propTypes = {
+ /**
+ * An additional className to apply to the text-field
+ */
+ className: PropTypes.string,
+ /**
+ * Show a clear button to clear the input
+ */
+ showClear: PropTypes.bool,
+ /**
+ * The event handler for when the clear button is clicked
+ */
+ onClear: PropTypes.func,
+ /**
+ * The props to pass to the clear button
+ */
+ clearButtonProps: PropTypes.shape(Box.PropTypes),
+ /**
+ * The props to pass to the icon inside of the close button
+ */
+ clearButtonIconProps: PropTypes.shape(Icon.PropTypes),
+ /**
+ * TextField accepts all the props from TextFieldBase and Box
+ */
+ ...TextFieldBase.propTypes,
+};
diff --git a/ui/components/component-library/text-field/text-field.scss b/ui/components/component-library/text-field/text-field.scss
new file mode 100644
index 000000000..b21024d81
--- /dev/null
+++ b/ui/components/component-library/text-field/text-field.scss
@@ -0,0 +1,10 @@
+.mm-text-field {
+ // TOD: remove most of these styles when replaced by ButtonIcon
+ &__button-clear {
+ height: 24px;
+ width: 24px;
+ max-width: 24px;
+ flex: 0 0 24px;
+ margin-left: -24px;
+ }
+}
diff --git a/ui/components/component-library/text-field/text-field.stories.js b/ui/components/component-library/text-field/text-field.stories.js
new file mode 100644
index 000000000..70832ed2b
--- /dev/null
+++ b/ui/components/component-library/text-field/text-field.stories.js
@@ -0,0 +1,247 @@
+import React, { useState } from 'react';
+
+import {
+ SIZES,
+ COLORS,
+ BORDER_RADIUS,
+} from '../../../helpers/constants/design-system';
+
+import { Text } from '../text';
+
+import { TEXT_FIELD_SIZES, TEXT_FIELD_TYPES } from './text-field.constants';
+import { TextField } from './text-field';
+
+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/TextField',
+ id: __filename,
+ component: TextField,
+ parameters: {
+ docs: {
+ page: README,
+ },
+ },
+ argTypes: {
+ showClear: {
+ control: 'boolean',
+ },
+ value: {
+ control: 'text',
+ },
+ onChange: {
+ action: 'onChange',
+ table: { category: 'text field base props' },
+ },
+ onClear: {
+ action: 'onClear',
+ },
+ clearButtonIconProps: {
+ control: 'object',
+ },
+ 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: {
+ 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' },
+ },
+ 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: {
+ showClear: false,
+ placeholder: 'Placeholder...',
+ autoFocus: false,
+ disabled: false,
+ error: false,
+ id: '',
+ readOnly: false,
+ required: false,
+ size: SIZES.MD,
+ type: 'text',
+ truncate: false,
+ },
+};
+
+const Template = (args) => ;
+
+export const DefaultStory = Template.bind({});
+DefaultStory.storyName = 'Default';
+
+export const ShowClear = (args) => {
+ const [value, setValue] = useState('show clear');
+ const handleOnChange = (e) => {
+ setValue(e.target.value);
+ };
+ return (
+
+ );
+};
+
+export const OnClear = (args) => {
+ const [value, setValue] = useState('onClear example');
+ const [showOnClearMessage, setShowOnClearMessage] = useState(false);
+ const handleOnChange = (e) => {
+ setValue(e.target.value);
+ showOnClearMessage && setShowOnClearMessage(false);
+ };
+ const handleOnClear = () => {
+ setShowOnClearMessage(true);
+ };
+ return (
+ <>
+
+ {showOnClearMessage && onClear called}
+ >
+ );
+};
+
+export const ClearButtonPropsClearButtonIconProps = Template.bind({});
+ClearButtonPropsClearButtonIconProps.args = {
+ value: 'clear button props',
+ size: SIZES.LG,
+ showClear: true,
+ clearButtonProps: {
+ backgroundColor: COLORS.BACKGROUND_ALTERNATIVE,
+ borderRadius: BORDER_RADIUS.XS,
+ },
+ clearButtonIconProps: {
+ size: SIZES.MD,
+ },
+};
diff --git a/ui/components/component-library/text-field/text-field.test.js b/ui/components/component-library/text-field/text-field.test.js
new file mode 100644
index 000000000..f2d7819be
--- /dev/null
+++ b/ui/components/component-library/text-field/text-field.test.js
@@ -0,0 +1,161 @@
+/* eslint-disable jest/require-top-level-describe */
+import React from 'react';
+import { fireEvent, render } from '@testing-library/react';
+
+import { TextField } from './text-field';
+
+describe('TextField', () => {
+ it('should render correctly', () => {
+ const { getByRole } = render();
+ expect(getByRole('textbox')).toBeDefined();
+ });
+ it('should render and be able to input text', () => {
+ const { getByTestId } = render(
+ ,
+ );
+ 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', () => {
+ const onFocus = jest.fn();
+ const onBlur = jest.fn();
+ const { getByTestId } = render(
+ ,
+ );
+ const textField = getByTestId('text-field');
+
+ fireEvent.focus(textField);
+ expect(onFocus).toHaveBeenCalledTimes(1);
+ fireEvent.blur(textField);
+ expect(onBlur).toHaveBeenCalledTimes(1);
+ });
+ it('should render and fire onChange event', () => {
+ const onChange = jest.fn();
+ const { getByTestId } = render(
+ ,
+ );
+ const textField = getByTestId('text-field');
+
+ fireEvent.change(textField, { target: { value: 'text value' } });
+ expect(onChange).toHaveBeenCalledTimes(1);
+ });
+ it('should render and fire onClick event', () => {
+ const onClick = jest.fn();
+ const { getByTestId } = render(
+ ,
+ );
+ const textField = getByTestId('text-field');
+
+ fireEvent.click(textField);
+ expect(onClick).toHaveBeenCalledTimes(1);
+ });
+ it('should render showClear button when showClear is true and value exists', () => {
+ const { getByRole, getByTestId } = render(
+ ,
+ );
+ const textField = getByRole('textbox');
+ expect(textField.value).toBe(''); // initial value is empty string
+ fireEvent.change(textField, { target: { value: 'text value' } });
+ expect(textField.value).toBe('text value');
+ expect(getByTestId('clear-button')).toBeDefined();
+ expect(getByTestId('clear-button-icon')).toBeDefined();
+ });
+ it('should render with the rightAccessory', () => {
+ const { getByText } = render(
+ right-accessory} />,
+ );
+ expect(getByText('right-accessory')).toBeDefined();
+ });
+ it('should still render with the rightAccessory when showClear is true', () => {
+ const { getByRole, getByTestId, getByText } = render(
+ right-accessory}
+ showClear
+ />,
+ );
+ const textField = getByRole('textbox');
+ expect(textField.value).toBe(''); // initial value is empty string
+ fireEvent.change(textField, { target: { value: 'text value' } });
+ expect(textField.value).toBe('text value');
+ expect(getByTestId('clear-button')).toBeDefined();
+ expect(getByTestId('clear-button-icon')).toBeDefined();
+ expect(getByText('right-accessory')).toBeDefined();
+ });
+ it('should clear text when clear button is clicked', () => {
+ const { getByRole, getByTestId } = render(
+ right-accessory}
+ showClear
+ />,
+ );
+ const textField = getByRole('textbox');
+ fireEvent.change(textField, { target: { value: 'text value' } });
+ expect(textField.value).toBe('text value');
+ fireEvent.click(getByTestId('clear-button'));
+ expect(textField.value).toBe('');
+ });
+ it('should fire onClear event when passed to onClear prop', () => {
+ const onClear = jest.fn();
+ const { getByRole, getByTestId } = render(
+ ,
+ );
+ const textField = getByRole('textbox');
+ fireEvent.change(textField, { target: { value: 'text value' } });
+ expect(textField.value).toBe('text value');
+ fireEvent.click(getByTestId('clear-button'));
+ expect(onClear).toHaveBeenCalledTimes(1);
+ });
+ it('should fire clearButtonProps.onClick event when passed to clearButtonProps.onClick prop', () => {
+ const onClear = jest.fn();
+ const onClick = jest.fn();
+ const { getByRole, getByTestId } = render(
+ ,
+ );
+ const textField = getByRole('textbox');
+ fireEvent.change(textField, { target: { value: 'text value' } });
+ expect(textField.value).toBe('text value');
+ fireEvent.click(getByTestId('clear-button'));
+ expect(onClear).toHaveBeenCalledTimes(1);
+ expect(onClick).toHaveBeenCalledTimes(1);
+ });
+ it('should be able to accept inputProps', () => {
+ const { getByRole } = render(
+ ,
+ );
+ const textField = getByRole('textbox');
+ expect(textField).toBeDefined();
+ });
+});