Skip to content

Commit

Permalink
Input permanent pour les objectifs du simulateur
Browse files Browse the repository at this point in the history
Nous alternions avant entre un <span /> et un <input /> selon le contexte

Fixes #558
  • Loading branch information
mquandalle committed Sep 17, 2019
1 parent 3849db4 commit 98563f2
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 102 deletions.
3 changes: 2 additions & 1 deletion source/components/CurrencyInput/CurrencyInput.js
Expand Up @@ -76,14 +76,15 @@ export default function CurrencyInput({
<div
className={classnames(className, 'currencyInput__container')}
{...(valueLength > 5 ? { style: { width } } : {})}>
{isCurrencyPrefixed && '€'}
{!currentValue && isCurrencyPrefixed && '€'}
<NumberFormat
{...forwardedProps}
thousandSeparator={thousandSeparator}
decimalSeparator={decimalSeparator}
allowNegative={!valueHasChanged}
className="currencyInput__input"
inputMode="numeric"
prefix={isCurrencyPrefixed ? '€ ' : ''}
onValueChange={({ value }) => {
setCurrentValue(value)
nextValue.current = value.toString().replace(/^-/, '')
Expand Down
5 changes: 4 additions & 1 deletion source/components/PeriodSwitch.js
Expand Up @@ -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 (
<span id="PeriodSwitch">
Expand Down
34 changes: 23 additions & 11 deletions source/components/TargetSelection.css
Expand Up @@ -106,35 +106,47 @@
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;
border: 2px solid;
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%;
Expand Down
156 changes: 83 additions & 73 deletions 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'
Expand All @@ -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(
Expand All @@ -40,6 +36,7 @@ export default compose(
)
const situation = useSituation()
const dispatch = useDispatch()
const colours = useContext(ThemeColoursContext)

const targets =
analysis?.targets.filter(
Expand All @@ -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 =>
Expand Down Expand Up @@ -114,7 +112,7 @@ export default compose(
)}
</div>
)
})
}

let Targets = ({ targets, initialRender }) => (
<div>
Expand Down Expand Up @@ -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 (
Expand All @@ -156,8 +155,7 @@ const Target = ({ target, initialRender }) => {
<Header
{...{
target,

isActiveInput: activeInput === target.dottedName
isActiveInput
}}
/>
{isSmallTarget && (
Expand All @@ -172,11 +170,12 @@ const Target = ({ target, initialRender }) => {
<TargetInputOrValue
{...{
target,
activeInput
isActiveInput,
isSmallTarget
}}
/>
</div>
{activeInput === target.dottedName && (
{isActiveInput && (
<Animate.fromTop>
<InputSuggestions
suggestions={target.suggestions}
Expand Down Expand Up @@ -209,85 +208,94 @@ let Header = withSitePaths(({ target, sitePaths }) => {
)
})

let DebouncedCurrencyField = withColours(props => {
return (
<CurrencyInput
style={{
color: props.colours.textColour,
borderColor: props.colours.textColour
}}
debounce={600}
className="targetInput"
{...props}
/>
)
})
let DebouncedPercentageField = props => (
<PercentageField debounce={600} {...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 (
<>
<AnimatedTargetValue value={value} />
<Input
value={value}
debounce={600}
style={{
color: colors.textColour,
borderColor: colors.textColour
}}
{...otherProps}
/>
<span className="targetInputBottomBorder">
{formatCurrency(value, language)}
</span>
</>
)
}

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 (
<span className="targetInputOrValue">
{inputIsActive || !target.formule || isEmpty(target.formule) ? (
<span
className="targetInputOrValue"
style={blurValue ? { filter: 'blur(3px)' } : {}}>
{target.question ? (
<Component
name={target.dottedName}
value={situationValue}
value={situationValue || value}
className={
isActiveInput || isNil(value) ? 'targetInput' : 'editableTarget'
}
onChange={evt =>
dispatch(updateSituation(target.dottedName, evt.target.value))
}
onBlur={event => event.preventDefault()}
{...(inputIsActive ? { autoFocus: true } : {})}
// We use onMouseDown instead of onClick because the cursor appear onMouseDown
onMouseDown={() => {
if (isSmallTarget) return
dispatch({
type: 'SET_ACTIVE_TARGET_INPUT',
name: target.dottedName
})
}}
{...(isActiveInput ? { autoFocus: true } : {})}
language={i18n.language}
/>
) : (
<TargetValue target={target} />
<span>
{Number.isNaN(value) ? '—' : formatCurrency(value, i18n.language)}
</span>
)}
{target.dottedName.includes('rémunération . total') && <AidesGlimpse />}
</span>
)
}

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 (
<div
className={classNames({
editable: target.question,
attractClick: target.question && isNil(target.nodeValue)
})}
style={blurValue ? { filter: 'blur(3px)' } : {}}
{...(target.question ? { tabIndex: 0 } : {})}
onClick={showField(value)}
onFocus={showField(value)}>
<AnimatedTargetValue value={value} />
</div>
)
}

function AidesGlimpse() {
const aides = useTarget('contrat salarié . aides employeur')
if (!aides?.nodeValue) return null
Expand All @@ -297,7 +305,9 @@ function AidesGlimpse() {
<RuleLink {...aides}>
-{' '}
<strong>
<AnimatedTargetValue value={aides.nodeValue} />
<AnimatedTargetValue value={aides.nodeValue}>
<span>{formatCurrency(aides.nodeValue)}</span>
</AnimatedTargetValue>
</strong>{' '}
<T>d'aides</T> {emoji(aides.icons)}
</RuleLink>
Expand Down
21 changes: 6 additions & 15 deletions source/components/ui/AnimatedTargetValue.js
Expand Up @@ -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(() => {
Expand All @@ -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 (
Expand All @@ -40,12 +30,13 @@ export default function AnimatedTargetValue({ value }: Props) {
{shouldDisplayDifference && (
<Evaporate
style={{
color: difference > 0 ? 'chartreuse' : 'red'
color: difference > 0 ? 'chartreuse' : 'red',
pointerEvents: 'none'
}}>
{(difference > 0 ? '+' : '') + formattedDifference}
</Evaporate>
)}{' '}
<span>{Number.isNaN(value) ? '—' : format(value)}</span>
{children}
</span>
</>
)
Expand Down

0 comments on commit 98563f2

Please sign in to comment.