Skip to content

Commit

Permalink
feat(TextArea): add autoHeight prop (#1083)
Browse files Browse the repository at this point in the history
* feat(982): add autoHeight proporty for TextArea

* feat(982): recalculate height when the autoHeight is set and component did mount

* feat(982): test cases for onInput and onBlur props

* docs(TextArea): add autoHeight example

* feat(TextArea): handle changing autoHeight

* test(TextArea): add autoHeight tests

* test(TextArea): account for CI CSS rendering

* test(TextArea): assert height within a range
  • Loading branch information
luski authored and levithomason committed Dec 27, 2016
1 parent 9281947 commit 3b59d6c
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Form, TextArea } from 'semantic-ui-react'

const TextAreaExample = () => (
<Form>
<TextArea placeholder='Tell us more' rows='4' />
<TextArea placeholder='Tell us more' />
</Form>
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'
import { Form, TextArea } from 'semantic-ui-react'

const TextAreaExampleAutoHeight = () => (
<Form>
<TextArea placeholder='Try adding multiple lines' autoHeight />
</Form>
)

export default TextAreaExampleAutoHeight
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ const TextAreaTypesExamples = () => (
<ComponentExample
title='TextArea'
description='A default TextArea.'
examplePath='addons/TextArea/Types/TextAreaExample'
examplePath='addons/TextArea/Usage/TextAreaExample'
/>
<ComponentExample
title='Auto Height'
description='A TextArea can adjust its height to fit its contents.'
examplePath='addons/TextArea/Usage/TextAreaExampleAutoHeight'
/>
</ExampleSection>
)
Expand Down
4 changes: 2 additions & 2 deletions docs/app/Examples/addons/TextArea/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react'
import Types from './Types'
import Usage from './Usage'

const TextAreaExamples = () => (
<div>
<Types />
<Usage />
</div>
)

Expand Down
64 changes: 58 additions & 6 deletions src/addons/TextArea/TextArea.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import {
} from '../../lib'

/**
* A simple <textarea> wrapper for use in Form.TextArea.
* We may add more features to the TextArea in the future.
* A TextArea can be used to allow for extended user input.
* @see Form
*/
class TextArea extends Component {
Expand All @@ -21,30 +20,83 @@ class TextArea extends Component {
/** An element type to render as (string or function). */
as: customPropTypes.as,

/** Indicates whether height of the textarea fits the content or not */
autoHeight: PropTypes.bool,

/**
* Called on change.
* @param {SyntheticEvent} event - The React SyntheticEvent object
* @param {object} data - All props and the event value.
*/
onChange: PropTypes.func,

/** The value of the textarea. */
value: PropTypes.string,
}

static defaultProps = {
as: 'textarea',
}

componentDidMount() {
this.updateHeight()
}

componentDidUpdate(prevProps, prevState) {
// removed autoHeight
if (!this.props.autoHeight && prevProps.autoHeight) {
this.removeAutoHeightStyles()
}
// added autoHeight or value changed
if (this.props.autoHeight && !prevProps.autoHeight || prevProps.value !== this.props.value) {
this.updateHeight()
}
}

handleChange = (e) => {
const { onChange } = this.props
if (onChange) {
onChange(e, { ...this.props, value: e.target && e.target.value })
}
if (onChange) onChange(e, { ...this.props, value: e.target && e.target.value })

this.updateHeight(e.target)
}

removeAutoHeightStyles = () => {
this.rootNode.removeAttribute('rows')
this.rootNode.style.height = null
this.rootNode.style.minHeight = null
this.rootNode.style.resize = null
}

updateHeight = () => {
if (!this.rootNode) return

const { autoHeight } = this.props
if (!autoHeight) return

let { borderTopWidth, borderBottomWidth } = window.getComputedStyle(this.rootNode)
borderTopWidth = parseInt(borderTopWidth, 10)
borderBottomWidth = parseInt(borderBottomWidth, 10)

this.rootNode.rows = '1'
this.rootNode.style.minHeight = '0'
this.rootNode.style.resize = 'none'
this.rootNode.style.height = 'auto'
this.rootNode.style.height = (this.rootNode.scrollHeight + borderTopWidth + borderBottomWidth) + 'px'
}

render() {
const { value } = this.props
const rest = getUnhandledProps(TextArea, this.props)
const ElementType = getElementType(TextArea, this.props)

return <ElementType {...rest} onChange={this.handleChange} />
return (
<ElementType
{...rest}
value={value}
onChange={this.handleChange}
ref={c => (this.rootNode = c)}
/>
)
}
}

Expand Down
100 changes: 99 additions & 1 deletion test/specs/addons/TextArea-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,36 @@ import TextArea from 'src/addons/TextArea/TextArea'
import { sandbox } from 'test/utils'
import * as common from '../commonTests'

// ----------------------------------------
// Wrapper
// ----------------------------------------
// we need to unmount the dropdown after every test to ensure all event listeners are cleaned up
// wrap the render methods to update a global wrapper that is unmounted after each test
let attachTo
let wrapper
const wrapperMount = (element, opts) => {
attachTo = document.createElement('div')
document.body.appendChild(attachTo)

wrapper = mount(element, { ...opts, attachTo })
return wrapper
}
const wrapperShallow = (...args) => (wrapper = shallow(...args))

describe('TextArea', () => {
beforeEach(() => {
attachTo = undefined
wrapper = undefined
})

afterEach(() => {
if (wrapper) {
if (wrapper.unmount) wrapper.unmount()
if (wrapper.detach) wrapper.detach()
}
if (attachTo) document.body.removeChild(attachTo)
})

common.isConformant(TextArea, {
eventTargets: {
onChange: 'textarea',
Expand All @@ -17,12 +46,81 @@ describe('TextArea', () => {
const e = { target: { value: 'name' } }
const props = { 'data-foo': 'bar', onChange: spy }

const wrapper = shallow(<TextArea {...props} />)
wrapperShallow(<TextArea {...props} />)

wrapper.find('textarea').simulate('change', e)

spy.should.have.been.calledOnce()
spy.should.have.been.calledWithMatch(e, { ...props, value: e.target.value })
})
})

describe('autoHeight', () => {
// simplify styles to make height assertions easier
const style = { padding: 0, fontSize: '10px', lineHeight: 1, border: 'none' }

const assertHeight = (height) => {
const element = document.querySelector('textarea')

if (!height) {
element.should.not.have.property('rows', 1)
element.style.should.have.property('minHeight', '')
element.style.should.have.property('resize', '')
element.style.should.have.property('height', '')
return
}

element.should.have.property('rows', 1)
element.style.should.have.property('minHeight', '0px')
element.style.should.have.property('resize', 'none')

// CI renders textareas with an extra pixel
// assert height with a margin of error of one pixel
const parsedHeight = parseInt(height, 10)
parseInt(element.style.height, 10).should.be.within(parsedHeight - 1, parsedHeight + 1)
}

it('sets styles when true', () => {
wrapperMount(<TextArea style={style} autoHeight />)

assertHeight('10px') // 1 line
})
it('sets styles when there is a multiline value', () => {
wrapperMount(<TextArea style={style} autoHeight value={'line1\nline2\nline3'} />)

assertHeight('30px') // 3 lines
})
it('does not set styles when not set', () => {
wrapperMount(<TextArea style={style} />)

assertHeight('') // no height
})
it('updates the height on change', () => {
wrapperMount(<TextArea style={style} autoHeight />)

// initial height
const element = document.querySelector('textarea')
element.style.height.should.equal('10px')

// update the value and fire a change event
element.value = 'line1\nline2\nline3'
wrapper.simulate('change')

assertHeight('30px') // 3 lines
})
it('adds styles when toggled to true', () => {
wrapperMount(<TextArea style={style} />)

wrapper.setProps({ autoHeight: true })

assertHeight('10px') // 1 line
})
it('removes styles when toggled to false', () => {
wrapperMount(<TextArea style={style} autoHeight />)

wrapper.setProps({ autoHeight: false })

assertHeight('') // no height
})
})
})

0 comments on commit 3b59d6c

Please sign in to comment.