1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Merge pull request #4386 from MetaMask/i4077-replace-currency-input-with-numeric-input

Replace currency-input.js with NumericInput
This commit is contained in:
Dan J Miller 2018-05-31 15:09:13 -02:30 committed by GitHub
commit 15f4ce352d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 108 additions and 156 deletions

View File

@ -142,6 +142,7 @@
"operator-linebreak": [1, "after", { "overrides": { "?": "ignore", ":": "ignore" } }], "operator-linebreak": [1, "after", { "overrides": { "?": "ignore", ":": "ignore" } }],
"padded-blocks": "off", "padded-blocks": "off",
"quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}], "quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}],
"react/no-deprecated": 0,
"semi": [2, "never"], "semi": [2, "never"],
"semi-spacing": [2, { "before": false, "after": true }], "semi-spacing": [2, { "before": false, "after": true }],
"space-before-blocks": [1, "always"], "space-before-blocks": [1, "always"],

12
package-lock.json generated
View File

@ -26436,6 +26436,18 @@
"object-assign": "4.1.1" "object-assign": "4.1.1"
} }
}, },
"react": {
"version": "15.6.2",
"resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz",
"integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=",
"requires": {
"create-react-class": "15.6.2",
"fbjs": "0.8.16",
"loose-envify": "1.3.1",
"object-assign": "4.1.1",
"prop-types": "15.6.1"
}
},
"react-hyperscript": { "react-hyperscript": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/react-hyperscript/-/react-hyperscript-2.4.2.tgz", "resolved": "https://registry.npmjs.org/react-hyperscript/-/react-hyperscript-2.4.2.tgz",

View File

@ -101,7 +101,7 @@ async function runSendFlowTest(assert, done) {
const sendAmountField = await queryAsync($, '.send-v2__form-row:eq(2)') const sendAmountField = await queryAsync($, '.send-v2__form-row:eq(2)')
sendAmountField.find('.currency-display')[0].click() sendAmountField.find('.currency-display')[0].click()
const sendAmountFieldInput = await findAsync(sendAmountField, 'input:text') const sendAmountFieldInput = await findAsync(sendAmountField, '.currency-display__input')
sendAmountFieldInput.val('5.1') sendAmountFieldInput.val('5.1')
reactTriggerChange(sendAmountField.find('input')[0]) reactTriggerChange(sendAmountField.find('input')[0])
@ -127,7 +127,7 @@ async function runSendFlowTest(assert, done) {
) )
await customizeGas(assert, 0, 21000, '0', '$0.00 USD') await customizeGas(assert, 0, 21000, '0', '$0.00 USD')
await customizeGas(assert, 500, 60000, '0.003', '$3.60 USD') await customizeGas(assert, 500, 60000, '0.03', '$36.03 USD')
const sendButton = await queryAsync($, 'button.btn-primary--lg.page-container__footer-button') const sendButton = await queryAsync($, 'button.btn-primary--lg.page-container__footer-button')
assert.equal(sendButton[0].textContent, 'Next', 'next button rendered') assert.equal(sendButton[0].textContent, 'Next', 'next button rendered')
@ -165,7 +165,7 @@ async function runSendFlowTest(assert, done) {
const sendAmountFieldInEdit = await queryAsync($, '.send-v2__form-row:eq(2)') const sendAmountFieldInEdit = await queryAsync($, '.send-v2__form-row:eq(2)')
sendAmountFieldInEdit.find('.currency-display')[0].click() sendAmountFieldInEdit.find('.currency-display')[0].click()
const sendAmountFieldInputInEdit = sendAmountFieldInEdit.find('input:text') const sendAmountFieldInputInEdit = sendAmountFieldInEdit.find('.currency-display__input')
sendAmountFieldInputInEdit.val('1.0') sendAmountFieldInputInEdit.val('1.0')
reactTriggerChange(sendAmountFieldInputInEdit[0]) reactTriggerChange(sendAmountFieldInputInEdit[0])

View File

@ -1,113 +0,0 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
module.exports = CurrencyInput
inherits(CurrencyInput, Component)
function CurrencyInput (props) {
Component.call(this)
const sanitizedValue = sanitizeValue(props.value)
this.state = {
value: sanitizedValue,
emptyState: false,
focused: false,
}
}
function removeNonDigits (str) {
return str.match(/\d|$/g).join('')
}
// Removes characters that are not digits, then removes leading zeros
function sanitizeInteger (val) {
return String(parseInt(removeNonDigits(val) || '0', 10))
}
function sanitizeDecimal (val) {
return removeNonDigits(val)
}
// Take a single string param and returns a non-negative integer or float as a string.
// Breaks the input into three parts: the integer, the decimal point, and the decimal/fractional part.
// Removes leading zeros from the integer, and non-digits from the integer and decimal
// The integer is returned as '0' in cases where it would be empty. A decimal point is
// included in the returned string if one is included in the param
// Examples:
// sanitizeValue('0') -> '0'
// sanitizeValue('a') -> '0'
// sanitizeValue('010.') -> '10.'
// sanitizeValue('0.005') -> '0.005'
// sanitizeValue('22.200') -> '22.200'
// sanitizeValue('.200') -> '0.200'
// sanitizeValue('a.b.1.c,89.123') -> '0.189123'
function sanitizeValue (value) {
let [ , integer, point, decimal] = (/([^.]*)([.]?)([^.]*)/).exec(value)
integer = sanitizeInteger(integer) || '0'
decimal = sanitizeDecimal(decimal)
return `${integer}${point}${decimal}`
}
CurrencyInput.prototype.handleChange = function (newValue) {
const { onInputChange } = this.props
const { value } = this.state
let parsedValue = newValue
const newValueLastIndex = newValue.length - 1
if (value === '0' && newValue[newValueLastIndex] === '0') {
parsedValue = parsedValue.slice(0, newValueLastIndex)
}
const sanitizedValue = sanitizeValue(parsedValue)
this.setState({
value: sanitizedValue,
emptyState: newValue === '' && sanitizedValue === '0',
})
onInputChange(sanitizedValue)
}
// If state.value === props.value plus a decimal point, or at least one
// zero or a decimal point and at least one zero, then this returns state.value
// after it is sanitized with getValueParts
CurrencyInput.prototype.getValueToRender = function () {
const { value } = this.props
const { value: stateValue } = this.state
const trailingStateString = (new RegExp(`^${value}(.+)`)).exec(stateValue)
const trailingDecimalAndZeroes = trailingStateString && (/^[.0]0*/).test(trailingStateString[1])
return sanitizeValue(trailingDecimalAndZeroes
? stateValue
: value)
}
CurrencyInput.prototype.render = function () {
const {
className,
placeholder,
readOnly,
inputRef,
type,
} = this.props
const { emptyState, focused } = this.state
const inputSizeMultiplier = readOnly ? 1 : 1.2
const valueToRender = this.getValueToRender()
return h('input', {
className,
type,
value: emptyState ? '' : valueToRender,
placeholder: focused ? '' : placeholder,
size: valueToRender.length * inputSizeMultiplier,
readOnly,
onFocus: () => this.setState({ focused: true, emptyState: valueToRender === '0' }),
onBlur: () => this.setState({ focused: false, emptyState: false }),
onChange: e => this.handleChange(e.target.value),
ref: inputRef,
})
}

View File

@ -1,7 +1,6 @@
const Component = require('react').Component const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const CurrencyInput = require('./currency-input')
const { const {
addCurrencies, addCurrencies,
conversionGTE, conversionGTE,
@ -51,14 +50,15 @@ InputNumber.prototype.render = function () {
const { unitLabel, step = 1, placeholder, value = 0 } = this.props const { unitLabel, step = 1, placeholder, value = 0 } = this.props
return h('div.customize-gas-input-wrapper', {}, [ return h('div.customize-gas-input-wrapper', {}, [
h(CurrencyInput, { h('input', {
className: 'customize-gas-input', className: 'customize-gas-input',
value, value,
placeholder, placeholder,
type: 'number', type: 'number',
onInputChange: newValue => { onChange: e => {
this.setValue(newValue) this.setValue(e.target.value)
}, },
min: 0,
}), }),
h('span.gas-tooltip-input-detail', {}, [unitLabel]), h('span.gas-tooltip-input-detail', {}, [unitLabel]),
h('div.gas-tooltip-input-arrows', {}, [ h('div.gas-tooltip-input-arrows', {}, [

View File

@ -1,7 +1,6 @@
const Component = require('react').Component const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const CurrencyInput = require('../currency-input')
const { conversionUtil, multiplyCurrencies } = require('../../conversion-util') const { conversionUtil, multiplyCurrencies } = require('../../conversion-util')
const currencyFormatter = require('currency-formatter') const currencyFormatter = require('currency-formatter')
const currencies = require('currency-formatter/currencies') const currencies = require('currency-formatter/currencies')
@ -21,21 +20,36 @@ function toHexWei (value) {
}) })
} }
CurrencyDisplay.prototype.componentWillMount = function () {
this.setState({
valueToRender: this.getValueToRender(this.props),
})
}
CurrencyDisplay.prototype.componentWillReceiveProps = function (nextProps) {
const currentValueToRender = this.getValueToRender(this.props)
const newValueToRender = this.getValueToRender(nextProps)
if (currentValueToRender !== newValueToRender) {
this.setState({
valueToRender: newValueToRender,
})
}
}
CurrencyDisplay.prototype.getAmount = function (value) { CurrencyDisplay.prototype.getAmount = function (value) {
const { selectedToken } = this.props const { selectedToken } = this.props
const { decimals } = selectedToken || {} const { decimals } = selectedToken || {}
const multiplier = Math.pow(10, Number(decimals || 0)) const multiplier = Math.pow(10, Number(decimals || 0))
const sendAmount = multiplyCurrencies(value, multiplier, {toNumericBase: 'hex'}) const sendAmount = multiplyCurrencies(value || '0', multiplier, {toNumericBase: 'hex'})
return selectedToken return selectedToken
? sendAmount ? sendAmount
: toHexWei(value) : toHexWei(value)
} }
CurrencyDisplay.prototype.getValueToRender = function () { CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversionRate, value }) {
const { selectedToken, conversionRate, value } = this.props if (value === '0x0') return '0'
const { decimals, symbol } = selectedToken || {} const { decimals, symbol } = selectedToken || {}
const multiplier = Math.pow(10, Number(decimals || 0)) const multiplier = Math.pow(10, Number(decimals || 0))
@ -76,6 +90,18 @@ CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValu
: convertedValue : convertedValue
} }
CurrencyDisplay.prototype.handleChange = function (newVal) {
this.setState({ valueToRender: newVal })
this.props.onChange(this.getAmount(newVal))
}
CurrencyDisplay.prototype.getInputWidth = function (valueToRender, readOnly) {
const valueString = String(valueToRender)
const valueLength = valueString.length || 1
const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0
return (valueLength + decimalPointDeficit + 0.75) + 'ch'
}
CurrencyDisplay.prototype.render = function () { CurrencyDisplay.prototype.render = function () {
const { const {
className = 'currency-display', className = 'currency-display',
@ -85,10 +111,10 @@ CurrencyDisplay.prototype.render = function () {
convertedCurrency, convertedCurrency,
readOnly = false, readOnly = false,
inError = false, inError = false,
handleChange, onBlur,
} = this.props } = this.props
const { valueToRender } = this.state
const valueToRender = this.getValueToRender()
const convertedValueToRender = this.getConvertedValueToRender(valueToRender) const convertedValueToRender = this.getConvertedValueToRender(valueToRender)
return h('div', { return h('div', {
@ -96,24 +122,30 @@ CurrencyDisplay.prototype.render = function () {
style: { style: {
borderColor: inError ? 'red' : null, borderColor: inError ? 'red' : null,
}, },
onClick: () => this.currencyInput && this.currencyInput.focus(), onClick: () => {
this.currencyInput && this.currencyInput.focus()
},
}, [ }, [
h('div.currency-display__primary-row', [ h('div.currency-display__primary-row', [
h('div.currency-display__input-wrapper', [ h('div.currency-display__input-wrapper', [
h(readOnly ? 'input' : CurrencyInput, { h('input', {
className: primaryBalanceClassName, className: primaryBalanceClassName,
value: `${valueToRender}`, value: `${valueToRender}`,
placeholder: '0', placeholder: '0',
type: 'number',
readOnly, readOnly,
...(!readOnly ? { ...(!readOnly ? {
onInputChange: newValue => { onChange: e => this.handleChange(e.target.value),
handleChange(this.getAmount(newValue)) onBlur: () => onBlur(this.getAmount(valueToRender)),
},
inputRef: input => { this.currencyInput = input },
} : {}), } : {}),
ref: input => { this.currencyInput = input },
style: {
width: this.getInputWidth(valueToRender, readOnly),
},
min: 0,
}), }),
h('span.currency-display__currency-symbol', primaryCurrency), h('span.currency-display__currency-symbol', primaryCurrency),

View File

@ -49,11 +49,10 @@ export default class SendAmountRow extends Component {
}) })
} }
handleAmountChange (amount) { updateAmount (amount) {
const { updateSendAmount, setMaxModeTo } = this.props const { updateSendAmount, setMaxModeTo } = this.props
setMaxModeTo(false) setMaxModeTo(false)
this.validateAmount(amount)
updateSendAmount(amount) updateSendAmount(amount)
} }
@ -78,7 +77,8 @@ export default class SendAmountRow extends Component {
<CurrencyDisplay <CurrencyDisplay
conversionRate={amountConversionRate} conversionRate={amountConversionRate}
convertedCurrency={convertedCurrency} convertedCurrency={convertedCurrency}
handleChange={newAmount => this.handleAmountChange(newAmount)} onBlur={newAmount => this.updateAmount(newAmount)}
onChange={newAmount => this.validateAmount(newAmount)}
inError={inError} inError={inError}
primaryCurrency={primaryCurrency || 'ETH'} primaryCurrency={primaryCurrency || 'ETH'}
selectedToken={selectedToken} selectedToken={selectedToken}

View File

@ -14,7 +14,7 @@ const propsMethodSpies = {
updateSendAmountError: sinon.spy(), updateSendAmountError: sinon.spy(),
} }
sinon.spy(SendAmountRow.prototype, 'handleAmountChange') sinon.spy(SendAmountRow.prototype, 'updateAmount')
sinon.spy(SendAmountRow.prototype, 'validateAmount') sinon.spy(SendAmountRow.prototype, 'validateAmount')
describe('SendAmountRow Component', function () { describe('SendAmountRow Component', function () {
@ -45,7 +45,7 @@ describe('SendAmountRow Component', function () {
propsMethodSpies.updateSendAmount.resetHistory() propsMethodSpies.updateSendAmount.resetHistory()
propsMethodSpies.updateSendAmountError.resetHistory() propsMethodSpies.updateSendAmountError.resetHistory()
SendAmountRow.prototype.validateAmount.resetHistory() SendAmountRow.prototype.validateAmount.resetHistory()
SendAmountRow.prototype.handleAmountChange.resetHistory() SendAmountRow.prototype.updateAmount.resetHistory()
}) })
describe('validateAmount', () => { describe('validateAmount', () => {
@ -71,11 +71,11 @@ describe('SendAmountRow Component', function () {
}) })
describe('handleAmountChange', () => { describe('updateAmount', () => {
it('should call setMaxModeTo', () => { it('should call setMaxModeTo', () => {
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0) assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0)
instance.handleAmountChange('someAmount') instance.updateAmount('someAmount')
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1) assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1)
assert.deepEqual( assert.deepEqual(
propsMethodSpies.setMaxModeTo.getCall(0).args, propsMethodSpies.setMaxModeTo.getCall(0).args,
@ -83,19 +83,9 @@ describe('SendAmountRow Component', function () {
) )
}) })
it('should call this.validateAmount', () => {
assert.equal(SendAmountRow.prototype.validateAmount.callCount, 0)
instance.handleAmountChange('someAmount')
assert.equal(SendAmountRow.prototype.validateAmount.callCount, 1)
assert.deepEqual(
propsMethodSpies.updateSendAmount.getCall(0).args,
['someAmount']
)
})
it('should call updateSendAmount', () => { it('should call updateSendAmount', () => {
assert.equal(propsMethodSpies.updateSendAmount.callCount, 0) assert.equal(propsMethodSpies.updateSendAmount.callCount, 0)
instance.handleAmountChange('someAmount') instance.updateAmount('someAmount')
assert.equal(propsMethodSpies.updateSendAmount.callCount, 1) assert.equal(propsMethodSpies.updateSendAmount.callCount, 1)
assert.deepEqual( assert.deepEqual(
propsMethodSpies.updateSendAmount.getCall(0).args, propsMethodSpies.updateSendAmount.getCall(0).args,
@ -136,7 +126,8 @@ describe('SendAmountRow Component', function () {
const { const {
conversionRate, conversionRate,
convertedCurrency, convertedCurrency,
handleChange, onBlur,
onChange,
inError, inError,
primaryCurrency, primaryCurrency,
selectedToken, selectedToken,
@ -148,11 +139,18 @@ describe('SendAmountRow Component', function () {
assert.equal(primaryCurrency, 'mockPrimaryCurrency') assert.equal(primaryCurrency, 'mockPrimaryCurrency')
assert.deepEqual(selectedToken, { address: 'mockTokenAddress' }) assert.deepEqual(selectedToken, { address: 'mockTokenAddress' })
assert.equal(value, 'mockAmount') assert.equal(value, 'mockAmount')
assert.equal(SendAmountRow.prototype.handleAmountChange.callCount, 0) assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0)
handleChange('mockNewAmount') onBlur('mockNewAmount')
assert.equal(SendAmountRow.prototype.handleAmountChange.callCount, 1) assert.equal(SendAmountRow.prototype.updateAmount.callCount, 1)
assert.deepEqual( assert.deepEqual(
SendAmountRow.prototype.handleAmountChange.getCall(0).args, SendAmountRow.prototype.updateAmount.getCall(0).args,
['mockNewAmount']
)
assert.equal(SendAmountRow.prototype.validateAmount.callCount, 0)
onChange('mockNewAmount')
assert.equal(SendAmountRow.prototype.validateAmount.callCount, 1)
assert.deepEqual(
SendAmountRow.prototype.validateAmount.getCall(0).args,
['mockNewAmount'] ['mockNewAmount']
) )
}) })

View File

@ -47,10 +47,32 @@
&__input-wrapper { &__input-wrapper {
position: relative; position: relative;
display: flex; display: flex;
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
display: none;
}
input[type="number"]:hover::-webkit-inner-spin-button {
-webkit-appearance: none;
display: none;
}
} }
&__currency-symbol { &__currency-symbol {
margin-top: 1px; margin-top: 1px;
color: $scorpion; color: $scorpion;
} }
.react-numeric-input {
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
display: none;
}
input[type="number"]:hover::-webkit-inner-spin-button {
-webkit-appearance: none;
display: none;
}
}
} }