From 8c9a4015c4cc981e2dfacf8e79d244ad9ec40e22 Mon Sep 17 00:00:00 2001 From: Maxime Quandalle Date: Tue, 17 Sep 2019 12:13:14 +0200 Subject: [PATCH] Input permanent pour les objectifs du simulateur Nous alternions avant entre un et un selon le contexte Fixes #558 --- .../components/CurrencyInput/CurrencyInput.js | 6 +- .../CurrencyInput/CurrencyInput.test.js | 22 ++- source/components/PeriodSwitch.js | 5 +- source/components/TargetSelection.css | 34 ++-- source/components/TargetSelection.js | 162 ++++++++++-------- source/components/ui/AnimatedTargetValue.js | 21 +-- source/components/utils/withColours.js | 2 +- 7 files changed, 143 insertions(+), 109 deletions(-) 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() )