diff options
author | Alexander Tseung <alextsg@users.noreply.github.com> | 2018-10-17 07:03:29 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-10-17 07:03:29 +0800 |
commit | badebe017fe28b58ac742082368484c3a4b1c1bc (patch) | |
tree | b0810d259ea104a11051dd48dc1038a9c2d82310 /ui/app/components/unit-input | |
parent | 8bccb88132447882f81105a0033a4f199200714f (diff) | |
download | tangerine-wallet-browser-badebe017fe28b58ac742082368484c3a4b1c1bc.tar.gz tangerine-wallet-browser-badebe017fe28b58ac742082368484c3a4b1c1bc.tar.zst tangerine-wallet-browser-badebe017fe28b58ac742082368484c3a4b1c1bc.zip |
Adds toggle for primary currency (#5421)
* Add UnitInput component
* Add CurrencyInput component
* Add UserPreferencedCurrencyInput component
* Add UserPreferencedCurrencyDisplay component
* Add updatePreferences action
* Add styles for CurrencyInput, CurrencyDisplay, and UnitInput
* Update SettingsTab page with Primary Currency toggle
* Refactor currency displays and inputs to use UserPreferenced displays and inputs
* Add TokenInput component
* Add UserPreferencedTokenInput component
* Use TokenInput in the send screen
* Fix unit tests
* Fix e2e and integration tests
* Remove send/CurrencyDisplay component
* Replace diamond unicode character with Eth logo. Fix typos
Diffstat (limited to 'ui/app/components/unit-input')
-rw-r--r-- | ui/app/components/unit-input/index.js | 1 | ||||
-rw-r--r-- | ui/app/components/unit-input/index.scss | 44 | ||||
-rw-r--r-- | ui/app/components/unit-input/tests/unit-input.component.test.js | 146 | ||||
-rw-r--r-- | ui/app/components/unit-input/unit-input.component.js | 104 |
4 files changed, 295 insertions, 0 deletions
diff --git a/ui/app/components/unit-input/index.js b/ui/app/components/unit-input/index.js new file mode 100644 index 000000000..7c33c9e5c --- /dev/null +++ b/ui/app/components/unit-input/index.js @@ -0,0 +1 @@ +export { default } from './unit-input.component' diff --git a/ui/app/components/unit-input/index.scss b/ui/app/components/unit-input/index.scss new file mode 100644 index 000000000..28c5bf6f0 --- /dev/null +++ b/ui/app/components/unit-input/index.scss @@ -0,0 +1,44 @@ +.unit-input { + min-height: 54px; + border: 1px solid #dedede; + border-radius: 4px; + background-color: #fff; + color: #4d4d4d; + font-size: 1rem; + padding: 8px 10px; + position: relative; + + input[type="number"] { + -moz-appearance: textfield; + } + + input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + input[type="number"]:hover::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: none; + display: none; + } + + &__input { + color: #4d4d4d; + font-size: 1rem; + font-family: Roboto; + border: none; + outline: 0 !important; + max-width: 22ch; + } + + &__input-container { + display: flex; + align-items: center; + } + + &--error { + border-color: $red; + } +} diff --git a/ui/app/components/unit-input/tests/unit-input.component.test.js b/ui/app/components/unit-input/tests/unit-input.component.test.js new file mode 100644 index 000000000..97d987bc7 --- /dev/null +++ b/ui/app/components/unit-input/tests/unit-input.component.test.js @@ -0,0 +1,146 @@ +import React from 'react' +import assert from 'assert' +import { shallow, mount } from 'enzyme' +import sinon from 'sinon' +import UnitInput from '../unit-input.component' + +describe('UnitInput Component', () => { + describe('rendering', () => { + it('should render properly without a suffix', () => { + const wrapper = shallow( + <UnitInput /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input__suffix').length, 0) + }) + + it('should render properly with a suffix', () => { + const wrapper = shallow( + <UnitInput + suffix="ETH" + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input__suffix').length, 1) + assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH') + }) + + it('should render properly with a child omponent', () => { + const wrapper = shallow( + <UnitInput> + <div className="testing"> + TESTCOMPONENT + </div> + </UnitInput> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.testing').length, 1) + assert.equal(wrapper.find('.testing').text(), 'TESTCOMPONENT') + }) + + it('should render with an error class when props.error === true', () => { + const wrapper = shallow( + <UnitInput + error + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.find('.unit-input--error').length, 1) + }) + }) + + describe('handling actions', () => { + const handleChangeSpy = sinon.spy() + const handleBlurSpy = sinon.spy() + + afterEach(() => { + handleChangeSpy.resetHistory() + handleBlurSpy.resetHistory() + }) + + it('should focus the input on component click', () => { + const wrapper = mount( + <UnitInput /> + ) + + assert.ok(wrapper) + const handleFocusSpy = sinon.spy(wrapper.instance(), 'handleFocus') + wrapper.instance().forceUpdate() + wrapper.update() + assert.equal(handleFocusSpy.callCount, 0) + wrapper.find('.unit-input').simulate('click') + assert.equal(handleFocusSpy.callCount, 1) + }) + + it('should call onChange on input changes with the value', () => { + const wrapper = mount( + <UnitInput + onChange={handleChangeSpy} + /> + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + const input = wrapper.find('input') + input.simulate('change', { target: { value: 123 } }) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith(123)) + assert.equal(wrapper.state('value'), 123) + }) + + it('should call onBlur on blur with the value', () => { + const wrapper = mount( + <UnitInput + onChange={handleChangeSpy} + onBlur={handleBlurSpy} + /> + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + assert.equal(handleBlurSpy.callCount, 0) + const input = wrapper.find('input') + input.simulate('change', { target: { value: 123 } }) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith(123)) + assert.equal(wrapper.state('value'), 123) + input.simulate('blur') + assert.equal(handleBlurSpy.callCount, 1) + assert.ok(handleBlurSpy.calledWith(123)) + }) + + it('should set the component state value with props.value', () => { + const wrapper = mount( + <UnitInput + value={123} + /> + ) + + assert.ok(wrapper) + assert.equal(wrapper.state('value'), 123) + }) + + it('should update the component state value with props.value', () => { + const wrapper = mount( + <UnitInput + onChange={handleChangeSpy} + /> + ) + + assert.ok(wrapper) + assert.equal(handleChangeSpy.callCount, 0) + const input = wrapper.find('input') + input.simulate('change', { target: { value: 123 } }) + assert.equal(wrapper.state('value'), 123) + assert.equal(handleChangeSpy.callCount, 1) + assert.ok(handleChangeSpy.calledWith(123)) + wrapper.setProps({ value: 456 }) + assert.equal(wrapper.state('value'), 456) + assert.equal(handleChangeSpy.callCount, 1) + }) + }) +}) diff --git a/ui/app/components/unit-input/unit-input.component.js b/ui/app/components/unit-input/unit-input.component.js new file mode 100644 index 000000000..f1ebf4d77 --- /dev/null +++ b/ui/app/components/unit-input/unit-input.component.js @@ -0,0 +1,104 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { removeLeadingZeroes } from '../send/send.utils' + +/** + * Component that attaches a suffix or unit of measurement trailing user input, ex. 'ETH'. Also + * allows rendering a child component underneath the input to, for example, display conversions of + * the shown suffix. + */ +export default class UnitInput extends PureComponent { + static propTypes = { + children: PropTypes.node, + error: PropTypes.bool, + onBlur: PropTypes.func, + onChange: PropTypes.func, + placeholder: PropTypes.string, + suffix: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + } + + static defaultProps = { + placeholder: '0', + } + + constructor (props) { + super(props) + + this.state = { + value: props.value || '', + } + } + + componentDidUpdate (prevProps) { + const { value: prevPropsValue } = prevProps + const { value: propsValue } = this.props + const { value: stateValue } = this.state + + if (prevPropsValue !== propsValue && propsValue !== stateValue) { + this.setState({ value: propsValue }) + } + } + + handleFocus = () => { + this.unitInput.focus() + } + + handleChange = event => { + const { value: userInput } = event.target + let value = userInput + + if (userInput.length && userInput.length > 1) { + value = removeLeadingZeroes(userInput) + } + + this.setState({ value }) + this.props.onChange(value) + } + + handleBlur = event => { + const { onBlur } = this.props + typeof onBlur === 'function' && onBlur(this.state.value) + } + + getInputWidth (value) { + const valueString = String(value) + const valueLength = valueString.length || 1 + const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0 + return (valueLength + decimalPointDeficit + 0.75) + 'ch' + } + + render () { + const { error, placeholder, suffix, children } = this.props + const { value } = this.state + + return ( + <div + className={classnames('unit-input', { 'unit-input--error': error })} + onClick={this.handleFocus} + > + <div className="unit-input__input-container"> + <input + type="number" + className="unit-input__input" + value={value} + placeholder={placeholder} + onChange={this.handleChange} + onBlur={this.handleBlur} + style={{ width: this.getInputWidth(value) }} + ref={ref => { this.unitInput = ref }} + /> + { + suffix && ( + <div className="unit-input__suffix"> + { suffix } + </div> + ) + } + </div> + { children } + </div> + ) + } +} |