Merge pull request #551 from mquandalle/format-currency-input
Formatage des prix dans les champs de saisiepull/559/head
commit
fe8615c4cc
|
@ -31,7 +31,7 @@ if (fr) {
|
|||
|
||||
describe('Simulation saving test', function() {
|
||||
it('should save the current simulation', function() {
|
||||
salaryInput('Salaire net').type('5471')
|
||||
salaryInput('Salaire net').type('471')
|
||||
cy.wait(1000)
|
||||
cy.contains('CDD').click()
|
||||
cy.contains('passer').click()
|
||||
|
@ -40,7 +40,7 @@ if (fr) {
|
|||
cy.wait(1100)
|
||||
cy.visit('/sécurité-sociale/salarié')
|
||||
cy.contains('Retrouver ma simulation').click()
|
||||
salaryInput('Salaire net').should('have.value', '5471')
|
||||
salaryInput('Salaire net').should('have.value', '471')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
"react-helmet": "6.0.0-beta",
|
||||
"react-highlight-words": "^0.11.0",
|
||||
"react-i18next": "^10.0.1",
|
||||
"react-number-format": "^4.0.8",
|
||||
"react-redux": "^7.0.3",
|
||||
"react-router": "^4.3.1",
|
||||
"react-router-dom": "^4.3.1",
|
||||
|
|
|
@ -1,51 +1,61 @@
|
|||
import classnames from 'classnames'
|
||||
import { omit } from 'ramda'
|
||||
import React, { Component } from 'react'
|
||||
import NumberFormat from 'react-number-format'
|
||||
import { debounce } from '../../utils'
|
||||
import './CurrencyInput.css'
|
||||
|
||||
let isCurrencyPrefixed = language =>
|
||||
!!Intl.NumberFormat(language, {
|
||||
let currencyFormat = language => ({
|
||||
isCurrencyPrefixed: !!Intl.NumberFormat(language, {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
})
|
||||
.format(12)
|
||||
.match(/€.*12/)
|
||||
.match(/€.*12/),
|
||||
|
||||
thousandSeparator: Intl.NumberFormat(language)
|
||||
.format(1000)
|
||||
.charAt(1),
|
||||
|
||||
decimalSeparator: Intl.NumberFormat(language)
|
||||
.format(0.1)
|
||||
.charAt(1)
|
||||
})
|
||||
|
||||
class CurrencyInput extends Component {
|
||||
state = {
|
||||
value: this.props.storeValue
|
||||
value: this.props.value,
|
||||
initialValue: this.props.value
|
||||
}
|
||||
|
||||
onChange = this.props.debounce
|
||||
? debounce(this.props.debounce, this.props.onChange)
|
||||
: this.props.onChange
|
||||
input = React.createRef()
|
||||
|
||||
handleNextChange = false
|
||||
value = undefined
|
||||
handleChange = event => {
|
||||
let value = event.target.value
|
||||
value = value
|
||||
.replace(/,/g, '.')
|
||||
.replace(/[^\d.]/g, '')
|
||||
.replace(/\.(.*)\.(.*)/g, '$1.$2')
|
||||
this.setState({ value })
|
||||
|
||||
if (value.endsWith('.')) {
|
||||
// Only trigger the `onChange` event if the value has changed -- and not
|
||||
// only its formating, we don't want to call it when a dot is added in `12.`
|
||||
// for instance
|
||||
if (!this.handleNextChange) {
|
||||
return
|
||||
}
|
||||
event.target.value = value
|
||||
|
||||
if (event.persist) {
|
||||
event.persist()
|
||||
this.handleNextChange = false
|
||||
event.persist()
|
||||
event.target = {
|
||||
...event.target,
|
||||
value: this.value
|
||||
}
|
||||
this.onChange(event)
|
||||
}
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.storeValue !== this.props.storeValue &&
|
||||
this.props.storeValue !== this.state.value
|
||||
) {
|
||||
this.setState({ value: this.props.storeValue })
|
||||
|
||||
// See https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#alternative-1-reset-uncontrolled-component-with-an-id-prop
|
||||
static getDerivedStateFromProps({ value }, { initialValue }) {
|
||||
if (value !== initialValue) {
|
||||
return { value, initialValue: value }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -54,22 +64,40 @@ class CurrencyInput extends Component {
|
|||
this.props
|
||||
)
|
||||
|
||||
const {
|
||||
isCurrencyPrefixed,
|
||||
thousandSeparator,
|
||||
decimalSeparator
|
||||
} = currencyFormat(this.props.language)
|
||||
|
||||
// We display negative numbers iff this was the provided value (but we allow the user to enter them)
|
||||
const valueHasChanged = this.state.value !== this.state.initialValue
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
this.props.className,
|
||||
'currencyInput__container'
|
||||
)}>
|
||||
{isCurrencyPrefixed(this.props.language) && '€'}
|
||||
<input
|
||||
{isCurrencyPrefixed && '€'}
|
||||
<NumberFormat
|
||||
{...forwardedProps}
|
||||
thousandSeparator={thousandSeparator}
|
||||
decimalSeparator={decimalSeparator}
|
||||
allowNegative={!valueHasChanged}
|
||||
className="currencyInput__input"
|
||||
inputMode="numeric"
|
||||
onValueChange={({ value }) => {
|
||||
this.setState({ value })
|
||||
this.value = value.toString().replace(/^\-/, '')
|
||||
this.handleNextChange = true
|
||||
}}
|
||||
onChange={this.handleChange}
|
||||
ref={this.input}
|
||||
value={this.state.value}
|
||||
value={(this.state.value || '')
|
||||
.toString()
|
||||
.replace('.', decimalSeparator)}
|
||||
/>
|
||||
{!isCurrencyPrefixed(this.props.language) && <> €</>}
|
||||
{!isCurrencyPrefixed && <> €</>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { expect } from 'chai'
|
||||
import { shallow } from 'enzyme'
|
||||
import { shallow, mount } from 'enzyme'
|
||||
import React from 'react'
|
||||
import { match, spy, useFakeTimers } from 'sinon'
|
||||
import CurrencyInput from './CurrencyInput'
|
||||
|
||||
let getInput = component => shallow(component).find('input')
|
||||
let getInput = component => mount(component).find('input')
|
||||
describe('CurrencyInput', () => {
|
||||
it('should render an input', () => {
|
||||
expect(getInput(<CurrencyInput />)).to.have.length(1)
|
||||
|
@ -13,20 +13,36 @@ describe('CurrencyInput', () => {
|
|||
it('should accept both . and , as decimal separator', () => {
|
||||
let onChange = spy()
|
||||
const input = getInput(<CurrencyInput onChange={onChange} />)
|
||||
input.simulate('change', { target: { value: '12.1' } })
|
||||
input.simulate('change', { target: { value: '12.1', focus: () => {} } })
|
||||
expect(onChange).to.have.been.calledWith(
|
||||
match.hasNested('target.value', '12.1')
|
||||
)
|
||||
input.simulate('change', { target: { value: '12,1' } })
|
||||
input.simulate('change', { target: { value: '12,1', focus: () => {} } })
|
||||
expect(onChange).to.have.been.calledWith(
|
||||
match.hasNested('target.value', '12.1')
|
||||
)
|
||||
})
|
||||
|
||||
it('should separate thousand groups', () => {
|
||||
const input1 = getInput(<CurrencyInput value={1000} language="fr" />)
|
||||
const input2 = getInput(<CurrencyInput value={1000} language="en" />)
|
||||
const input3 = getInput(<CurrencyInput value={1000.5} language="en" />)
|
||||
const input4 = getInput(<CurrencyInput value={1000000} language="en" />)
|
||||
expect(input1.instance().value).to.equal('1 000')
|
||||
expect(input2.instance().value).to.equal('1,000')
|
||||
expect(input3.instance().value).to.equal('1,000.5')
|
||||
expect(input4.instance().value).to.equal('1,000,000')
|
||||
})
|
||||
|
||||
it('should handle decimal separator', () => {
|
||||
const input = getInput(<CurrencyInput value={0.5} language="fr" />)
|
||||
expect(input.instance().value).to.equal('0,5')
|
||||
})
|
||||
|
||||
it('should not accept negative number', () => {
|
||||
let onChange = spy()
|
||||
const input = getInput(<CurrencyInput onChange={onChange} />)
|
||||
input.simulate('change', { target: { value: '-12' } })
|
||||
input.simulate('change', { target: { value: '-12', focus: () => {} } })
|
||||
expect(onChange).to.have.been.calledWith(
|
||||
match.hasNested('target.value', '12')
|
||||
)
|
||||
|
@ -35,19 +51,21 @@ describe('CurrencyInput', () => {
|
|||
it('should not accept anything else than number', () => {
|
||||
let onChange = spy()
|
||||
const input = getInput(<CurrencyInput onChange={onChange} />)
|
||||
input.simulate('change', { target: { value: '*1/2abc3' } })
|
||||
input.simulate('change', { target: { value: '*1/2abc3', focus: () => {} } })
|
||||
expect(onChange).to.have.been.calledWith(
|
||||
match.hasNested('target.value', '123')
|
||||
)
|
||||
})
|
||||
|
||||
it('should pass other props to the input', () => {
|
||||
const input = getInput(<CurrencyInput autoFocus />)
|
||||
expect(input.prop('autoFocus')).to.be.true
|
||||
})
|
||||
|
||||
it('should not call onChange while the decimal part is being written', () => {
|
||||
let onChange = spy()
|
||||
const input = getInput(<CurrencyInput onChange={onChange} />)
|
||||
input.simulate('change', { target: { value: '111,' } })
|
||||
const input = getInput(<CurrencyInput value="111" onChange={onChange} />)
|
||||
input.simulate('change', { target: { value: '111,', focus: () => {} } })
|
||||
expect(onChange).not.to.have.been.called
|
||||
})
|
||||
|
||||
|
@ -74,10 +92,10 @@ describe('CurrencyInput', () => {
|
|||
const input = getInput(
|
||||
<CurrencyInput onChange={onChange} debounce={1000} />
|
||||
)
|
||||
input.simulate('change', { target: { value: '1' } })
|
||||
input.simulate('change', { target: { value: '1', focus: () => {} } })
|
||||
expect(onChange).not.to.have.been.called
|
||||
clock.tick(500)
|
||||
input.simulate('change', { target: { value: '12' } })
|
||||
input.simulate('change', { target: { value: '12', focus: () => {} } })
|
||||
clock.tick(600)
|
||||
expect(onChange).not.to.have.been.called
|
||||
clock.tick(400)
|
||||
|
@ -87,25 +105,23 @@ describe('CurrencyInput', () => {
|
|||
clock.restore()
|
||||
})
|
||||
|
||||
it('should initialize with value of the storeValue prop', () => {
|
||||
const input = getInput(<CurrencyInput storeValue={1} />)
|
||||
expect(input.prop('value')).to.eq(1)
|
||||
it('should initialize with value of the value prop', () => {
|
||||
const wrapper = shallow(<CurrencyInput value={1} />)
|
||||
expect(wrapper.state('value')).to.eq(1)
|
||||
})
|
||||
|
||||
it('should update its value if the storeValue prop changes', () => {
|
||||
const wrapper = shallow(<CurrencyInput storeValue={1} />)
|
||||
wrapper.setProps({ storeValue: 2 })
|
||||
it('should update its value if the value prop changes', () => {
|
||||
const wrapper = shallow(<CurrencyInput value={1} />)
|
||||
wrapper.setProps({ value: 2 })
|
||||
expect(wrapper.state('value')).to.equal(2)
|
||||
})
|
||||
it('should not update state if the storeValue is the same as the current input value', () => {
|
||||
const wrapper = shallow(
|
||||
<CurrencyInput storeValue={1000} onChange={() => {}} />
|
||||
)
|
||||
|
||||
it('should not call onChange the value is the same as the current input value', () => {
|
||||
let onChange = spy()
|
||||
const wrapper = mount(<CurrencyInput value={1000} onChange={() => {}} />)
|
||||
const input = wrapper.find('input')
|
||||
input.simulate('change', { target: { value: '2000' } })
|
||||
const state1 = wrapper.state()
|
||||
wrapper.setProps({ storeValue: '2000' })
|
||||
const state2 = wrapper.state()
|
||||
expect(state1).to.equal(state2)
|
||||
input.simulate('change', { target: { value: '2000', focus: () => {} } })
|
||||
wrapper.setProps({ value: '2000' })
|
||||
expect(onChange).not.to.have.been.called
|
||||
})
|
||||
})
|
||||
|
|
|
@ -250,7 +250,7 @@ let CurrencyField = withColours(props => {
|
|||
}}
|
||||
debounce={600}
|
||||
className="targetInput"
|
||||
storeValue={props.input.value}
|
||||
value={props.input.value}
|
||||
{...props.input}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -274,6 +274,7 @@ let TargetInputOrValue = withLanguage(
|
|||
<Field
|
||||
name={target.dottedName}
|
||||
component={CurrencyField}
|
||||
onBlur={event => event.preventDefault()}
|
||||
{...(inputIsActive ? { autoFocus: true } : {})}
|
||||
language={language}
|
||||
/>
|
||||
|
|
|
@ -35,6 +35,11 @@ export default withLanguage(
|
|||
this.timeoutId = null
|
||||
}, 250)
|
||||
}
|
||||
componentWillUnmount() {
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId)
|
||||
}
|
||||
}
|
||||
format = value => {
|
||||
return value == null
|
||||
? ''
|
||||
|
|
|
@ -7695,6 +7695,13 @@ react-lifecycles-compat@^3.0.4:
|
|||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||
|
||||
react-number-format@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/react-number-format/-/react-number-format-4.0.8.tgz#f0a0dfbeded9a746f4d8b309926cf55d7effebb2"
|
||||
integrity sha512-A7Gi4BSkdgnyY1DO98lwFvWujcyxZCOfNP6tkiOKkwMY6oFP+JTQhd/vQ9dXXs6TpyXfl1eBJdueokM7o98YTQ==
|
||||
dependencies:
|
||||
prop-types "^15.7.2"
|
||||
|
||||
react-redux@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.0.3.tgz#983c5a6de81cb1e696bd1c090ba826545f9170f1"
|
||||
|
|
Loading…
Reference in New Issue