mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +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:
parent
cd4ddffd9c
commit
6d34d85f6e
15
ui/components/ui/textarea/README.mdx
Normal file
15
ui/components/ui/textarea/README.mdx
Normal 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} />
|
1
ui/components/ui/textarea/index.js
Normal file
1
ui/components/ui/textarea/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './textarea';
|
25
ui/components/ui/textarea/index.scss
Normal file
25
ui/components/ui/textarea/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
91
ui/components/ui/textarea/textarea.js
Normal file
91
ui/components/ui/textarea/textarea.js
Normal 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;
|
119
ui/components/ui/textarea/textarea.stories.js
Normal file
119
ui/components/ui/textarea/textarea.stories.js
Normal 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,
|
||||
},
|
||||
};
|
105
ui/components/ui/textarea/textarea.test.js
Normal file
105
ui/components/ui/textarea/textarea.test.js
Normal 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);
|
||||
});
|
||||
});
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user