aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/components/unit-input
diff options
context:
space:
mode:
authorAlexander Tseung <alextsg@users.noreply.github.com>2018-10-17 07:03:29 +0800
committerGitHub <noreply@github.com>2018-10-17 07:03:29 +0800
commitbadebe017fe28b58ac742082368484c3a4b1c1bc (patch)
treeb0810d259ea104a11051dd48dc1038a9c2d82310 /ui/app/components/unit-input
parent8bccb88132447882f81105a0033a4f199200714f (diff)
downloadtangerine-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.js1
-rw-r--r--ui/app/components/unit-input/index.scss44
-rw-r--r--ui/app/components/unit-input/tests/unit-input.component.test.js146
-rw-r--r--ui/app/components/unit-input/unit-input.component.js104
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>
+ )
+ }
+}