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

Textarea UI component (#12688)

* Initial Textarea component

* added no-scroll class and css

* added tests

* removed comment from prettier, updated README title

* updated tests

* added resize tests

* fixed grammar

* updated scss

* changes per linter

* updated title to match new folder structure for storybook

* reverted unintended change

Co-authored-by: hmalik88 <hassan.malik@consensys.net>
Co-authored-by: Hassan Malik <41640681+hmalik88@users.noreply.github.com>
This commit is contained in:
George Marshall 2021-12-03 08:54:29 -08:00 committed by GitHub
parent cd4ddffd9c
commit 6d34d85f6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 377 additions and 43 deletions

View File

@ -0,0 +1,15 @@
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import TextArea from '.';
# TextArea
TextArea allows users to enter text into the UI
<Canvas>
<Story id="ui-components-ui-textarea-textarea-stories-js--default-story" />
</Canvas>
## Component API
<ArgsTable of={TextArea} />

View File

@ -0,0 +1 @@
export { default } from './textarea';

View File

@ -0,0 +1,25 @@
@use "design-system";
.textarea {
display: block;
box-shadow: none;
color: design-system.$black;
@include design-system.H6;
font-size: 1rem;
&--scrollable {
overflow-y: scroll;
}
&--not-scrollable {
overflow-y: hidden;
}
@each $size in design-system.$resize {
&--resize-#{$size} {
resize: $size;
}
}
}

View File

@ -0,0 +1,91 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import {
COLORS,
RESIZE,
SIZES,
BORDER_STYLE,
BLOCK_SIZES,
} from '../../../helpers/constants/design-system';
import Box from '../box';
const TextArea = ({
className,
value,
onChange,
resize = RESIZE.BOTH,
scrollable = false,
height,
boxProps,
...props
}) => {
const textAreaClassnames = classnames(
'textarea',
className,
`textarea--resize-${resize}`,
{
'textarea--scrollable': scrollable,
'textarea--not-scrollable': !scrollable,
},
);
return (
<Box
borderColor={COLORS.UI3}
borderRadius={SIZES.SM}
borderStyle={BORDER_STYLE.SOLID}
padding={[4, 4]}
width={BLOCK_SIZES.FULL}
{...boxProps}
>
{(boxClassName) => (
<textarea
required
style={{ height }}
className={classnames(boxClassName, textAreaClassnames)}
{...{ value, onChange, ...props }}
/>
)}
</Box>
);
};
TextArea.propTypes = {
/**
* The height of the Textarea component. Accepts any number, px or % value
*/
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/**
* Optional additional className to add to the Textarea component
*/
className: PropTypes.string,
/**
* Value is the text of the TextArea component
*/
value: PropTypes.string,
/**
* The onChange function of the textarea
*/
onChange: PropTypes.func,
/**
* Resize is the resize capability of the textarea accepts all valid css values
* Defaults to "both"
*/
resize: PropTypes.oneOf(Object.values(RESIZE)),
/**
* Whether the Textarea should be scrollable. Applies overflow-y: scroll to the textarea
* Defaults to false
*/
scrollable: PropTypes.bool,
/**
* The Textarea component accepts all Box component props inside the boxProps object
*/
boxProps: PropTypes.shape({
...Box.propTypes,
}),
};
export default TextArea;

View File

@ -0,0 +1,119 @@
import React from 'react';
import { useArgs } from '@storybook/client-api';
import {
COLORS,
RESIZE,
SIZES,
BORDER_STYLE,
BLOCK_SIZES,
} from '../../../helpers/constants/design-system';
import README from './README.mdx';
import Textarea from '.';
export default {
title: 'Components/UI/Textarea',
id: __filename,
component: Textarea,
parameters: {
docs: {
page: README,
},
},
argTypes: {
className: {
control: 'text',
},
value: {
control: 'text',
},
onChange: {
action: 'onChange',
},
resize: {
control: 'select',
options: Object.values(RESIZE),
},
scrollable: {
control: 'boolean',
},
height: {
control: 'number',
},
boxProps: {
control: 'object',
},
},
};
export const DefaultStory = (args) => {
const [{ value }, updateArgs] = useArgs();
const handleOnChange = (e) => {
updateArgs({
value: e.target.value,
});
};
return (
<>
<label htmlFor="textarea">Label</label>
<Textarea {...args} value={value} onChange={handleOnChange} id="textarea">
{args.children}
</Textarea>
</>
);
};
DefaultStory.storyName = 'Default';
DefaultStory.args = {
value:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod temporld, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod temporld, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod temporld',
resize: RESIZE.BOTH,
scrollable: false,
boxProps: {
borderColor: COLORS.UI3,
borderRadius: SIZES.SM,
borderStyle: BORDER_STYLE.SOLID,
padding: [2, 4],
},
height: 'auto',
};
export const Scrollable = (args) => {
const [{ value }, updateArgs] = useArgs();
const handleOnChange = (e) => {
updateArgs({
value: e.target.value,
});
};
return (
<div style={{ width: 280 }}>
<Textarea
{...args}
value={value}
onChange={handleOnChange}
aria-label="textarea"
>
{args.children}
</Textarea>
</div>
);
};
Scrollable.args = {
value:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod temporld, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod temporld, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod temporld. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod temporld, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod temporld, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod temporld',
resize: RESIZE.NONE,
scrollable: true,
height: 170,
boxProps: {
borderColor: COLORS.TRANSPARENT,
borderRadius: SIZES.NONE,
borderStyle: BORDER_STYLE.NONE,
padding: [2, 4],
width: BLOCK_SIZES.FULL,
},
};

View File

@ -0,0 +1,105 @@
import * as React from 'react';
import { render, fireEvent } from '@testing-library/react';
import {
COLORS,
RESIZE,
SIZES,
BORDER_STYLE,
} from '../../../helpers/constants/design-system';
import TextArea from '.';
describe('TextArea', () => {
const text =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod temporld, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod temporld, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod temporld';
const onChange = jest.fn();
let args;
beforeEach(() => {
args = {
name: 'Text area',
value: text,
resize: RESIZE.BOTH,
scrollable: false,
boxProps: {
borderColor: COLORS.UI3,
borderRadius: SIZES.SM,
borderStyle: BORDER_STYLE.SOLID,
padding: [2, 4],
},
height: '100px',
onChange,
};
});
it('should render the TextArea component without crashing', () => {
const { getByText } = render(<TextArea {...args} />);
expect(getByText(text)).toBeDefined();
});
it('should call onChange when there is a change made', () => {
const { container } = render(<TextArea {...args} />);
fireEvent.change(container.firstChild, { target: { value: 'abc' } });
expect(onChange).toHaveBeenCalled();
});
it('should not be able to resize if the resize prop is RESIZE.NONE', () => {
args.resize = RESIZE.NONE;
const { container } = render(<TextArea {...args} />);
const classList = [...container.firstChild.classList];
const matches = classList.filter((itm) =>
itm.startsWith('textarea--resize'),
);
expect(matches).toHaveLength(1);
expect(matches[0]).toStrictEqual('textarea--resize-none');
});
it('should be able to resize both height and width if the resize prop is RESIZE.BOTH', () => {
args.resize = RESIZE.BOTH;
const { container } = render(<TextArea {...args} />);
const classList = [...container.firstChild.classList];
const matches = classList.filter((itm) =>
itm.startsWith('textarea--resize'),
);
expect(matches).toHaveLength(1);
expect(matches[0]).toStrictEqual('textarea--resize-both');
});
it('should only be able to resize width if the resize prop is RESIZE.HORIZONTAL', () => {
args.resize = RESIZE.HORIZONTAL;
const { container } = render(<TextArea {...args} />);
const classList = [...container.firstChild.classList];
const matches = classList.filter((itm) =>
itm.startsWith('textarea--resize'),
);
expect(matches).toHaveLength(1);
expect(matches[0]).toStrictEqual('textarea--resize-horizontal');
});
it('should only be able to resize height if the resize prop is RESIZE.VERTICAL', () => {
args.resize = RESIZE.VERTICAL;
const { container } = render(<TextArea {...args} />);
const classList = [...container.firstChild.classList];
const matches = classList.filter((itm) =>
itm.startsWith('textarea--resize'),
);
expect(matches).toHaveLength(1);
expect(matches[0]).toStrictEqual('textarea--resize-vertical');
});
it('should be able to scroll when given a true value for scrollable', () => {
args.scrollable = true;
const { container } = render(<TextArea {...args} />);
const doesScroll = container.firstChild.classList.contains(
'textarea--scrollable',
);
const doesNotScroll = container.firstChild.classList.contains(
'textarea--not-scrollable',
);
expect(doesScroll).toStrictEqual(true);
expect(doesNotScroll).toStrictEqual(false);
});
it('should NOT be able to scroll when given a false value for scrollable', () => {
const { container } = render(<TextArea {...args} />);
const doesScroll = container.firstChild.classList.contains(
'textarea--scrollable',
);
const doesNotScroll = container.firstChild.classList.contains(
'textarea--not-scrollable',
);
expect(doesScroll).toStrictEqual(false);
expect(doesNotScroll).toStrictEqual(true);
});
});

View File

@ -53,6 +53,7 @@
@import 'tooltip/index';
@import 'truncated-definition-list/truncated-definition-list';
@import 'typography/typography';
@import 'textarea/index';
@import 'unit-input/index';
@import 'url-icon/index';
@import 'update-nickname-popover/index';

View File

@ -1,28 +1,11 @@
$align-items:
baseline,
center,
flex-end,
flex-start,
stretch;
$align-items: baseline, center, flex-end, flex-start, stretch;
$justify-content:
center,
flex-end,
flex-start,
space-around,
space-between,
$justify-content: center, flex-end, flex-start, space-around, space-between,
space-evenly;
$flex-direction:
row,
row-reverse,
column,
column-reverse;
$flex-direction: row, row-reverse, column, column-reverse;
$flex-wrap:
wrap,
wrap-reverse,
nowrap;
$flex-wrap: wrap, wrap-reverse, nowrap;
$fractions: (
1\/2: 50%,
@ -50,31 +33,12 @@ $fractions: (
8\/12: 66.666667%,
9\/12: 75%,
10\/12: 83.333333%,
11\/12: 91.666667%,
11\/12: 91.666667%
);
$sizes-numeric:
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12;
$sizes-numeric: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12;
$sizes-strings:
xs,
sm,
md,
lg,
xl,
none;
$sizes-strings: xs, sm, md, lg, xl, none;
$border-style: solid, double, none, dashed, dotted;
$directions: top, right, bottom, left;
@ -82,3 +46,7 @@ $display: block, grid, flex, inline-block, inline-grid, inline-flex, list-item;
$text-align: left, right, center, justify, end;
$font-weight: bold, normal, 100, 200, 300, 400, 500, 600, 700, 800, 900;
$font-style: normal, italic, oblique;
$font-size: 10px, 12px;
// textarea
$resize: none, both, horizontal, vertical, initial, inherit;

View File

@ -184,3 +184,12 @@ export const SEVERITIES = {
INFO: 'info',
SUCCESS: 'success',
};
export const RESIZE = {
NONE: 'none',
BOTH: 'both',
HORIZONTAL: 'horizontal',
VERTICAL: 'vertical',
INITIAL: 'initial',
INHERIT: 'inherit',
};