1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +01:00

TextFieldBase house keeping 🧹 (#16667)

* TextFieldBase house keeping updates

* Fixing story

* Updating custom input story

* Updating ButtonIcon props and lint issues

* Updating snapshots
This commit is contained in:
George Marshall 2022-12-06 11:51:48 -08:00 committed by GitHub
parent 1fa213835f
commit 971f153e65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 197 additions and 154 deletions

View File

@ -179,7 +179,7 @@ export const DefaultStory = (args) => {
backgroundColor={COLORS.BACKGROUND_ALTERNATIVE}
rightAccessory={
<ButtonIcon
icon={ICON_NAMES.COPY_FILLED}
iconName={ICON_NAMES.COPY_FILLED}
size={SIZES.SM}
color={COLORS.ICON_ALTERNATIVE}
ariaLabel="Copy to clipboard"

View File

@ -35,8 +35,8 @@ Defaults to `md`
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/components/component-library';
import { SIZES } from '../../../helpers/constants/design-system';
import { TextFieldBase } from '../../component-library';
<TextFieldBase size={SIZES.SM} />
<TextFieldBase size={SIZES.MD} />
@ -60,7 +60,7 @@ Defaults to `text`.
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/components/component-library';
import { TextFieldBase } from '../../component-library';
<TextFieldBase type="text" /> // (Default)
<TextFieldBase type="number" />
@ -76,7 +76,7 @@ Use the `truncate` prop to truncate the text of the the `TextFieldBase`. Default
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/components/component-library';
import { TextFieldBase } from '../../component-library';
<TextFieldBase truncate />; // truncate is set to `true` by default
<TextFieldBase truncate={false} />;
@ -92,9 +92,7 @@ Use the `leftAccessory` and `rightAccessory` props to add components such as ico
```jsx
import { COLORS, SIZES, DISPLAY } from '../../../helpers/constants/design-system';
import { Icon, ICON_NAMES } from '../../ui/components/component-library';
import { TextFieldBase } from '../../ui/components/component-library';
import { ButtonIcon, Icon, ICON_NAMES, TextFieldBase } from '../../component-library';
<TextFieldBase
placeholder="Search"
@ -109,18 +107,11 @@ import { TextFieldBase } from '../../ui/components/component-library';
<TextFieldBase
placeholder="Public address (0x), or ENS"
rightAccessory={
// TODO: replace with ButtonIcon
<Box
as="button"
display={DISPLAY.FLEX}
style={{ padding: 0 }}
backgroundColor={COLORS.TRANSPARENT}
>
<Icon
color={COLORS.PRIMARY_DEFAULT}
name={ICON_NAMES.SCAN_BARCODE_FILLED}
/>
</Box>
<ButtonIcon
iconName={ICON_NAMES.SCAN_BARCODE_FILLED}
ariaLabel="Scan QR code"
iconProps={{ color: COLORS.PRIMARY_DEFAULT }}
/>
}
/>
@ -156,7 +147,7 @@ Use the `inputRef` prop to access the ref of the `<input />` html element of `Te
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/components/component-library';
import { Button, TextFieldBase } from '../../component-library';
const inputRef = useRef(null);
const [value, setValue] = useState('');
@ -172,20 +163,9 @@ const handleOnChange = (e) => {
value={value}
onChange={handleOnChange}
/>
// TODO: replace with Button component
<Box
as="button"
backgroundColor={COLORS.BACKGROUND_ALTERNATIVE}
color={COLORS.TEXT_DEFAULT}
borderColor={COLORS.BORDER_DEFAULT}
borderRadius={SIZES.XL}
marginLeft={1}
paddingLeft={2}
paddingRight={2}
onClick={handleOnClick}
>
<Button marginLeft={1} onClick={handleOnClick}>
Edit
</Box>
</Button>
```
### Input Component
@ -227,7 +207,7 @@ To function fully the custom component should accept the following props:
</Canvas>
```jsx
import { TextFieldBase, Icon, ICON_NAMES } from '../../ui/component-library';
import { TextFieldBase, Icon, ICON_NAMES } from '../../component-library';
// should map the props to the custom input component
const CustomInputComponent = () => <div>{/* Custom input component */}</div>;
@ -252,7 +232,7 @@ Use the `autoComplete` prop to set the autocomplete html attribute. It allows th
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/components/component-library';
import { TextFieldBase } from '../../component-library';
<TextFieldBase type="password" autoComplete />;
```
@ -266,7 +246,7 @@ Use the `autoFocus` prop to focus the `TextFieldBase` during the first mount
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/components/component-library';
import { TextFieldBase } from '../../component-library';
<TextFieldBase autoFocus />;
```
@ -280,7 +260,7 @@ Use the `defaultValue` prop to set the default value of the `TextFieldBase`
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/components/component-library';
import { TextFieldBase } from '../../component-library';
<TextFieldBase defaultValue="default value" />;
```
@ -294,7 +274,7 @@ Use the `disabled` prop to set the disabled state of the `TextFieldBase`
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/components/component-library';
import { TextFieldBase } from '../../component-library';
<TextFieldBase disabled />;
```
@ -308,7 +288,7 @@ Use the `error` prop to set the error state of the `TextFieldBase`
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/components/component-library';
import { TextFieldBase } from '../../component-library';
<TextFieldBase error />;
```
@ -322,7 +302,7 @@ Use the `maxLength` prop to set the maximum allowed input characters for the `Te
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/components/component-library';
import { TextFieldBase } from '../../component-library';
<TextFieldBase maxLength={10} />;
```
@ -336,7 +316,7 @@ Use the `readOnly` prop to set the `TextFieldBase` to read only. When `readOnly`
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/components/component-library';
import { TextFieldBase } from '../../component-library';
<TextFieldBase readOnly />;
```
@ -350,7 +330,7 @@ Use the `required` prop to set the `TextFieldBase` to required. Currently there
</Canvas>
```jsx
import { TextFieldBase } from '../../ui/components/component-library';
import { TextFieldBase } from '../../component-library';
// Currently no visual difference
<TextFieldBase required />;

View File

@ -0,0 +1,17 @@
// 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>
`;

View File

@ -67,7 +67,7 @@ export const TextFieldBase = ({
setFocused(true);
}
if (onClick) {
if (onClick && !disabled) {
onClick(event);
}
};
@ -97,7 +97,7 @@ export const TextFieldBase = ({
'mm-text-field-base',
`mm-text-field-base--size-${size}`,
{
'mm-text-field-base--focused': focused && !disabled && !readOnly,
'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,

View File

@ -1,4 +1,6 @@
import React, { useState, useRef } from 'react';
import { useArgs } from '@storybook/client-api';
import PropTypes from 'prop-types';
import {
SIZES,
@ -10,16 +12,22 @@ import {
} from '../../../helpers/constants/design-system';
import Box from '../../ui/box/box';
import { Icon, ICON_NAMES } from '../icon';
import { AvatarToken } from '../avatar-token';
import { AvatarAccount } from '../avatar-account';
import { Text } from '../text';
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 = [
@ -144,7 +152,13 @@ export default {
},
};
const Template = (args) => <TextFieldBase {...args} />;
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';
@ -228,7 +242,6 @@ export const LeftAccessoryRightAccessory = (args) => {
value={value.search}
name="search"
onChange={handleOnChange}
showClear
leftAccessory={
<Icon
color={COLORS.ICON_ALTERNATIVE}
@ -243,17 +256,11 @@ export const LeftAccessoryRightAccessory = (args) => {
name="address"
onChange={handleOnChange}
rightAccessory={
<Box
as="button"
display={DISPLAY.FLEX}
style={{ padding: 0 }}
backgroundColor={COLORS.TRANSPARENT}
>
<Icon
color={COLORS.PRIMARY_DEFAULT}
name={ICON_NAMES.SCAN_BARCODE_FILLED}
/>
</Box>
<ButtonIcon
iconName={ICON_NAMES.SCAN_BARCODE_FILLED}
ariaLabel="Scan QR code"
iconProps={{ color: COLORS.PRIMARY_DEFAULT }}
/>
}
/>
<TextFieldBase
@ -274,11 +281,11 @@ export const LeftAccessoryRightAccessory = (args) => {
alignItems={ALIGN_ITEMS.CENTER}
>
<AvatarToken
tokenName="ast"
tokenImageUrl="./AST.png"
tokenName="eth"
tokenImageUrl="./images/eth_logo.svg"
size={SIZES.SM}
/>
<Text>AST</Text>
<Text>ETH</Text>
<Icon
name={ICON_NAMES.ARROW_DOWN}
color={COLORS.ICON_DEFAULT}
@ -287,8 +294,12 @@ export const LeftAccessoryRightAccessory = (args) => {
</Box>
}
rightAccessory={
<Text variant={TEXT.BODY_SM} color={COLORS.TEXT_ALTERNATIVE}>
= ${handleTokenPrice(value.amount, 0.11)}
<Text
variant={TEXT.BODY_SM}
color={COLORS.TEXT_ALTERNATIVE}
style={{ whiteSpace: 'nowrap' }}
>
= ${handleTokenPrice(value.amount, 1173.58)}
</Text>
}
/>
@ -327,70 +338,66 @@ export const InputRef = (args) => {
setValue(e.target.value);
};
return (
<>
<Box display={DISPLAY.FLEX}>
<TextFieldBase
{...args}
inputRef={inputRef}
value={value}
onChange={handleOnChange}
/>
<Box
as="button"
backgroundColor={COLORS.BACKGROUND_ALTERNATIVE}
color={COLORS.TEXT_DEFAULT}
borderColor={COLORS.BORDER_DEFAULT}
borderRadius={SIZES.XL}
marginLeft={1}
paddingLeft={2}
paddingRight={2}
onClick={handleOnClick}
>
<Button marginLeft={1} onClick={handleOnClick}>
Edit
</Box>
</>
</Button>
</Box>
);
};
const CustomInputComponent = ({
as,
autoComplete,
autoFocus,
defaultValue,
disabled,
focused,
id,
maxLength,
name,
onBlur,
onChange,
onFocus,
padding,
paddingLeft,
paddingRight,
placeholder,
readOnly,
ref,
required,
value,
variant,
type,
className,
'aria-invalid': ariaInvalid,
...props
}) => {
return (
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}
{...{
as,
className,
as,
autoComplete,
autoFocus,
defaultValue,
@ -404,11 +411,11 @@ const CustomInputComponent = ({
onFocus,
placeholder,
readOnly,
ref,
required,
value,
variant,
type,
...inputProps,
}}
/>
<Text variant={TEXT.BODY_XS} color={COLORS.TEXT_ALTERNATIVE}>
@ -417,10 +424,43 @@ const CustomInputComponent = ({
</Box>
<Text variant={TEXT.BODY_XS}>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.propTypes = { ...TextFieldBase.propTypes };
CustomInputComponent.displayName = 'CustomInputComponent';
export const InputComponent = (args) => (
<TextFieldBase
@ -435,6 +475,8 @@ export const InputComponent = (args) => (
/>
);
InputComponent.args = { autoComplete: true };
export const AutoComplete = Template.bind({});
AutoComplete.args = {
autoComplete: true,

View File

@ -1,7 +1,8 @@
/* eslint-disable jest/require-top-level-describe */
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithUserEvent } from '../../../../test/lib/render-helpers';
import { SIZES } from '../../../helpers/constants/design-system';
import Box from '../../ui/box';
@ -10,8 +11,9 @@ import { TextFieldBase } from './text-field-base';
describe('TextFieldBase', () => {
it('should render correctly', () => {
const { getByRole } = render(<TextFieldBase />);
const { getByRole, container } = render(<TextFieldBase />);
expect(getByRole('textbox')).toBeDefined();
expect(container).toMatchSnapshot();
});
it('should render and be able to input text', () => {
const { getByTestId } = render(
@ -25,54 +27,54 @@ describe('TextFieldBase', () => {
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', () => {
const { getByTestId } = render(
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('text-field-base');
const textFieldBase = getByTestId('input');
fireEvent.click(textFieldBase);
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', () => {
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(
<TextFieldBase
inputProps={{ 'data-testid': 'text-field-base' }}
onFocus={onFocus}
onBlur={onBlur}
/>,
);
const textFieldBase = getByTestId('text-field-base');
fireEvent.focus(textFieldBase);
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', () => {
it('should render and fire onChange event', async () => {
const onChange = jest.fn();
const { getByTestId } = render(
const { getByTestId, user } = renderWithUserEvent(
<TextFieldBase
inputProps={{ 'data-testid': 'text-field-base' }}
onChange={onChange}
/>,
);
const textFieldBase = getByTestId('text-field-base');
fireEvent.change(textFieldBase, { target: { value: 'text value' } });
expect(onChange).toHaveBeenCalledTimes(1);
await user.type(textFieldBase, '123');
expect(textFieldBase).toHaveValue('123');
expect(onChange).toHaveBeenCalledTimes(3);
});
it('should render and fire onClick event', () => {
it('should render and fire onClick event', async () => {
const onClick = jest.fn();
const { getByTestId } = render(
const { getByTestId, user } = renderWithUserEvent(
<TextFieldBase
inputProps={{ 'data-testid': 'text-field-base' }}
onClick={onClick}
@ -80,7 +82,7 @@ describe('TextFieldBase', () => {
);
const textFieldBase = getByTestId('text-field-base');
fireEvent.click(textFieldBase);
await user.click(textFieldBase);
expect(onClick).toHaveBeenCalledTimes(1);
});
it('should render with different size classes', () => {
@ -175,14 +177,21 @@ describe('TextFieldBase', () => {
);
expect(getByRole('textbox').value).toBe('default value');
});
it('should render in disabled state and not focus or be clickable', () => {
it('should render in disabled state and not focus or be clickable', async () => {
const mockOnClick = jest.fn();
const mockOnFocus = jest.fn();
const { getByRole } = render(
<TextFieldBase disabled onFocus={mockOnFocus} onClick={mockOnClick} />,
const { getByRole, getByTestId, user } = renderWithUserEvent(
<TextFieldBase
disabled
onFocus={mockOnFocus}
onClick={mockOnClick}
data-testid="text-field-base"
/>,
);
getByRole('textbox').focus();
const textFieldBase = getByTestId('text-field-base');
await user.click(textFieldBase);
expect(getByRole('textbox')).toBeDisabled();
expect(mockOnClick).toHaveBeenCalledTimes(0);
expect(mockOnFocus).toHaveBeenCalledTimes(0);
@ -196,29 +205,24 @@ describe('TextFieldBase', () => {
);
});
it('should render with maxLength and not allow more than the set characters', async () => {
const { getByRole } = render(<TextFieldBase maxLength={5} />);
const { getByRole, user } = renderWithUserEvent(
<TextFieldBase maxLength={5} />,
);
const textFieldBase = getByRole('textbox');
await userEvent.type(textFieldBase, '1234567890');
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', () => {
const { getByTestId } = render(
<TextFieldBase
readOnly
data-testid="read-only"
inputProps={{ 'data-testid': 'text-field-base-readonly' }}
/>,
);
expect(getByTestId('read-only')).not.toHaveClass(
'mm-text-field-base--focused ',
);
expect(getByTestId('text-field-base-readonly')).toHaveAttribute(
'readonly',
'',
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(
@ -232,12 +236,12 @@ describe('TextFieldBase', () => {
'',
);
});
it('should render with a custom input and still work', () => {
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 } = render(
const { getByTestId, user } = renderWithUserEvent(
<TextFieldBase
InputComponent={CustomInputComponent}
inputProps={{ 'data-testid': 'text-field-base', className: 'test' }}
@ -246,7 +250,7 @@ describe('TextFieldBase', () => {
const textFieldBase = getByTestId('text-field-base');
expect(textFieldBase.value).toBe(''); // initial value is empty string
fireEvent.change(textFieldBase, { target: { value: 'text value' } });
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

View File

@ -30,7 +30,7 @@ export const TextField = ({
<ButtonIcon
className="mm-text-field__button-clear"
ariaLabel="Clear" // TODO: i18n
icon={ICON_NAMES.CLOSE_OUTLINE}
iconName={ICON_NAMES.CLOSE_OUTLINE}
size={SIZES.SM}
onClick={clearButtonOnClick}
{...clearButtonProps}