Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TextField] Add in support for multiline text fields #6553

Merged
merged 66 commits into from
May 13, 2017
Merged
Show file tree
Hide file tree
Changes from 59 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
d90eef5
changes focusColor in FormLabel from palette.accent.A200 to palette.p…
peteratticusberg Apr 7, 2017
e1bf713
makes it so on focus the inkbar turns `palette.primary.A200` instead …
peteratticusberg Apr 7, 2017
ab6dfdd
changes checkbox 'checked' color to primary[500] from accent[500]
peteratticusberg Apr 7, 2017
a763d47
sets switch 'checked' color to primary[500] (used to be accent[500])
peteratticusberg Apr 7, 2017
ac69a5b
Merge branch 'next' of github.com:/callemall/material-ui into next
peteratticusberg Apr 7, 2017
050e3b1
modifies TextField to take `multiLine` and `rows` as props and forwar…
peteratticusberg Apr 8, 2017
0ed3174
adds styles to Input component for multiline inputs
peteratticusberg Apr 8, 2017
126e94e
adds a multiline input example
peteratticusberg Apr 8, 2017
9456d66
fixes linting errors
peteratticusberg Apr 8, 2017
9d18e60
Adds a test for the Input component
peteratticusberg Apr 8, 2017
9ad7d98
Adds a test for the TextField component
peteratticusberg Apr 8, 2017
e9ede40
adds visual regression tests for multiline text fields
peteratticusberg Apr 8, 2017
108c76f
fixes linting errors
peteratticusberg Apr 8, 2017
c3d1de3
Merge branch 'next' into multiline-textfield
peteratticusberg Apr 8, 2017
f0feccf
Adds defaultValue in as a prop to TextField to get defaultValue worki…
peteratticusberg Apr 9, 2017
8df8a6f
adds a defaultValue prop into Input to get defaultValues working cons…
peteratticusberg Apr 9, 2017
31268d3
adjusts the docs
peteratticusberg Apr 9, 2017
c17afe7
makes it so Input is responsible for deciding what element to render
peteratticusberg Apr 9, 2017
9d3635e
Makes it so that Input decides whether to render a textarea or an inp…
peteratticusberg Apr 9, 2017
38608e9
Makes it so the component prop will override the multiline default co…
peteratticusberg Apr 17, 2017
3f9e522
multiLine —> multiline
peteratticusberg Apr 17, 2017
ddd51d9
gets tests passing
peteratticusberg Apr 17, 2017
6bed551
removes default value from input
peteratticusberg Apr 17, 2017
94ac535
Merge branch 'next' of github.com:/callemall/material-ui into mtf
peteratticusberg Apr 17, 2017
b3b0534
fixes some typos
peteratticusberg Apr 17, 2017
3812485
adjusts syntax
peteratticusberg Apr 17, 2017
65e9441
Merge branch 'next' of github.com:/peteratticusberg/material-ui into …
peteratticusberg Apr 17, 2017
2cd3fa7
Apply the .underline class to the input wrapper instead of the input …
peteratticusberg Apr 17, 2017
31ed81a
gets visual regression tests Inputs/inputs and TextField/error passing
peteratticusberg Apr 17, 2017
99eddaf
adds new screenshot for multiline textfields
peteratticusberg Apr 17, 2017
2959331
fixes issue with inkbars for multiline textfields which was causing t…
peteratticusberg Apr 17, 2017
8d6b599
Merge branch 'next' into multiline-textfield
oliviertassinari Apr 18, 2017
fe7494f
Merge branch 'next' of github.com:/callemall/material-ui into next
peteratticusberg May 11, 2017
1f74023
Merge branch 'next' into mtf
peteratticusberg May 11, 2017
2a2577b
pulls in @mixby’s auto-resizing textarea
peteratticusberg May 11, 2017
6b681ed
renders an AutoResizingTextArea if multiline is specified but rows is…
peteratticusberg May 11, 2017
00c7e92
Adds a “Multiline Flexible” TextField demo and distinguishes it from …
peteratticusberg May 11, 2017
4bd4b41
swaps demo components
peteratticusberg May 11, 2017
dd21104
gets AutoResizingTextArea communicating with MuiFormControl
peteratticusberg May 11, 2017
bbdd7db
fixes small issue that was just introduced
peteratticusberg May 11, 2017
1f6a0f9
makes it so flow type annotations can be placed at the top of a file …
peteratticusberg May 11, 2017
7abc53b
Currently AutoResizingTextArea is the only “InputComponent” rendered …
peteratticusberg May 11, 2017
b49e7b3
slight adjustment to textfield demos
peteratticusberg May 11, 2017
eb536d8
removes unused classname
peteratticusberg May 11, 2017
5b6d156
a more idiomatic way of saying transparent
peteratticusberg May 11, 2017
b8195a2
having appearance: textfield set was causing a border box to appear a…
peteratticusberg May 11, 2017
65e1924
fixes some linting errors
peteratticusberg May 11, 2017
b5fee37
it’s now the wrapper that has the underline class
peteratticusberg May 11, 2017
d6a5ea1
Updates multiline tests for TextField
peteratticusberg May 11, 2017
5c66857
Adds tests for AutoResizingTextArea
peteratticusberg May 11, 2017
22e7650
cleans some things up…
peteratticusberg May 11, 2017
ab8a3bf
Replaces setTimeout hack with a slightly less offensive hack
peteratticusberg May 12, 2017
62a615f
Position the top of inkbar on top of the underline.
peteratticusberg May 12, 2017
98df3d6
remove TextFieldMultliLine test
peteratticusberg May 12, 2017
ed12ff3
adds TextFieldMultiline regression test back in with proper capitaliz…
peteratticusberg May 12, 2017
da71e70
Fix some comments
peteratticusberg May 12, 2017
0fe9bbc
new baseline for the TextFields demo
peteratticusberg May 12, 2017
25647aa
add a baseline test for the Multiline TextFields
peteratticusberg May 12, 2017
3161cca
empty commmit
peteratticusberg May 12, 2017
8f02f4e
Cleans up some comments and syntax
peteratticusberg May 13, 2017
a068813
Fixes positioning hack
peteratticusberg May 13, 2017
929e6c7
Remove unused methods from AutoResizingTextArea, debounce resize wind…
peteratticusberg May 13, 2017
3cde356
removes AutoResizingTextArea from project
peteratticusberg May 13, 2017
0b4509b
adds AutoResizingTextArea back in with different capitalization
peteratticusberg May 13, 2017
043d843
AutoResizingTextArea --> AutoResizingTextarea
peteratticusberg May 13, 2017
647d625
Add new regression tests for TextFields demo (the difference here was…
peteratticusberg May 13, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ module.exports = {
'react/sort-prop-types': 'error', // airbnb do nothing here.
'react/sort-comp': [2, {
order: [
'type-annotations',
'static-methods',
'lifecycle',
// 'properties', // not real -- NEEDS A PR!!!
Expand Down
3 changes: 1 addition & 2 deletions docs/src/pages/component-demos/progress/LinearBuffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const styleSheet = createStyleSheet('LinearBuffer', () => ({
}));

class LinearBuffer extends Component {
timer: number
state = {
completed: 0,
buffer: 10,
Expand All @@ -26,8 +27,6 @@ class LinearBuffer extends Component {
clearInterval(this.timer);
}

timer: number

progress = () => {
const { completed } = this.state;
if (completed > 100) {
Expand Down
3 changes: 1 addition & 2 deletions docs/src/pages/component-demos/progress/LinearDeterminate.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const styleSheet = createStyleSheet('LinearDeterminate', () => ({
}));

class LinearDeterminate extends Component {
timer: number
state = {
completed: 0,
}
Expand All @@ -25,8 +26,6 @@ class LinearDeterminate extends Component {
clearInterval(this.timer);
}

timer: number

progress = () => {
const { completed } = this.state;
if (completed > 100) {
Expand Down
15 changes: 15 additions & 0 deletions docs/src/pages/component-demos/text-fields/TextFields.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ class TextFields extends Component {
className={classes.input}
type="password"
/>
<TextField
id="multiline flexible"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that it's a valid id. id="multiline-flexible"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch

label="MultiLine"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiline?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

label="Multiline"

multiline
defaultValue="Default Value"
className={classes.input}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about adding rowsMax={6}?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call

/>
<TextField
id="multiline static"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that it's a valid id. id="multiline-static"

label="MultiLine"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

label="Multiline"

multiline
rows={4}
defaultValue="Default Value"
className={classes.input}
/>
</div>
);
}
Expand Down
204 changes: 204 additions & 0 deletions src/Input/AutoResizingTextArea.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// @flow weak

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { createStyleSheet } from 'jss-theme-reactor';
import classnames from 'classnames';
import EventListener from 'react-event-listener';
import customPropTypes from '../utils/customPropTypes';

const rowsHeight = 24;

export const styleSheet = createStyleSheet('MuiTextarea', () => {
return {
root: {
position: 'relative', // because the shadow has position: 'absolute',
'margin-bottom': '-4px', // this is an unfortunate hack
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

marginBottom: -4,

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But can be removed by (so you get the idea)
1.

-<div className={classnames(classes.root, className)}>
+<div className={classnames(classes.root, className)} style={{ height: this.state.height }}>
        <textarea
          {...other}
          ref={(c) => { this.input = c; }}
          rows={this.props.rows}
          className={classes.textarea}
-         style={{ height: this.state.height }}
          onChange={this.handleChange}
        />
    textarea: {
      width: '100%',
+     height: '100%',
      resize: 'none',

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome.

},
textarea: {
width: '100%',
resize: 'none',
font: 'inherit',
padding: 0,
cursor: 'inherit',
boxSizing: 'border-box',
lineHeight: 'inherit',
border: 'none',
outline: 'none',
'background-color': 'rgba(0,0,0,0)',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

background: 'transparent';

},
shadow: {
resize: 'none',
// Overflow also needed to here to remove the extra row
// added to textareas in Firefox.
overflow: 'hidden',
// Visibility needed to hide the extra text area on ipads
visibility: 'hidden',
position: 'absolute',
height: 'auto',
whiteSpace: 'pre-wrap',
},
};
});

/**
* Input
*/
export default class AutoResizingTextArea extends Component {
shadow: HTMLInputElement;
singleLineShadow: HTMLInputElement;
input: HTMLInputElement;
value: string;

static propTypes = {
/**
* Override the inline-styles of the root element.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong wording.

*/
className: PropTypes.string,
defaultValue: PropTypes.any,
disabled: PropTypes.bool,
hintText: PropTypes.string,
onChange: PropTypes.func,
onHeightChange: PropTypes.func,
rows: PropTypes.number,
rowsMax: PropTypes.number,
shadowClassName: PropTypes.object,
value: PropTypes.string,
};

static defaultProps = {
rows: 1,
};

static contextTypes = {
styleManager: customPropTypes.muiRequired,
};

state = {
height: null,
};

componentWillMount() {
// <Input> expects the components it renders to respond to 'value'
this.value = this.props.defaultValue;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What this.value is for?

Copy link
Contributor Author

@peteratticusberg peteratticusberg May 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's so that Input can check whether an AutoResizingTextArea is dirty:

function isDirty(obj) {
  return obj && obj.value && obj.value.length > 0;
}

It's not the prettiest thing in the world but it gets the job done and allows us to get by without having to make any changes to Input.

this.setState({
height: this.props.rows * rowsHeight,
});
}

componentDidMount() {
this.syncHeightWithShadow();
}

componentWillReceiveProps(nextProps) {
if (nextProps.value !== this.props.value ||
nextProps.rowsMax !== this.props.rowsMax) {
this.syncHeightWithShadow(nextProps.value, null, nextProps);
}
}

handleResize = (event) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we should debounce the events, for instance:
https://github.com/callemall/material-ui/blob/next/src/Tabs/Tabs.js#L163-L173

this.syncHeightWithShadow(undefined, event);
};

getInputNode() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that we need that function indirection

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea I agree

return this.input;
}

setValue(value) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that we should expose such method

this.getInputNode().value = value;
this.syncHeightWithShadow(value);
}

syncHeightWithShadow(newValue, event, props) {
const shadow = this.shadow;
const singleLineShadow = this.singleLineShadow;

const hasNewValue = (newValue && newValue !== '');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const hasNewValue = newValue && newValue !== '';

const displayText = this.props.hintText && !hasNewValue ? this.props.hintText : newValue;

if (displayText !== undefined) {
shadow.value = displayText;
}

const lineHeight = singleLineShadow.scrollHeight;
let newHeight = shadow.scrollHeight;

// Guarding for jsdom, where scrollHeight isn't present.
// See https://github.com/tmpvar/jsdom/issues/1013
if (newHeight === undefined) return;

props = props || this.props;

if (props.rowsMax >= props.rows) {
newHeight = Math.min(props.rowsMax * lineHeight, newHeight);
}

newHeight = Math.max(newHeight, lineHeight);

if (this.state.height !== newHeight) {
this.setState({
height: newHeight,
});

if (props.onHeightChange) {
props.onHeightChange(event, newHeight);
}
}
}

handleChange = (event) => {
const value = event.target.value;
this.syncHeightWithShadow(value);
this.value = value;
if (this.props.onChange) {
this.props.onChange(event);
}
};

render() {
const {
onChange,
onHeightChange,
rows,
rowsMax,
hintText,
className,
...other
} = this.props;

const { styleManager } = this.context;
const classes = styleManager.render(styleSheet);

return (
<div className={classnames(classes.root, className)}>
<EventListener target="window" onResize={this.handleResize} />
<textarea
ref={(c) => { this.singleLineShadow = c; }}
className={classnames(classes.shadow, classes.textarea)}
tabIndex="-1"
rows={1}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rows="1"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rows is a PropTypes.number -- do you think it'd be better to make a PropTypes.string?

readOnly
value={''}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we should add aria-hidden="true".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call

/>
<textarea
ref={(c) => { this.shadow = c; }}
className={classnames(classes.shadow, classes.textarea)}
tabIndex="-1"
rows={this.props.rows}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rows={rows}

defaultValue={this.props.defaultValue}
readOnly
value={this.props.value}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we should add aria-hidden="true".

/>
<textarea
{...other}
ref={(c) => { this.input = c; }}
rows={this.props.rows}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rows={rows}

className={classes.textarea}
style={{ height: this.state.height }}
onChange={this.handleChange}
/>
</div>
);
}
}
63 changes: 63 additions & 0 deletions src/Input/AutoResizingTextArea.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// @flow weak

import React from 'react';
import { assert } from 'chai';
import { createShallow } from 'src/test-utils';
import AutoResizingTextArea from './AutoResizingTextArea';

describe('<AutoResizingTextArea />', () => {
let shallow;

before(() => {
shallow = createShallow();
});

it('should render 3 textareas', () => {
const wrapper = shallow(<AutoResizingTextArea multiline />);
assert.strictEqual(wrapper.find('textarea').length, 3);
});
it('should change its height when the height of its shadows changes', () => {
const wrapper = shallow(<AutoResizingTextArea multiline onChange={(() => {})} />);
assert.strictEqual(wrapper.state().height, 24);

// refs don't work with shallow renders in enzyme so here we directly define
// 'this.input', 'this.shadow', etc. for this AutoResizingTextArea via wrapper.instance()
const textArea = wrapper.find('textarea').last();
wrapper.instance().input = textArea;
const shadow = wrapper.find('textarea').at(2);
wrapper.instance().shadow = shadow;
const singleLineShandow = wrapper.find('textarea').first();
wrapper.instance().singleLineShadow = singleLineShandow;

// jsdom doesn't support scroll height so we have to simulate it changing
// which makes this not so great of a test :(
shadow.scrollHeight = 43;
singleLineShandow.scrollHeight = 43;
textArea.simulate('change', { target: { value: 'x' } }); // this is needed to trigger the resize
assert.strictEqual(wrapper.state().height, 43);

shadow.scrollHeight = 24;
singleLineShandow.scrollHeight = 24;
textArea.simulate('change', { target: { value: '' } });
assert.strictEqual(wrapper.state().height, 24);
});

it('should set dirty', () => {
const wrapper = shallow(<AutoResizingTextArea multiline />);
assert.strictEqual(wrapper.find('textarea').length, 3);

// refs don't work with shallow renders in enzyme so here we directly define
// 'this.input', 'this.shadow', etc. for this AutoResizingTextArea via wrapper.instance()
const textArea = wrapper.find('textarea').last();
wrapper.instance().input = textArea;
const shadow = wrapper.find('textarea').at(2);
wrapper.instance().shadow = shadow;
const singleLineShandow = wrapper.find('textarea').first();
wrapper.instance().singleLineShadow = singleLineShandow;

textArea.simulate('change', { target: { value: 'x' } }); // this is needed to trigger the resize
assert.strictEqual(wrapper.instance().value, 'x');
textArea.simulate('change', { target: { value: '' } }); // this is needed to trigger the resize
assert.strictEqual(wrapper.instance().value, '');
});
});
Loading