Browse Source

Merge pull request #705 from mrfelton/feat/extract-amount-input

feat(components): add new AmountInput component
renovate/lint-staged-8.x
JimmyMow 7 years ago
committed by GitHub
parent
commit
a8f738b5ac
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 164
      app/components/AmountInput/AmountInput.js
  2. 0
      app/components/AmountInput/AmountInput.scss
  3. 3
      app/components/AmountInput/index.js
  4. 16
      app/components/Contacts/SubmitChannelForm.js
  5. 17
      app/components/Form/Pay.js
  6. 10
      app/components/Form/Request.js
  7. 2
      app/routes/app/containers/AppContainer.js
  8. 251
      test/unit/components/AmountInput.spec.js
  9. 8
      test/unit/components/Form.spec.js
  10. 8
      test/unit/components/Form/Pay.spec.js
  11. 4
      test/unit/components/Form/Request.spec.js

164
app/components/AmountInput/AmountInput.js

@ -0,0 +1,164 @@
import React from 'react'
import PropTypes from 'prop-types'
class AmountInput extends React.Component {
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
this.handleBlur = this.handleBlur.bind(this)
this.handleKeyDown = this.handleKeyDown.bind(this)
}
setRules() {
const { currency } = this.props
switch (currency) {
case 'btc':
this.rules = {
precision: 8,
placeholder: '0.00000000',
pattern: '[0-9.]*'
}
break
case 'bits':
this.rules = {
precision: 2,
placeholder: '0.00',
pattern: '[0-9.]*'
}
break
case 'sats':
this.rules = {
precision: 0,
placeholder: '00000000',
pattern: '[0-9]*'
}
break
default:
this.rules = {
precision: 2,
pattern: '[0-9]*'
}
}
}
parseNumber(_value) {
let value = _value || ''
if (typeof _value === 'string') {
value = _value.replace(/[^0-9.]/g, '')
}
let integer = null
let fractional = null
if (value * 1.0 < 0) {
value = '0.0'
}
// pearse integer and fractional value so that we can reproduce the same string value afterwards
// [0, 0] === 0.0
// [0, ''] === 0.
// [0, null] === 0
if (value.match(/^[0-9]*\.[0-9]*$/)) {
;[integer, fractional] = value.toString().split(/\./)
if (!fractional) {
fractional = ''
}
} else {
integer = value
}
// Limit fractional precision to the correct number of decimal places.
if (fractional && fractional.length > this.rules.precision) {
fractional = fractional.substring(0, this.rules.precision)
}
return [integer, fractional]
}
formatValue(integer, fractional) {
let value
if (fractional && fractional.length > 0) {
value = `${integer}.${fractional}`
} else {
// Empty string means `XYZ.` instead of just plain `XYZ`.
if (fractional === '') {
value = `${integer}.`
} else {
value = `${integer}`
}
}
return value
}
handleChange(e) {
let { value } = e.target
const [integer, fractional] = this.parseNumber(value)
value = this.formatValue(integer, fractional)
const { onChangeEvent } = this.props
if (onChangeEvent) {
onChangeEvent(value)
}
}
handleBlur(e) {
let { value } = e.target
const [integer, fractional] = this.parseNumber(value)
value = this.formatValue(integer, fractional)
const { onBlurEvent } = this.props
if (onBlurEvent) {
onBlurEvent(value)
}
}
handleKeyDown(e) {
// Do nothing if the user did select all key combo.
if (e.metaKey && e.key === 'a') {
return
}
// Do not allow multiple dots.
let { value } = e.target
if (e.key === '.') {
if (value.search(/\./) >= 0) {
e.preventDefault()
}
return
}
if (e.key.length === 1 && !e.key.match(/^[0-9.]$/)) {
e.preventDefault()
return
}
}
render() {
let { amount = '', readOnly } = this.props
if (amount === null) {
amount = ''
}
this.setRules()
return (
<input
id="amount"
onChange={this.handleChange}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
readOnly={readOnly}
type="text"
required
value={amount}
pattern={this.rules.pattern}
placeholder={this.rules.placeholder}
/>
)
}
}
AmountInput.propTypes = {
amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
currency: PropTypes.string.isRequired,
onChangeEvent: PropTypes.func,
onBlurEvent: PropTypes.func,
readOnly: PropTypes.bool
}
export default AmountInput

0
app/components/AmountInput/AmountInput.scss

3
app/components/AmountInput/index.js

@ -0,0 +1,3 @@
import AmountInput from './AmountInput'
export default AmountInput

16
app/components/Contacts/SubmitChannelForm.js

@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import FaAngleDown from 'react-icons/lib/fa/angle-down'
import AmountInput from 'components/AmountInput'
import styles from './SubmitChannelForm.scss'
class SubmitChannelForm extends React.Component {
@ -16,6 +17,8 @@ class SubmitChannelForm extends React.Component {
updateContactCapacity,
openChannel,
ticker,
toggleCurrencyProps: {
setContactsCurrencyFilters,
showCurrencyFilters,
@ -73,14 +76,11 @@ class SubmitChannelForm extends React.Component {
<section className={styles.amount}>
<div className={styles.input}>
<input
type="number"
min="0"
size=""
placeholder="0.00000000"
value={contactCapacity || ''}
onChange={event => updateContactCapacity(event.target.value)}
<AmountInput
id="amount"
amount={contactCapacity}
currency={ticker.currency}
onChangeEvent={updateContactCapacity}
/>
<div className={styles.currency}>
<section
@ -127,6 +127,8 @@ SubmitChannelForm.propTypes = {
updateContactCapacity: PropTypes.func.isRequired,
openChannel: PropTypes.func.isRequired,
ticker: PropTypes.object.isRequired,
toggleCurrencyProps: PropTypes.object.isRequired
}

17
app/components/Form/Pay.js

@ -7,6 +7,7 @@ import link from 'icons/link.svg'
import FaAngleDown from 'react-icons/lib/fa/angle-down'
import { btc } from 'lib/utils'
import AmountInput from 'components/AmountInput'
import styles from './Pay.scss'
@ -132,18 +133,12 @@ class Pay extends Component {
<span />
</div>
<div className={styles.bottom}>
<input
type="number"
min="0"
ref={input => {
this.amountInput = input
}}
size=""
placeholder="0.00000000"
value={currentAmount || ''}
onChange={event => setPayAmount(event.target.value)}
onBlur={onPayAmountBlur}
<AmountInput
id="amount"
amount={currentAmount}
currency={ticker.currency}
onChangeEvent={setPayAmount}
onBlurEvent={onPayAmountBlur}
readOnly={isLn}
/>
<div className={styles.currency}>

10
app/components/Form/Request.js

@ -6,6 +6,7 @@ import hand from 'icons/hand.svg'
import FaAngleDown from 'react-icons/lib/fa/angle-down'
import { btc } from 'lib/utils'
import AmountInput from 'components/AmountInput'
import styles from './Request.scss'
const Request = ({
@ -45,12 +46,11 @@ const Request = ({
<span />
</div>
<div className={styles.bottom}>
<input
type="number"
value={amount || ''}
onChange={event => setRequestAmount(event.target.value)}
<AmountInput
id="amount"
placeholder="0.00000000"
amount={amount}
currency={ticker.currency}
onChangeEvent={setRequestAmount}
/>
<div className={styles.currency}>
<section

2
app/routes/app/containers/AppContainer.js

@ -409,6 +409,8 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
openChannel: dispatchProps.openChannel,
ticker: stateProps.ticker,
toggleCurrencyProps: {
currentCurrencyFilters: stateProps.currentCurrencyFilters,
currencyName: stateProps.currencyName,

251
test/unit/components/AmountInput.spec.js

@ -0,0 +1,251 @@
import React from 'react'
import { configure, mount } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
import AmountInput from 'components/AmountInput'
configure({ adapter: new Adapter() })
let component
let wrapped
let input
const defaultProps = {
amount: '',
currency: '',
onChange: () => {}
}
const mountInput = props => {
wrapped = mount(<AmountInput {...defaultProps} {...props} />)
input = wrapped.find('input')
component = wrapped.instance()
}
describe('AmountInput', () => {
describe('render', () => {
it('renders amount', () => {
mountInput({ amount: '0.1234' })
expect(input.props().value).toEqual('0.1234')
})
})
describe('input properties', () => {
it('passes readOnly to input', () => {
mountInput({ readOnly: true })
expect(input.props().readOnly).toEqual(true)
})
})
describe('parseNumber', () => {
const test = (from, to) => {
expect(component.parseNumber(from)).toEqual(to)
}
beforeEach(() => mountInput())
it('splits number into integer and fraction', () => {
test(null, ['', null])
test('', ['', null])
test('0', ['0', null])
test('00', ['00', null])
test('0.', ['0', ''])
test('1', ['1', null])
test('01', ['01', null])
test('1.', ['1', ''])
test('0.0', ['0', '0'])
test('0.1', ['0', '1'])
test('0.01', ['0', '01'])
test('0.10', ['0', '10'])
test('1.0', ['1', '0'])
test('01.0', ['01', '0'])
})
describe('bitcoin', () => {
beforeEach(() => {
let props = { ...defaultProps, currency: 'btc' }
component = mount(<AmountInput {...props} />).instance()
})
it('has a precision of 8', () => {
test('1.1', ['1', '1'])
test('1.12', ['1', '12'])
test('1.123', ['1', '123'])
test('1.1234', ['1', '1234'])
test('1.12345', ['1', '12345'])
test('1.123456', ['1', '123456'])
test('1.1234567', ['1', '1234567'])
test('1.12345678', ['1', '12345678'])
test('1.123456789', ['1', '12345678'])
test('1.1234567890', ['1', '12345678'])
})
})
describe('bits', () => {
beforeEach(() => {
let props = { ...defaultProps, currency: 'bits' }
component = mount(<AmountInput {...props} />).instance()
})
it('has a precision of 2', () => {
test('1.1', ['1', '1'])
test('1.12', ['1', '12'])
test('1.123', ['1', '12'])
test('1.1234', ['1', '12'])
})
})
describe('sats', () => {
beforeEach(() => {
let props = { ...defaultProps, currency: 'sats' }
component = mount(<AmountInput {...props} />).instance()
})
it('has a precision of 0', () => {
test('1.1', ['1', ''])
test('1.12', ['1', ''])
})
})
})
describe('formatValue', () => {
const component = mount(<AmountInput {...defaultProps} />).instance()
const test = (from, to) => {
expect(component.formatValue(from[0], from[1])).toEqual(to)
}
it('turns parsed number into a string', () => {
test(['0', null], '0')
test(['00', null], '00')
test(['0', ''], '0.')
test(['1', null], '1')
test(['01', null], '01')
test(['1', ''], '1.')
test(['0', '0'], '0.0')
test(['0', '1'], '0.1')
test(['0', '01'], '0.01')
test(['0', '10'], '0.10')
test(['1', '0'], '1.0')
test(['01', '0'], '01.0')
})
})
describe('handleChange', () => {
let onChangeEvent
let component
beforeEach(() => {
onChangeEvent = jest.fn()
let props = { ...defaultProps, onChangeEvent }
component = mount(<AmountInput {...props} />).instance()
})
it('sends value to parseNumber', () => {
jest.spyOn(component, 'parseNumber')
component.handleChange({ target: { value: '123.0' } })
expect(component.parseNumber).toHaveBeenCalledTimes(1)
expect(component.parseNumber).toHaveBeenCalledWith('123.0')
})
it('calls formatValue with presult from parseNumber', () => {
jest.spyOn(component, 'formatValue')
component.handleChange({ target: { value: '123.0' } })
expect(component.formatValue).toHaveBeenCalledTimes(1)
expect(component.formatValue).toHaveBeenCalledWith('123', '0')
})
it('calls props.onChange with formatValue result', () => {
jest.spyOn(component, 'formatValue').mockReturnValue('456')
component.handleChange({ target: { value: '123.0' } })
expect(onChangeEvent).toHaveBeenCalledWith('456')
})
})
describe('handleBlur', () => {
let onBlurEvent
let component
beforeEach(() => {
onBlurEvent = jest.fn()
let props = { ...defaultProps, onBlurEvent }
component = mount(<AmountInput {...props} />).instance()
})
it('calls props.onBlurEvent with formatValue result', () => {
jest.spyOn(component, 'formatValue').mockReturnValue('456')
component.handleBlur({ target: { value: '123.0' } })
expect(onBlurEvent).toHaveBeenCalledWith('456')
})
})
describe('handleKeyDown', () => {
let event
let preventDefault
beforeEach(() => {
wrapped = mount(<AmountInput {...defaultProps} />)
component = wrapped.instance()
preventDefault = jest.fn()
})
describe('dot', () => {
beforeEach(() => {
wrapped = mount(<AmountInput {...defaultProps} />)
component = wrapped.instance()
preventDefault = jest.fn()
event = {
key: '.',
target: { value: '123' },
preventDefault
}
})
it('allows adding dot to a number that has no dot', () => {
component.handleKeyDown({ ...event, target: { value: '123' } }) // no dot
expect(preventDefault).not.toHaveBeenCalled()
})
it('does not allow multiple dots in the number', () => {
component.handleKeyDown({ ...event, target: { value: '123.' } }) // has a dot
expect(preventDefault).toHaveBeenCalled()
})
})
describe('Non-supported keys', () => {
beforeEach(() => {
wrapped = mount(<AmountInput {...defaultProps} />)
component = wrapped.instance()
preventDefault = jest.fn()
event = {
key: '.',
target: { value: '123' },
preventDefault
}
})
it('prevents event', () => {
component.handleKeyDown({ ...event, key: 'b' })
expect(preventDefault).toHaveBeenCalled()
component.handleKeyDown({ ...event, key: 'B' })
expect(preventDefault).toHaveBeenCalled()
component.handleKeyDown({ ...event, key: '-' })
expect(preventDefault).toHaveBeenCalled()
component.handleKeyDown({ ...event, key: '?' })
expect(preventDefault).toHaveBeenCalled()
})
})
})
})

8
test/unit/components/Form.spec.js

@ -15,10 +15,12 @@ const payFormProps = {
invoice: {},
showErrors: {}
},
currency: {},
crypto: {},
currency: '',
crypto: '',
nodes: [],
ticker: {},
ticker: {
currency: 'btc'
},
isOnchain: false,
isLn: true,

8
test/unit/components/Form/Pay.spec.js

@ -13,10 +13,12 @@ const defaultProps = {
invoice: {},
showErrors: {}
},
currency: {},
crypto: {},
currency: '',
crypto: '',
nodes: [],
ticker: {},
ticker: {
currency: 'btc'
},
isOnchain: false,
isLn: true,

4
test/unit/components/Form/Request.spec.js

@ -8,7 +8,9 @@ configure({ adapter: new Adapter() })
const defaultProps = {
requestform: {},
ticker: {},
ticker: {
currency: 'btc'
},
currentCurrencyFilters: [],
showCurrencyFilters: true,

Loading…
Cancel
Save