diff --git a/source/components/CurrencyInput/CurrencyInput.js b/source/components/CurrencyInput/CurrencyInput.js
index 7507264ff2..593c27a87a 100644
--- a/source/components/CurrencyInput/CurrencyInput.js
+++ b/source/components/CurrencyInput/CurrencyInput.js
@@ -24,6 +24,7 @@ let currencyFormat = language => ({
export default function CurrencyInput({
value: valueProp = '',
debounce: debounceTimeout,
+ currencySymbol = '€',
onChange,
language,
className,
@@ -76,7 +77,7 @@ export default function CurrencyInput({
5 ? { style: { width } } : {})}>
- {isCurrencyPrefixed && '€'}
+ {!currentValue && isCurrencyPrefixed && currencySymbol}
{
setCurrentValue(value)
nextValue.current = value.toString().replace(/^-/, '')
diff --git a/source/components/CurrencyInput/CurrencyInput.test.js b/source/components/CurrencyInput/CurrencyInput.test.js
index 8e6a331e35..a0469a8ed7 100644
--- a/source/components/CurrencyInput/CurrencyInput.test.js
+++ b/source/components/CurrencyInput/CurrencyInput.test.js
@@ -24,10 +24,18 @@ describe('CurrencyInput', () => {
})
it('should separate thousand groups', () => {
- const input1 = getInput()
- const input2 = getInput()
- const input3 = getInput()
- const input4 = getInput()
+ const input1 = getInput(
+
+ )
+ const input2 = getInput(
+
+ )
+ const input3 = getInput(
+
+ )
+ const input4 = getInput(
+
+ )
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')
@@ -90,7 +98,7 @@ describe('CurrencyInput', () => {
const clock = useFakeTimers()
let onChange = spy()
const input = getInput(
-
+
)
input.simulate('change', { target: { value: '1', focus: () => {} } })
expect(onChange).not.to.have.been.called
@@ -106,12 +114,12 @@ describe('CurrencyInput', () => {
})
it('should initialize with value of the value prop', () => {
- const input = getInput()
+ const input = getInput()
expect(input.instance().value).to.equal('1')
})
it('should update its value if the value prop changes', () => {
- const component = mount()
+ const component = mount()
component.setProps({ value: 2 })
expect(component.find('input').instance().value).to.equal('2')
})
diff --git a/source/components/PeriodSwitch.js b/source/components/PeriodSwitch.js
index 1feb823a1d..d2c4950ef3 100644
--- a/source/components/PeriodSwitch.js
+++ b/source/components/PeriodSwitch.js
@@ -29,7 +29,10 @@ export default function PeriodSwitch() {
},
[dispatch, rules, situation]
)
- const periods = ['mois', 'année']
+ let periods = ['mois', 'année']
+ if (initialPeriod === 'année') {
+ periods.reverse()
+ }
return (
diff --git a/source/components/TargetSelection.css b/source/components/TargetSelection.css
index 4da72c2d54..63783d9ebc 100644
--- a/source/components/TargetSelection.css
+++ b/source/components/TargetSelection.css
@@ -106,28 +106,19 @@
text-decoration: none;
}
-#targetSelection .editable:not(.attractClick) {
- border: 2px solid rgba(0, 0, 0, 0);
- border-bottom: 1px dashed #ffffff91;
- min-width: 2.5em;
- display: inline-block;
-}
#targetSelection .targetInputOrValue > :not(.targetInput):not(.attractClick) {
margin: 0.2rem 0.6rem;
}
-#targetSelection .attractClick.editable::before {
- content: '€';
+#targetSelection input {
+ margin: 2.7px 0;
}
-#targetSelection .attractClick,
#targetSelection .targetInput {
width: 5.5em;
max-width: 7.5em;
- display: inline-block;
text-align: right;
background: rgba(255, 255, 255, 0.2);
- cursor: text;
padding: 0;
padding: 0.2rem 0.6rem;
border-radius: 0.3rem;
@@ -135,6 +126,27 @@
font-size: inherit;
}
+#targetSelection .editableTarget {
+ max-width: 7.5em;
+ display: inline-block;
+ text-align: right;
+ padding: 0 2px;
+ font-size: inherit;
+}
+
+#targetSelection .targetInputBottomBorder {
+ margin: 0;
+ padding: 0;
+ height: 0;
+ overflow: hidden;
+ position: relative;
+ top: -6px;
+}
+
+#targetSelection .editableTarget + .targetInputBottomBorder {
+ border-bottom: 1px dashed #ffffff91;
+}
+
#targetSelection .unit {
margin-left: 0.4em;
font-size: 110%;
diff --git a/source/components/TargetSelection.js b/source/components/TargetSelection.js
index 1dae7bf025..e781539d17 100644
--- a/source/components/TargetSelection.js
+++ b/source/components/TargetSelection.js
@@ -1,16 +1,15 @@
import { updateSituation } from 'Actions/actions'
-import classNames from 'classnames'
import { T } from 'Components'
import InputSuggestions from 'Components/conversation/InputSuggestions'
import PercentageField from 'Components/PercentageField'
import PeriodSwitch from 'Components/PeriodSwitch'
import RuleLink from 'Components/RuleLink'
-import withColours from 'Components/utils/withColours'
+import { ThemeColoursContext } from 'Components/utils/withColours'
import withSitePaths from 'Components/utils/withSitePaths'
import { encodeRuleName } from 'Engine/rules'
import { serialiseUnit } from 'Engine/units'
-import { compose, isEmpty, isNil } from 'ramda'
-import React, { memo, useEffect, useState } from 'react'
+import { isEmpty, isNil } from 'ramda'
+import React, { useEffect, useState, useContext } from 'react'
import emoji from 'react-easy-emoji'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
@@ -26,10 +25,7 @@ import AnimatedTargetValue from 'Ui/AnimatedTargetValue'
import CurrencyInput from './CurrencyInput/CurrencyInput'
import './TargetSelection.css'
-export default compose(
- withColours,
- memo
-)(function TargetSelection({ colours }) {
+export default function TargetSelection() {
const [initialRender, setInitialRender] = useState(true)
const analysis = useSelector(analysisWithDefaultsSelector)
const objectifs = useSelector(
@@ -40,6 +36,7 @@ export default compose(
)
const situation = useSituation()
const dispatch = useDispatch()
+ const colours = useContext(ThemeColoursContext)
const targets =
analysis?.targets.filter(
@@ -50,6 +47,7 @@ export default compose(
useEffect(() => {
// Initialize defaultValue for target that can't be computed
+ // TODO: this logic shouldn't be here
targets
.filter(
target =>
@@ -114,7 +112,7 @@ export default compose(
)}
)
-})
+}
let Targets = ({ targets, initialRender }) => (
@@ -144,6 +142,7 @@ const Target = ({ target, initialRender }) => {
const activeInput = useSelector(state => state.activeTargetInput)
const dispatch = useDispatch()
+ const isActiveInput = activeInput === target.dottedName
const isSmallTarget =
!target.question || !target.formule || isEmpty(target.formule)
return (
@@ -156,8 +155,7 @@ const Target = ({ target, initialRender }) => {
{isSmallTarget && (
@@ -172,11 +170,12 @@ const Target = ({ target, initialRender }) => {
- {activeInput === target.dottedName && (
+ {isActiveInput && (
{
)
})
-let DebouncedCurrencyField = withColours(props => {
- return (
-
- )
-})
-let DebouncedPercentageField = props => (
-
-)
+export const formatCurrency = (value, language) => {
+ return value == null
+ ? ''
+ : Intl.NumberFormat(language, {
+ style: 'currency',
+ currency: 'EUR',
+ maximumFractionDigits: 0,
+ minimumFractionDigits: 0
+ })
+ .format(value)
+ .replace(/^€/, '€ ')
+}
-let TargetInputOrValue = ({ target, activeInput }) => {
+let clickableField = Input =>
+ function WrappedClickableField({ value, ...otherProps }) {
+ const colors = useContext(ThemeColoursContext)
+ const { language } = useTranslation().i18n
+ return (
+ <>
+
+
+
+ {formatCurrency(value, language)}
+
+ >
+ )
+ }
+
+let unitToComponent = {
+ '€': clickableField(CurrencyInput),
+ '%': clickableField(PercentageField)
+}
+
+let TargetInputOrValue = ({ target, isActiveInput, isSmallTarget }) => {
const { i18n } = useTranslation()
const dispatch = useDispatch()
const situationValue = useSituationValue(target.dottedName)
-
- let inputIsActive = activeInput === target.dottedName
- const Component = {
- '€': DebouncedCurrencyField,
- '%': DebouncedPercentageField
- }[serialiseUnit(target.unit)]
+ const targetWithValue = useTarget(target.dottedName)
+ const value = targetWithValue?.nodeValue?.toFixed(0)
+ const inversionFail = useSelector(
+ state => analysisWithDefaultsSelector(state)?.cache.inversionFail
+ )
+ const blurValue = inversionFail && !isActiveInput && value
+ const Component = unitToComponent[serialiseUnit(target.unit)]
return (
-
- {inputIsActive || !target.formule || isEmpty(target.formule) ? (
+
+ {target.question ? (
dispatch(updateSituation(target.dottedName, evt.target.value))
}
onBlur={event => event.preventDefault()}
- {...(inputIsActive ? { autoFocus: true } : {})}
+ // We use onMouseDown instead of onClick because that's when the browser moves the cursor
+ onMouseDown={() => {
+ if (isSmallTarget) return
+ dispatch({
+ type: 'SET_ACTIVE_TARGET_INPUT',
+ name: target.dottedName
+ })
+ // TODO: This shouldn't be necessary: we don't need to recalculate the situation
+ // when the user just focus another field. Removing this line is almost working
+ // however there is a weird bug in the selection of the next question.
+ if (value) {
+ dispatch(updateSituation(target.dottedName, '' + value))
+ }
+ }}
+ {...(isActiveInput ? { autoFocus: true } : {})}
language={i18n.language}
/>
) : (
-
+
+ {Number.isNaN(value) ? '—' : formatCurrency(value, i18n.language)}
+
)}
{target.dottedName.includes('rémunération . total') && }
)
}
-function TargetValue({ target }) {
- const blurValue = useSelector(
- state => analysisWithDefaultsSelector(state)?.cache.inversionFail
- )
- const targetWithValue = useTarget(target.dottedName)
- const dispatch = useDispatch()
-
- const value = targetWithValue?.nodeValue
- const showField = value => () => {
- if (!target.question) return
- if (value != null && !Number.isNaN(value))
- dispatch(updateSituation(target.dottedName, Math.round(value) + ''))
-
- dispatch({ type: 'SET_ACTIVE_TARGET_INPUT', name: target.dottedName })
- }
-
- return (
-
- )
-}
-
function AidesGlimpse() {
const aides = useTarget('contrat salarié . aides employeur')
if (!aides?.nodeValue) return null
@@ -297,7 +311,9 @@ function AidesGlimpse() {
-{' '}
-
+
+ {formatCurrency(aides.nodeValue)}
+
{' '}
d'aides {emoji(aides.icons)}
diff --git a/source/components/ui/AnimatedTargetValue.js b/source/components/ui/AnimatedTargetValue.js
index 542b065a92..0efd1eae5a 100644
--- a/source/components/ui/AnimatedTargetValue.js
+++ b/source/components/ui/AnimatedTargetValue.js
@@ -3,12 +3,13 @@ import React, { useEffect, useState } from 'react'
import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
import { useTranslation } from 'react-i18next'
import './AnimatedTargetValue.css'
+import { formatCurrency } from 'Components/TargetSelection'
type Props = {
value: ?number
}
-export default function AnimatedTargetValue({ value }: Props) {
+export default function AnimatedTargetValue({ value, children }: Props) {
const [difference, setDifference] = useState(0)
const [previousValue, setPreviousValue] = useState()
useEffect(() => {
@@ -20,18 +21,7 @@ export default function AnimatedTargetValue({ value }: Props) {
}, [previousValue, value])
const { i18n } = useTranslation()
- const format = value => {
- return value == null
- ? ''
- : Intl.NumberFormat(i18n.language, {
- style: 'currency',
- currency: 'EUR',
- maximumFractionDigits: 0,
- minimumFractionDigits: 0
- }).format(value)
- }
-
- const formattedDifference = format(difference)
+ const formattedDifference = formatCurrency(difference, i18n.language)
const shouldDisplayDifference =
Math.abs(difference) > 1 && value != null && !Number.isNaN(value)
return (
@@ -40,12 +30,13 @@ export default function AnimatedTargetValue({ value }: Props) {
{shouldDisplayDifference && (
0 ? 'chartreuse' : 'red'
+ color: difference > 0 ? 'chartreuse' : 'red',
+ pointerEvents: 'none'
}}>
{(difference > 0 ? '+' : '') + formattedDifference}
)}{' '}
- {Number.isNaN(value) ? '—' : format(value)}
+ {children}
>
)
diff --git a/source/components/utils/withColours.js b/source/components/utils/withColours.js
index 17822c1a40..9ef5b5f232 100644
--- a/source/components/utils/withColours.js
+++ b/source/components/utils/withColours.js
@@ -88,7 +88,7 @@ const generateTheme = (themeColour?: ?string): ThemeColours => {
}
}
-const ThemeColoursContext: React$Context = createContext(
+export const ThemeColoursContext: React$Context = createContext(
generateTheme()
)