Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b447b88

Browse files
committedApr 2, 2020
feat(textfield): make TextField and TextArea one component
Displays textarea when passed 'multiline' prop. Handles 'rows' prop. Added tests. BREAKING CHANGE: TextArea component removed from library (use <TextField multiline /> instead)
1 parent 859dce0 commit b447b88

File tree

6 files changed

+242
-298
lines changed

6 files changed

+242
-298
lines changed
 

‎src/components/TextArea/TextArea.js

Lines changed: 0 additions & 99 deletions
This file was deleted.

‎src/components/TextArea/TextArea.stories.js

Lines changed: 0 additions & 100 deletions
This file was deleted.

‎src/components/TextField/TextField.js

Lines changed: 61 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,113 @@
11
import React from 'react';
22
import propTypes from 'prop-types';
33

4-
import styled from 'styled-components';
4+
import styled, { css } from 'styled-components';
55
import { createDisabledTextStyles, createFlatBoxStyles } from '../common';
66
import { blockSizes, fontFamily } from '../common/system';
77
import Cutout from '../Cutout/Cutout';
88

9-
const StyledWrapper = styled(Cutout)`
10-
height: ${blockSizes.md};
9+
const sharedWrapperStyles = css`
10+
display: flex;
11+
align-items: center;
12+
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
13+
min-height: ${blockSizes.md};
14+
`;
15+
16+
const Wrapper = styled(Cutout)`
17+
${sharedWrapperStyles}
1118
background: ${({ theme, isDisabled }) =>
1219
isDisabled ? theme.material : theme.canvas};
1320
`;
14-
const StyledFlatWrapper = styled.div`
15-
position: relative;
16-
height: ${blockSizes.md};
21+
22+
const FlatWrapper = styled.div`
1723
${createFlatBoxStyles()}
24+
${sharedWrapperStyles}
25+
position: relative;
1826
`;
19-
export const StyledTextInput = styled.input`
27+
28+
const sharedInputStyles = css`
29+
display: block;
2030
box-sizing: border-box;
2131
width: 100%;
2232
height: 100%;
23-
padding: 0 8px;
2433
outline: none;
2534
border: none;
2635
background: none;
2736
font-size: 1rem;
37+
min-height: 27px;
2838
font-family: ${fontFamily};
2939
color: ${({ theme }) => theme.inputText};
3040
${({ disabled, variant }) =>
3141
variant !== 'flat' && disabled && createDisabledTextStyles()}
3242
`;
43+
44+
export const StyledTextInput = styled.input`
45+
${sharedInputStyles}
46+
padding: 0 8px;
47+
`;
48+
49+
const StyledTextArea = styled.textarea`
50+
${sharedInputStyles}
51+
padding: 8px;
52+
resize: none;
53+
`;
54+
3355
const TextField = React.forwardRef(function TextField(props, ref) {
3456
const {
35-
onChange,
57+
className,
3658
disabled,
37-
variant,
38-
type,
39-
style,
59+
fullWidth,
60+
multiline,
61+
onChange,
4062
shadow,
41-
className,
42-
width,
63+
style,
64+
type,
65+
variant,
4366
...otherProps
4467
} = props;
45-
const Wrapper = variant === 'flat' ? StyledFlatWrapper : StyledWrapper;
68+
const WrapperComponent = variant === 'flat' ? FlatWrapper : Wrapper;
69+
const Input = multiline ? StyledTextArea : StyledTextInput;
4670
return (
47-
<Wrapper
48-
width={width}
49-
shadow={shadow}
50-
isDisabled={disabled}
51-
style={{ ...style, width: width || 'auto' }}
71+
<WrapperComponent
5272
className={className}
73+
fullWidth={fullWidth}
74+
isDisabled={disabled}
75+
shadow={shadow}
76+
style={style}
5377
>
54-
<StyledTextInput
78+
<Input
79+
disabled={disabled}
5580
onChange={disabled ? undefined : onChange}
5681
readOnly={disabled}
57-
disabled={disabled}
58-
variant={variant}
59-
type={type}
6082
ref={ref}
83+
type={type}
84+
variant={variant}
6185
{...otherProps}
6286
/>
63-
</Wrapper>
87+
</WrapperComponent>
6488
);
6589
});
6690
TextField.defaultProps = {
91+
className: '',
6792
disabled: false,
68-
type: 'text',
93+
fullWidth: null,
94+
multiline: false,
95+
onChange: () => {},
6996
shadow: true,
70-
variant: 'default',
7197
style: {},
72-
width: null,
73-
onChange: () => {},
74-
className: ''
98+
type: 'text',
99+
variant: 'default'
75100
};
76101

77102
TextField.propTypes = {
78-
width: propTypes.oneOfType([propTypes.string, propTypes.number]),
79-
onChange: propTypes.func,
103+
className: propTypes.string,
80104
disabled: propTypes.bool,
81-
variant: propTypes.oneOf(['default', 'flat']),
105+
fullWidth: propTypes.bool,
106+
multiline: propTypes.bool,
107+
onChange: propTypes.func,
82108
shadow: propTypes.bool,
109+
style: propTypes.shape([propTypes.string, propTypes.number]),
83110
type: propTypes.string,
84-
className: propTypes.string,
85-
style: propTypes.shape([propTypes.string, propTypes.number])
111+
variant: propTypes.oneOf(['default', 'flat'])
86112
};
87113
export default TextField;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Pretty much straight out copied from https://github.com/mui-org/material-ui 😂
2+
3+
import React from 'react';
4+
import { fireEvent } from '@testing-library/react';
5+
6+
import { renderWithTheme } from '../../../test/utils';
7+
import TextField from './TextField';
8+
9+
describe('<TextField />', () => {
10+
it('should render an <input /> inside the div', () => {
11+
const { container } = renderWithTheme(<TextField />);
12+
const input = container.querySelector('input');
13+
expect(input).toHaveAttribute('type', 'text');
14+
expect(input).not.toHaveAttribute('required');
15+
});
16+
17+
it('should fire event callbacks', () => {
18+
const handleChange = jest.fn();
19+
const handleFocus = jest.fn();
20+
const handleBlur = jest.fn();
21+
const handleKeyUp = jest.fn();
22+
const handleKeyDown = jest.fn();
23+
const { getByRole } = renderWithTheme(
24+
<TextField
25+
onChange={handleChange}
26+
onFocus={handleFocus}
27+
onBlur={handleBlur}
28+
onKeyUp={handleKeyUp}
29+
onKeyDown={handleKeyDown}
30+
/>
31+
);
32+
const input = getByRole('textbox');
33+
34+
// simulating user input: gain focus, key input (keydown, (input), change, keyup), blur
35+
36+
input.focus();
37+
expect(handleFocus).toHaveBeenCalledTimes(1);
38+
39+
fireEvent.keyDown(document.activeElement, { key: 'a' });
40+
expect(handleKeyDown).toHaveBeenCalledTimes(1);
41+
42+
fireEvent.change(input, { target: { value: 'a' } });
43+
expect(handleChange).toHaveBeenCalledTimes(1);
44+
45+
fireEvent.keyUp(document.activeElement, { key: 'a' });
46+
expect(handleKeyUp).toHaveBeenCalledTimes(1);
47+
48+
input.blur();
49+
expect(handleBlur).toHaveBeenCalledTimes(1);
50+
});
51+
52+
it('should considered [] as controlled', () => {
53+
const { getByRole } = renderWithTheme(<TextField value={[]} />);
54+
const input = getByRole('textbox');
55+
56+
expect(input).toHaveProperty('value', '');
57+
fireEvent.change(input, { target: { value: 'do not work' } });
58+
expect(input).toHaveProperty('value', '');
59+
});
60+
61+
it('should forwardRef to native input', () => {
62+
const inputRef = React.createRef();
63+
const { getByRole } = renderWithTheme(<TextField ref={inputRef} />);
64+
const input = getByRole('textbox');
65+
expect(inputRef.current).toBe(input);
66+
});
67+
68+
describe('multiline', () => {
69+
it('should render textarea when passed the multiline prop', () => {
70+
const { container } = renderWithTheme(<TextField multiline />);
71+
const textarea = container.querySelector('textarea');
72+
expect(textarea).not.toBe(null);
73+
});
74+
75+
it('should forward rows prop', () => {
76+
const { container } = renderWithTheme(<TextField multiline rows={3} />);
77+
const textarea = container.querySelector('textarea');
78+
expect(textarea).toHaveAttribute('rows', '3');
79+
});
80+
});
81+
82+
describe('prop: disabled', () => {
83+
it('should render a disabled <input />', () => {
84+
const { container } = renderWithTheme(<TextField disabled />);
85+
const input = container.querySelector('input');
86+
expect(input).toHaveAttribute('disabled');
87+
});
88+
it('should be overridden by props', () => {
89+
const { getByRole, rerender } = renderWithTheme(<TextField disabled />);
90+
rerender(<TextField disabled={false} />);
91+
const input = getByRole('textbox');
92+
expect(input).not.toHaveAttribute('disabled');
93+
});
94+
});
95+
});

‎src/components/TextField/TextField.stories.js

Lines changed: 85 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { storiesOf } from '@storybook/react';
33

44
import styled from 'styled-components';
55

6-
import { TextField, Button, Toolbar, Cutout } from '..';
6+
import { TextField, Button, Cutout } from '..';
77

8+
const loremIpsum = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sollicitudin, ante vel porttitor posuere, tellus nisi interdum ipsum, non bibendum ante risus ut purus. Curabitur vel posuere odio. Vivamus rutrum, nunc et ullamcorper sagittis, tellus ligula maximus quam, id dapibus sapien metus lobortis diam. Proin luctus, dolor in finibus feugiat, lacus enim gravida sem, quis aliquet tellus leo nec enim. Morbi varius bibendum augue quis venenatis. Curabitur ut elit augue. Pellentesque posuere enim a mattis interdum. Donec sodales convallis turpis, a vulputate elit. Suspendisse potenti.`;
89
const onChange = e => console.log(e.target.value);
910

1011
const StyledCutout = styled(Cutout)`
@@ -18,68 +19,59 @@ const Wrapper = styled.div`
1819

1920
storiesOf('TextField', module)
2021
.addDecorator(story => <Wrapper>{story()}</Wrapper>)
21-
.add('default', () => <TextField defaultValue='' onChange={onChange} />)
22-
.add('controlled', () => <ControlledTextFieldExample />)
23-
.add('no shadow', () => (
24-
<TextField defaultValue='No shadow' shadow={false} onChange={onChange} />
25-
))
26-
.add('disabled', () => (
27-
<TextField defaultValue="Can't type 😥" disabled onChange={onChange} />
28-
))
29-
.add('custom width', () => (
30-
<TextField defaultValue='Custom width' width={150} onChange={onChange} />
31-
))
22+
.add('default', () => <DefaultTextFieldExample />)
3223
.add('flat', () => (
33-
<StyledCutout style={{ padding: '2rem', width: '300px' }}>
24+
<StyledCutout style={{ padding: '1rem', width: '400px' }}>
3425
<p style={{ lineHeight: 1.3 }}>
3526
When you want to add input field on a light background (like scrollable
3627
content), just use the flat variant:
3728
</p>
38-
<div style={{ display: 'flex', alignItems: 'center', marginTop: '1rem' }}>
39-
<label
40-
style={{ paddingRight: '0.5rem', fontSize: '1rem' }}
41-
htmlFor='name'
42-
>
43-
Name:
44-
</label>
45-
<TextField
46-
id='name'
47-
variant='flat'
48-
placeholder='type here...'
49-
width={150}
50-
onChange={onChange}
51-
/>
52-
</div>
53-
</StyledCutout>
54-
))
55-
.add('flat disabled', () => (
56-
<StyledCutout style={{ padding: '2rem', width: '300px' }}>
57-
<p style={{ lineHeight: 1.3 }}>
58-
When you want to add input field on a light background (like scrollable
59-
content), just use the flat variant:
60-
</p>
61-
<div style={{ display: 'flex', alignItems: 'center', marginTop: '1rem' }}>
62-
<label
63-
style={{ paddingRight: '0.5rem', fontSize: '1rem' }}
64-
htmlFor='name'
65-
>
66-
Name:
67-
</label>
68-
<TextField
69-
id='name'
70-
variant='flat'
71-
defaultValue="Can't type 😥"
72-
width={150}
73-
onChange={onChange}
74-
disabled
75-
/>
76-
</div>
29+
<br />
30+
<TextField
31+
id='name'
32+
variant='flat'
33+
placeholder='type here...'
34+
width={150}
35+
onChange={onChange}
36+
fullWidth
37+
/>
38+
<br />
39+
<TextField
40+
id='disabled'
41+
variant='flat'
42+
placeholder='Disabled'
43+
width={150}
44+
onChange={onChange}
45+
disabled
46+
fullWidth
47+
/>
48+
<br />
49+
<TextField
50+
multiline
51+
variant='flat'
52+
rows={4}
53+
id='name'
54+
defaultValue={loremIpsum}
55+
onChange={onChange}
56+
fullWidth
57+
/>
58+
<br />
59+
<TextField
60+
multiline
61+
variant='flat'
62+
disabled
63+
rows={4}
64+
id='name'
65+
defaultValue={loremIpsum}
66+
onChange={onChange}
67+
fullWidth
68+
/>
7769
</StyledCutout>
7870
));
7971

80-
class ControlledTextFieldExample extends React.Component {
72+
class DefaultTextFieldExample extends React.Component {
8173
state = {
82-
value: 'default value'
74+
value: ''
8375
};
8476

8577
handleChange = e => this.setState({ value: e.target.value });
@@ -90,12 +82,45 @@ class ControlledTextFieldExample extends React.Component {
9082
const { value } = this.state;
9183

9284
return (
93-
<Toolbar>
94-
<TextField value={value} onChange={this.handleChange} />
95-
<Button onClick={this.reset} style={{ marginLeft: '2px' }}>
96-
Reset
97-
</Button>
98-
</Toolbar>
85+
<div style={{ width: 400 }}>
86+
<div style={{ display: 'flex' }}>
87+
<TextField
88+
value={value}
89+
placeholder='Type here...'
90+
onChange={this.handleChange}
91+
fullWidth
92+
/>
93+
<Button onClick={this.reset} style={{ marginLeft: '2px' }}>
94+
Reset
95+
</Button>
96+
</div>
97+
<br />
98+
<TextField
99+
defaultValue='Disabled'
100+
disabled
101+
onChange={onChange}
102+
fullWidth
103+
/>
104+
<br />
105+
<TextField
106+
multiline
107+
rows={4}
108+
id='name'
109+
defaultValue={loremIpsum}
110+
onChange={onChange}
111+
fullWidth
112+
/>
113+
<br />
114+
<TextField
115+
multiline
116+
disabled
117+
rows={4}
118+
id='name'
119+
defaultValue={loremIpsum}
120+
onChange={onChange}
121+
fullWidth
122+
/>
123+
</div>
99124
);
100125
}
101126
}

‎src/components/index.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ export { default as TableDataCell } from './TableDataCell/TableDataCell';
3131
export { default as TableHead } from './TableHead/TableHead';
3232
export { default as TableHeadCell } from './TableHeadCell/TableHeadCell';
3333
export { default as TableRow } from './TableRow/TableRow';
34-
export { default as TextArea } from './TextArea/TextArea';
3534
export { default as TextField } from './TextField/TextField';
3635
export { default as Toolbar } from './Toolbar/Toolbar';
3736
export { default as Tooltip } from './Tooltip/Tooltip';
@@ -40,6 +39,4 @@ export { default as WindowContent } from './WindowContent/WindowContent';
4039
export { default as WindowHeader } from './WindowHeader/WindowHeader';
4140
export { default as ColorInput } from './ColorInput/ColorInput';
4241

43-
export {
44-
default as LoadingIndicator
45-
} from './LoadingIndicator/LoadingIndicator';
42+
export { default as LoadingIndicator } from './LoadingIndicator/LoadingIndicator';

0 commit comments

Comments
 (0)
Please sign in to comment.