-
-
Notifications
You must be signed in to change notification settings - Fork 32.5k
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
Changes from 59 commits
d90eef5
e1bf713
ab6dfdd
a763d47
ac69a5b
050e3b1
0ed3174
126e94e
9456d66
9d18e60
9ad7d98
e9ede40
108c76f
c3d1de3
f0feccf
8df8a6f
31268d3
c17afe7
9d3635e
38608e9
3f9e522
ddd51d9
6bed551
94ac535
b3b0534
3812485
65e9441
2cd3fa7
31ed81a
99eddaf
2959331
8d6b599
fe7494f
1f74023
2a2577b
6b681ed
00c7e92
4bd4b41
dd21104
bbdd7db
1f6a0f9
7abc53b
b49e7b3
eb536d8
5b6d156
b8195a2
65e1924
b5fee37
d6a5ea1
5c66857
22e7650
ab8a3bf
62a615f
98df3d6
ed12ff3
da71e70
0fe9bbc
25647aa
3161cca
8f02f4e
a068813
929e6c7
3cde356
0b4509b
043d843
647d625
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -58,6 +58,21 @@ class TextFields extends Component { | |
className={classes.input} | ||
type="password" | ||
/> | ||
<TextField | ||
id="multiline flexible" | ||
label="MultiLine" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
multiline | ||
defaultValue="Default Value" | ||
className={classes.input} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about adding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good call |
||
/> | ||
<TextField | ||
id="multiline static" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think that it's a valid id. |
||
label="MultiLine" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
multiline | ||
rows={4} | ||
defaultValue="Default Value" | ||
className={classes.input} | ||
/> | ||
</div> | ||
); | ||
} | ||
|
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. marginBottom: -4, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But can be removed by (so you get the idea) -<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', There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's so that
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) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that we should debounce the events, for instance: |
||
this.syncHeightWithShadow(undefined, event); | ||
}; | ||
|
||
getInputNode() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think that we need that function indirection There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yea I agree |
||
return this.input; | ||
} | ||
|
||
setValue(value) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 !== ''); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rows="1" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rows is a |
||
readOnly | ||
value={''} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess we should add There was a problem hiding this comment. Choose a reason for hiding this commentThe 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} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rows={rows} |
||
defaultValue={this.props.defaultValue} | ||
readOnly | ||
value={this.props.value} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess we should add |
||
/> | ||
<textarea | ||
{...other} | ||
ref={(c) => { this.input = c; }} | ||
rows={this.props.rows} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
); | ||
} | ||
} |
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, ''); | ||
}); | ||
}); |
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch