diff --git a/source/components/AnimatedTargetValue.css b/source/components/AnimatedTargetValue.css index 407ebfc7e..e4f074e8d 100644 --- a/source/components/AnimatedTargetValue.css +++ b/source/components/AnimatedTargetValue.css @@ -1,14 +1,33 @@ .Rule-value { - transition: background 0.8s; font-size: 105%; + position: relative; } -/* Animation of summary figures changes : flash ! */ -.flash-enter { - background: rgba(255, 255, 255, 1); -} - -.flash-leave { - /* Completely hide the button while it's being animated and before it's removed from the DOM. */ +.evaporate { display: none; + font-size: 80%; + /* text-shadow: 0 0 2px var(--colour); */ +} +.evaporate-enter { + display: block; + position: absolute; + right: 0; + top: -20px; + opacity: 0; + animation: evaporate 1.6s ease-out; + transform: scaleY(0.1); +} +@keyframes evaporate { + 5% { + opacity: 1; + transform: scaleY(1); + } + 70% { + opacity: 1; + } + + to { + transform: translateY(-15px); + opacity: 0; + } } diff --git a/source/components/AnimatedTargetValue.js b/source/components/AnimatedTargetValue.js index 3bc17e04d..d103ff619 100644 --- a/source/components/AnimatedTargetValue.js +++ b/source/components/AnimatedTargetValue.js @@ -1,32 +1,91 @@ +/* @flow */ import withLanguage from 'Components/utils/withLanguage' -import React, { Component } from 'react' +import React, { Component, PureComponent } from 'react' import ReactCSSTransitionGroup from 'react-addons-css-transition-group' import './AnimatedTargetValue.css' +type Props = { + value: ?number, + language: string +} +type State = { + difference: number +} export default withLanguage( - class AnimatedTargetValue extends Component { + class AnimatedTargetValue extends Component { + previousValue: ?number = null + timeoutId: ?TimeoutID = null + state = { difference: 0 } + + componentDidUpdate(prevProps) { + if (prevProps.value === this.props.value) { + return + } + if (this.timeoutId) { + clearTimeout(this.timeoutId) + } + this.previousValue = + this.previousValue === null ? prevProps.value : this.previousValue + + this.timeoutId = setTimeout(() => { + this.setState({ + difference: (this.props.value || 0) - (this.previousValue || 0) + }) + this.previousValue = null + this.timeoutId = null + }, 250) + } + format = value => { + return value == null + ? '' + : Intl.NumberFormat(this.props.language, { + style: 'currency', + currency: 'EUR', + maximumFractionDigits: 0, + minimumFractionDigits: 0 + }).format(value) + } render() { - let { value, language } = this.props - let formattedValue = - value == null - ? '' - : Intl.NumberFormat(language, { - style: 'currency', - currency: 'EUR', - maximumFractionDigits: 0, - minimumFractionDigits: 0 - }).format(value) + const formattedValue = this.format(this.props.value) + const formattedDifference = this.format(this.state.difference) + const shouldDisplayDifference = + Math.abs(this.state.difference) > 1 && + formattedDifference !== formattedValue && + this.props.value != null && + this.state.difference < 0.5 * this.props.value return ( - - - {' '} - {formattedValue} + <> + + {shouldDisplayDifference && ( + 0 ? 'chartreuse' : 'red' + }}> + {(this.state.difference > 0 ? '+' : '') + formattedDifference} + + )}{' '} + {this.format(this.props.value)} - + ) } } ) + +class Evaporate extends PureComponent<{ children: string, style: Object }> { + render() { + return ( + + + {this.props.children} + + + ) + } +} diff --git a/source/components/conversation/FormDecorator.js b/source/components/conversation/FormDecorator.js index e1b8fd4ac..d1082a37e 100644 --- a/source/components/conversation/FormDecorator.js +++ b/source/components/conversation/FormDecorator.js @@ -43,18 +43,16 @@ export var FormDecorator = formType => RenderField => } render() { let { - setFormValue, stepAction, subquestion, valueType, defaultValue, fieldName, inversion, + setFormValue, themeColours } = this.props - let validate = buildValidationFunction(valueType) - let submit = cause => stepAction('fold', fieldName, cause), stepProps = { ...this.props, diff --git a/source/components/conversation/Input.js b/source/components/conversation/Input.js index 7eb714355..07a3220f0 100644 --- a/source/components/conversation/Input.js +++ b/source/components/conversation/Input.js @@ -3,6 +3,7 @@ import withColours from 'Components/utils/withColours' import { compose } from 'ramda' import React, { Component } from 'react' import { withI18n } from 'react-i18next' +import { debounce } from '../../utils' import { FormDecorator } from './FormDecorator' import InputSuggestions from './InputSuggestions' import SendButton from './SendButton' @@ -13,6 +14,7 @@ export default compose( withColours )( class Input extends Component { + debouncedOnChange = debounce(750, this.props.input.onChange) render() { let { input, @@ -27,16 +29,18 @@ export default compose( suffixed = answerSuffix != null, inputError = dirty && error, submitDisabled = !dirty || inputError - return (
{ - this.inputElement = el - }} type="text" - {...input} + key={input.value} + autoFocus + defaultValue={input.value} + onChange={e => { + e.persist() + this.debouncedOnChange(e) + }} className={classnames({ suffixed })} id={'step-' + dottedName} inputMode="numeric" diff --git a/source/utils.js b/source/utils.js index f80f5b52e..6a71efe7c 100644 --- a/source/utils.js +++ b/source/utils.js @@ -9,11 +9,11 @@ export let parseDataAttributes = (value: any) => value === 'undefined' ? undefined : value === null - ? null - : !isNaN(value) - ? +value - : /* value is a normal string */ - value + ? null + : !isNaN(value) + ? +value + : /* value is a normal string */ + value export let getIframeOption = (optionName: string) => { let url = getUrl(),