diff options
author | Francesco Agosti <francesco.agosti93@gmail.com> | 2018-10-27 02:14:00 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-10-27 02:14:00 +0800 |
commit | 4c5b26db183f8f9163e043f5dd4bb0ce60e3aa7f (patch) | |
tree | dbfc6614e901c44d32775b1fe69bb957706c9cf0 | |
parent | d2bf23de71bc2bdfaca14bd7026daa59ca5ef386 (diff) | |
parent | 30809e646be02025d6f9e9ed0ff214d9ace681c8 (diff) | |
download | dexon-sol-tools-4c5b26db183f8f9163e043f5dd4bb0ce60e3aa7f.tar dexon-sol-tools-4c5b26db183f8f9163e043f5dd4bb0ce60e3aa7f.tar.gz dexon-sol-tools-4c5b26db183f8f9163e043f5dd4bb0ce60e3aa7f.tar.bz2 dexon-sol-tools-4c5b26db183f8f9163e043f5dd4bb0ce60e3aa7f.tar.lz dexon-sol-tools-4c5b26db183f8f9163e043f5dd4bb0ce60e3aa7f.tar.xz dexon-sol-tools-4c5b26db183f8f9163e043f5dd4bb0ce60e3aa7f.tar.zst dexon-sol-tools-4c5b26db183f8f9163e043f5dd4bb0ce60e3aa7f.zip |
Merge pull request #1175 from 0xProject/feature/instant/input-fees-rounding
[instant] Create a ScalingInput component and use it in the amount input and upgrade to styled-components v4
22 files changed, 473 insertions, 157 deletions
diff --git a/packages/instant/package.json b/packages/instant/package.json index 421802530..be85b5062 100644 --- a/packages/instant/package.json +++ b/packages/instant/package.json @@ -58,7 +58,7 @@ "react-redux": "^5.0.7", "redux": "^4.0.0", "redux-devtools-extension": "^2.13.5", - "styled-components": "^3.4.9", + "styled-components": "^4.0.2", "ts-optchain": "^0.1.1" }, "devDependencies": { @@ -73,6 +73,7 @@ "@types/react-dom": "^16.0.8", "@types/react-redux": "^6.0.9", "@types/redux": "^3.6.0", + "@types/styled-components": "^4.0.1", "awesome-typescript-loader": "^5.2.1", "enzyme": "^3.6.0", "enzyme-adapter-react-16": "^1.5.0", diff --git a/packages/instant/src/components/amount_input.tsx b/packages/instant/src/components/amount_input.tsx deleted file mode 100644 index c89fb05ad..000000000 --- a/packages/instant/src/components/amount_input.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { BigNumber } from '@0x/utils'; -import * as _ from 'lodash'; -import * as React from 'react'; - -import { ColorOption } from '../style/theme'; -import { util } from '../util/util'; - -import { Container, Input } from './ui'; - -export interface AmountInputProps { - fontColor?: ColorOption; - fontSize?: string; - value?: BigNumber; - onChange: (value?: BigNumber) => void; -} - -export class AmountInput extends React.Component<AmountInputProps> { - public static defaultProps = { - onChange: util.boundNoop, - }; - public render(): React.ReactNode { - const { fontColor, fontSize, value } = this.props; - return ( - <Container borderBottom="1px solid rgba(255,255,255,0.3)" display="inline-block"> - <Input - fontColor={fontColor} - fontSize={fontSize} - onChange={this._handleChange} - value={!_.isUndefined(value) ? value.toString() : ''} - placeholder="0.00" - width="2.2em" - /> - </Container> - ); - } - private readonly _handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => { - const value = event.target.value; - let bigNumberValue; - if (!_.isEmpty(value)) { - try { - bigNumberValue = new BigNumber(event.target.value); - } catch { - // We don't want to allow values that can't be a BigNumber, so don't even call onChange. - return; - } - } - this.props.onChange(bigNumberValue); - }; -} diff --git a/packages/instant/src/components/animations/slide_animations.tsx b/packages/instant/src/components/animations/slide_animations.tsx index 1f10a2ed6..84280372b 100644 --- a/packages/instant/src/components/animations/slide_animations.tsx +++ b/packages/instant/src/components/animations/slide_animations.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; +import { Keyframes } from 'styled-components'; -import { keyframes, styled } from '../../style/theme'; +import { css, keyframes, styled } from '../../style/theme'; const slideKeyframeGenerator = (fromY: string, toY: string) => keyframes` from { @@ -15,7 +16,7 @@ const slideKeyframeGenerator = (fromY: string, toY: string) => keyframes` `; export interface SlideAnimationProps { - keyframes: string; + keyframes: Keyframes; animationType: string; animationDirection?: string; } @@ -24,7 +25,10 @@ export const SlideAnimation = styled.div < SlideAnimationProps > ` - animation-name: ${props => props.keyframes}; + animation-name: ${props => + css` + ${props.keyframes}; + `}; animation-duration: 0.3s; animation-timing-function: ${props => props.animationType}; animation-delay: 0s; diff --git a/packages/instant/src/components/asset_amount_input.tsx b/packages/instant/src/components/asset_amount_input.tsx deleted file mode 100644 index c03ef1cf3..000000000 --- a/packages/instant/src/components/asset_amount_input.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { BigNumber } from '@0x/utils'; -import * as _ from 'lodash'; -import * as React from 'react'; - -import { ColorOption } from '../style/theme'; -import { ERC20Asset } from '../types'; -import { assetUtils } from '../util/asset'; -import { util } from '../util/util'; - -import { AmountInput, AmountInputProps } from './amount_input'; -import { Container, Text } from './ui'; - -// Asset amounts only apply to ERC20 assets -export interface AssetAmountInputProps extends AmountInputProps { - asset?: ERC20Asset; - onChange: (value?: BigNumber, asset?: ERC20Asset) => void; -} - -export class AssetAmountInput extends React.Component<AssetAmountInputProps> { - public static defaultProps = { - onChange: util.boundNoop, - }; - public render(): React.ReactNode { - const { asset, onChange, ...rest } = this.props; - return ( - <Container> - <AmountInput {...rest} onChange={this._handleChange} /> - <Container display="inline-block" marginLeft="10px"> - <Text fontSize={rest.fontSize} fontColor={ColorOption.white} textTransform="uppercase"> - {assetUtils.bestNameForAsset(asset)} - </Text> - </Container> - </Container> - ); - } - private readonly _handleChange = (value?: BigNumber): void => { - this.props.onChange(value, this.props.asset); - }; -} diff --git a/packages/instant/src/components/erc20_asset_amount_input.tsx b/packages/instant/src/components/erc20_asset_amount_input.tsx new file mode 100644 index 000000000..583fad28b --- /dev/null +++ b/packages/instant/src/components/erc20_asset_amount_input.tsx @@ -0,0 +1,84 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import { ColorOption, transparentWhite } from '../style/theme'; +import { ERC20Asset } from '../types'; +import { assetUtils } from '../util/asset'; +import { BigNumberInput } from '../util/big_number_input'; +import { util } from '../util/util'; + +import { ScalingAmountInput } from './scaling_amount_input'; +import { Container, Text } from './ui'; + +// Asset amounts only apply to ERC20 assets +export interface ERC20AssetAmountInputProps { + asset?: ERC20Asset; + value?: BigNumberInput; + onChange: (value?: BigNumberInput, asset?: ERC20Asset) => void; + startingFontSizePx: number; + fontColor?: ColorOption; +} + +export interface ERC20AssetAmountInputState { + currentFontSizePx: number; +} + +export class ERC20AssetAmountInput extends React.Component<ERC20AssetAmountInputProps, ERC20AssetAmountInputState> { + public static defaultProps = { + onChange: util.boundNoop, + }; + constructor(props: ERC20AssetAmountInputProps) { + super(props); + this.state = { + currentFontSizePx: props.startingFontSizePx, + }; + } + public render(): React.ReactNode { + const { asset, onChange, ...rest } = this.props; + return ( + <Container whiteSpace="nowrap"> + <Container borderBottom={`1px solid ${transparentWhite}`} display="inline-block"> + <ScalingAmountInput + {...rest} + textLengthThreshold={this._textLengthThresholdForAsset(asset)} + maxFontSizePx={this.props.startingFontSizePx} + onChange={this._handleChange} + onFontSizeChange={this._handleFontSizeChange} + /> + </Container> + <Container display="inline-flex" marginLeft="10px" title={assetUtils.bestNameForAsset(asset)}> + <Text + fontSize={`${this.state.currentFontSizePx}px`} + fontColor={ColorOption.white} + textTransform="uppercase" + > + {assetUtils.formattedSymbolForAsset(asset)} + </Text> + </Container> + </Container> + ); + } + private readonly _handleChange = (value?: BigNumberInput): void => { + this.props.onChange(value, this.props.asset); + }; + private readonly _handleFontSizeChange = (fontSizePx: number): void => { + this.setState({ + currentFontSizePx: fontSizePx, + }); + }; + // For assets with symbols of different length, + // start scaling the input at different character lengths + private readonly _textLengthThresholdForAsset = (asset?: ERC20Asset): number => { + if (_.isUndefined(asset)) { + return 3; + } + const symbol = asset.metaData.symbol; + if (symbol.length <= 3) { + return 5; + } + if (symbol.length === 5) { + return 3; + } + return 4; + }; +} diff --git a/packages/instant/src/components/instant_heading.tsx b/packages/instant/src/components/instant_heading.tsx index 17ac65429..1ef276ff3 100644 --- a/packages/instant/src/components/instant_heading.tsx +++ b/packages/instant/src/components/instant_heading.tsx @@ -2,7 +2,7 @@ import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import * as React from 'react'; -import { SelectedAssetAmountInput } from '../containers/selected_asset_amount_input'; +import { SelectedERC20AssetAmountInput } from '../containers/selected_erc20_asset_amount_input'; import { ColorOption } from '../style/theme'; import { AsyncProcessState, OrderProcessState, OrderState } from '../types'; import { format } from '../util/format'; @@ -48,7 +48,9 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> { </Text> </Container> <Flex direction="row" justify="space-between"> - <SelectedAssetAmountInput fontSize="45px" /> + <Flex height="60px"> + <SelectedERC20AssetAmountInput startingFontSizePx={38} /> + </Flex> <Flex direction="column" justify="space-between"> {iconOrAmounts} </Flex> diff --git a/packages/instant/src/components/scaling_amount_input.tsx b/packages/instant/src/components/scaling_amount_input.tsx new file mode 100644 index 000000000..655ae2b74 --- /dev/null +++ b/packages/instant/src/components/scaling_amount_input.tsx @@ -0,0 +1,52 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import { ColorOption } from '../style/theme'; +import { BigNumberInput } from '../util/big_number_input'; +import { util } from '../util/util'; + +import { ScalingInput } from './scaling_input'; + +export interface ScalingAmountInputProps { + maxFontSizePx: number; + textLengthThreshold: number; + fontColor?: ColorOption; + value?: BigNumberInput; + onChange: (value?: BigNumberInput) => void; + onFontSizeChange: (fontSizePx: number) => void; +} + +export class ScalingAmountInput extends React.Component<ScalingAmountInputProps> { + public static defaultProps = { + onChange: util.boundNoop, + onFontSizeChange: util.boundNoop, + }; + public render(): React.ReactNode { + const { textLengthThreshold, fontColor, maxFontSizePx, value, onFontSizeChange } = this.props; + return ( + <ScalingInput + maxFontSizePx={maxFontSizePx} + textLengthThreshold={textLengthThreshold} + onFontSizeChange={onFontSizeChange} + fontColor={fontColor} + onChange={this._handleChange} + value={!_.isUndefined(value) ? value.toDisplayString() : ''} + placeholder="0.00" + emptyInputWidthCh={3.5} + /> + ); + } + private readonly _handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => { + const value = event.target.value; + let bigNumberValue; + if (!_.isEmpty(value)) { + try { + bigNumberValue = new BigNumberInput(value); + } catch { + // We don't want to allow values that can't be a BigNumber, so don't even call onChange. + return; + } + } + this.props.onChange(bigNumberValue); + }; +} diff --git a/packages/instant/src/components/scaling_input.tsx b/packages/instant/src/components/scaling_input.tsx new file mode 100644 index 000000000..34cb0b5fd --- /dev/null +++ b/packages/instant/src/components/scaling_input.tsx @@ -0,0 +1,170 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import { ColorOption } from '../style/theme'; +import { util } from '../util/util'; + +import { Input } from './ui'; + +export enum ScalingInputPhase { + FixedFontSize, + ScalingFontSize, +} + +export interface ScalingSettings { + percentageToReduceFontSizePerCharacter: number; + constantPxToIncreaseWidthPerCharacter: number; +} + +export interface ScalingInputProps { + textLengthThreshold: number; + maxFontSizePx: number; + value: string; + emptyInputWidthCh: number; + onChange: (event: React.ChangeEvent<HTMLInputElement>) => void; + onFontSizeChange: (fontSizePx: number) => void; + fontColor?: ColorOption; + placeholder?: string; + maxLength?: number; + scalingSettings: ScalingSettings; +} + +export interface ScalingInputState { + inputWidthPxAtPhaseChange?: number; +} + +export interface ScalingInputSnapshot { + inputWidthPx: number; +} + +// These are magic numbers that were determined experimentally. +const defaultScalingSettings: ScalingSettings = { + percentageToReduceFontSizePerCharacter: 0.125, + constantPxToIncreaseWidthPerCharacter: 4, +}; + +export class ScalingInput extends React.Component<ScalingInputProps, ScalingInputState> { + public static defaultProps = { + onChange: util.boundNoop, + onFontSizeChange: util.boundNoop, + maxLength: 7, + scalingSettings: defaultScalingSettings, + }; + public state: ScalingInputState = { + inputWidthPxAtPhaseChange: undefined, + }; + private readonly _inputRef = React.createRef<HTMLInputElement>(); + public static getPhase(textLengthThreshold: number, value: string): ScalingInputPhase { + if (value.length <= textLengthThreshold) { + return ScalingInputPhase.FixedFontSize; + } + return ScalingInputPhase.ScalingFontSize; + } + public static getPhaseFromProps(props: ScalingInputProps): ScalingInputPhase { + const { value, textLengthThreshold } = props; + return ScalingInput.getPhase(textLengthThreshold, value); + } + public static calculateFontSize( + textLengthThreshold: number, + maxFontSizePx: number, + phase: ScalingInputPhase, + value: string, + percentageToReduceFontSizePerCharacter: number, + ): number { + if (phase !== ScalingInputPhase.ScalingFontSize) { + return maxFontSizePx; + } + const charactersOverMax = value.length - textLengthThreshold; + const scalingFactor = (1 - percentageToReduceFontSizePerCharacter) ** charactersOverMax; + const fontSize = scalingFactor * maxFontSizePx; + return fontSize; + } + public static calculateFontSizeFromProps(props: ScalingInputProps, phase: ScalingInputPhase): number { + const { textLengthThreshold, value, maxFontSizePx, scalingSettings } = props; + return ScalingInput.calculateFontSize( + textLengthThreshold, + maxFontSizePx, + phase, + value, + scalingSettings.percentageToReduceFontSizePerCharacter, + ); + } + public getSnapshotBeforeUpdate(): ScalingInputSnapshot { + return { + inputWidthPx: this._getInputWidthInPx(), + }; + } + public componentDidUpdate( + prevProps: ScalingInputProps, + prevState: ScalingInputState, + snapshot: ScalingInputSnapshot, + ): void { + const prevPhase = ScalingInput.getPhaseFromProps(prevProps); + const curPhase = ScalingInput.getPhaseFromProps(this.props); + // if we went from fixed to scaling, save the width from the transition + if (prevPhase !== ScalingInputPhase.ScalingFontSize && curPhase === ScalingInputPhase.ScalingFontSize) { + this.setState({ + inputWidthPxAtPhaseChange: snapshot.inputWidthPx, + }); + } + // if we went from scaling to fixed, revert back to scaling using `ch` + if (prevPhase === ScalingInputPhase.ScalingFontSize && curPhase !== ScalingInputPhase.ScalingFontSize) { + this.setState({ + inputWidthPxAtPhaseChange: undefined, + }); + } + const prevFontSize = ScalingInput.calculateFontSizeFromProps(prevProps, prevPhase); + const curFontSize = ScalingInput.calculateFontSizeFromProps(this.props, curPhase); + // If font size has changed, notify. + if (prevFontSize !== curFontSize) { + this.props.onFontSizeChange(curFontSize); + } + } + public render(): React.ReactNode { + const { fontColor, onChange, placeholder, value, maxLength } = this.props; + const phase = ScalingInput.getPhaseFromProps(this.props); + return ( + <Input + ref={this._inputRef as any} + fontColor={fontColor} + onChange={onChange} + value={value} + placeholder={placeholder} + fontSize={`${this._calculateFontSize(phase)}px`} + width={this._calculateWidth(phase)} + maxLength={maxLength} + /> + ); + } + private readonly _calculateWidth = (phase: ScalingInputPhase): string => { + const { value, textLengthThreshold, scalingSettings } = this.props; + if (_.isEmpty(value)) { + return `${this.props.emptyInputWidthCh}ch`; + } + switch (phase) { + case ScalingInputPhase.FixedFontSize: + return `${value.length}ch`; + case ScalingInputPhase.ScalingFontSize: + const { inputWidthPxAtPhaseChange } = this.state; + if (!_.isUndefined(inputWidthPxAtPhaseChange)) { + const charactersOverMax = value.length - textLengthThreshold; + const scalingAmount = scalingSettings.constantPxToIncreaseWidthPerCharacter * charactersOverMax; + const width = inputWidthPxAtPhaseChange + scalingAmount; + return `${width}px`; + } + return `${textLengthThreshold}ch`; + default: + return '1ch'; + } + }; + private readonly _calculateFontSize = (phase: ScalingInputPhase): number => { + return ScalingInput.calculateFontSizeFromProps(this.props, phase); + }; + private readonly _getInputWidthInPx = (): number => { + const ref = this._inputRef.current; + if (!ref) { + return 0; + } + return ref.getBoundingClientRect().width; + }; +} diff --git a/packages/instant/src/components/ui/container.tsx b/packages/instant/src/components/ui/container.tsx index 5e2218c68..76b570de7 100644 --- a/packages/instant/src/components/ui/container.tsx +++ b/packages/instant/src/components/ui/container.tsx @@ -1,5 +1,3 @@ -import * as React from 'react'; - import { ColorOption, styled } from '../../style/theme'; import { cssRuleIfExists } from '../../style/util'; @@ -11,6 +9,7 @@ export interface ContainerProps { bottom?: string; left?: string; width?: string; + height?: string; maxWidth?: string; margin?: string; marginTop?: string; @@ -27,14 +26,14 @@ export interface ContainerProps { backgroundColor?: ColorOption; hasBoxShadow?: boolean; zIndex?: number; + whiteSpace?: string; opacity?: number; } -const PlainContainer: React.StatelessComponent<ContainerProps> = ({ children, className }) => ( - <div className={className}>{children}</div> -); - -export const Container = styled(PlainContainer)` +export const Container = + styled.div < + ContainerProps > + ` box-sizing: border-box; ${props => cssRuleIfExists(props, 'display')} ${props => cssRuleIfExists(props, 'position')} @@ -43,6 +42,7 @@ export const Container = styled(PlainContainer)` ${props => cssRuleIfExists(props, 'bottom')} ${props => cssRuleIfExists(props, 'left')} ${props => cssRuleIfExists(props, 'width')} + ${props => cssRuleIfExists(props, 'height')} ${props => cssRuleIfExists(props, 'max-width')} ${props => cssRuleIfExists(props, 'margin')} ${props => cssRuleIfExists(props, 'margin-top')} @@ -55,6 +55,7 @@ export const Container = styled(PlainContainer)` ${props => cssRuleIfExists(props, 'border-top')} ${props => cssRuleIfExists(props, 'border-bottom')} ${props => cssRuleIfExists(props, 'z-index')} + ${props => cssRuleIfExists(props, 'white-space')} ${props => cssRuleIfExists(props, 'opacity')} ${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')}; background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')}; diff --git a/packages/instant/src/components/ui/flex.tsx b/packages/instant/src/components/ui/flex.tsx index 327e91926..5fa3fc95b 100644 --- a/packages/instant/src/components/ui/flex.tsx +++ b/packages/instant/src/components/ui/flex.tsx @@ -1,5 +1,3 @@ -import * as React from 'react'; - import { ColorOption, styled } from '../../style/theme'; import { cssRuleIfExists } from '../../style/util'; @@ -9,21 +7,22 @@ export interface FlexProps { justify?: 'flex-start' | 'center' | 'space-around' | 'space-between' | 'space-evenly' | 'flex-end'; align?: 'flex-start' | 'center' | 'space-around' | 'space-between' | 'space-evenly' | 'flex-end'; width?: string; + height?: string; backgroundColor?: ColorOption; className?: string; } -const PlainFlex: React.StatelessComponent<FlexProps> = ({ children, className }) => ( - <div className={className}>{children}</div> -); - -export const Flex = styled(PlainFlex)` +export const Flex = + styled.div < + FlexProps > + ` display: flex; flex-direction: ${props => props.direction}; flex-wrap: ${props => props.flexWrap}; justify-content: ${props => props.justify}; align-items: ${props => props.align}; ${props => cssRuleIfExists(props, 'width')} + ${props => cssRuleIfExists(props, 'height')} background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')}; `; diff --git a/packages/instant/src/components/ui/input.tsx b/packages/instant/src/components/ui/input.tsx index f8c6b6ef6..a884ff7cb 100644 --- a/packages/instant/src/components/ui/input.tsx +++ b/packages/instant/src/components/ui/input.tsx @@ -12,11 +12,10 @@ export interface InputProps { onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void; } -const PlainInput: React.StatelessComponent<InputProps> = ({ value, className, placeholder, onChange }) => ( - <input className={className} value={value} onChange={onChange} placeholder={placeholder} /> -); - -export const Input = styled(PlainInput)` +export const Input = + styled.input < + InputProps > + ` font-size: ${props => props.fontSize}; width: ${props => props.width}; padding: 0.1em 0em; diff --git a/packages/instant/src/components/ui/text.tsx b/packages/instant/src/components/ui/text.tsx index 9fb8ea26f..fd72f6cc8 100644 --- a/packages/instant/src/components/ui/text.tsx +++ b/packages/instant/src/components/ui/text.tsx @@ -23,14 +23,11 @@ export interface TextProps { display?: string; } -const PlainText: React.StatelessComponent<TextProps> = ({ children, className, onClick }) => ( - <div className={className} onClick={onClick}> - {children} - </div> -); - const darkenOnHoverAmount = 0.3; -export const Text = styled(PlainText)` +export const Text = + styled.div < + TextProps > + ` font-family: ${props => props.fontFamily}; font-style: ${props => props.fontStyle}; font-weight: ${props => props.fontWeight}; diff --git a/packages/instant/src/containers/selected_asset_amount_input.ts b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts index e9dbc61ce..ee76e9d66 100644 --- a/packages/instant/src/containers/selected_asset_amount_input.ts +++ b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts @@ -11,34 +11,35 @@ import { Action, actions } from '../redux/actions'; import { State } from '../redux/reducer'; import { ColorOption } from '../style/theme'; import { ERC20Asset, OrderProcessState } from '../types'; +import { BigNumberInput } from '../util/big_number_input'; import { errorUtil } from '../util/error'; -import { AssetAmountInput } from '../components/asset_amount_input'; +import { ERC20AssetAmountInput } from '../components/erc20_asset_amount_input'; -export interface SelectedAssetAmountInputProps { +export interface SelectedERC20AssetAmountInputProps { fontColor?: ColorOption; - fontSize?: string; + startingFontSizePx: number; } interface ConnectedState { assetBuyer?: AssetBuyer; - value?: BigNumber; + value?: BigNumberInput; asset?: ERC20Asset; } interface ConnectedDispatch { - updateBuyQuote: (assetBuyer?: AssetBuyer, value?: BigNumber, asset?: ERC20Asset) => void; + updateBuyQuote: (assetBuyer?: AssetBuyer, value?: BigNumberInput, asset?: ERC20Asset) => void; } interface ConnectedProps { - value?: BigNumber; + value?: BigNumberInput; asset?: ERC20Asset; - onChange: (value?: BigNumber, asset?: ERC20Asset) => void; + onChange: (value?: BigNumberInput, asset?: ERC20Asset) => void; } -type FinalProps = ConnectedProps & SelectedAssetAmountInputProps; +type FinalProps = ConnectedProps & SelectedERC20AssetAmountInputProps; -const mapStateToProps = (state: State, _ownProps: SelectedAssetAmountInputProps): ConnectedState => { +const mapStateToProps = (state: State, _ownProps: SelectedERC20AssetAmountInputProps): ConnectedState => { const selectedAsset = state.selectedAsset; if (_.isUndefined(selectedAsset) || selectedAsset.metaData.assetProxyId !== AssetProxyId.ERC20) { return { @@ -82,7 +83,7 @@ const debouncedUpdateBuyQuoteAsync = _.debounce(updateBuyQuoteAsync, 200, { trai const mapDispatchToProps = ( dispatch: Dispatch<Action>, - _ownProps: SelectedAssetAmountInputProps, + _ownProps: SelectedERC20AssetAmountInputProps, ): ConnectedDispatch => ({ updateBuyQuote: (assetBuyer, value, asset) => { // Update the input @@ -104,7 +105,7 @@ const mapDispatchToProps = ( const mergeProps = ( connectedState: ConnectedState, connectedDispatch: ConnectedDispatch, - ownProps: SelectedAssetAmountInputProps, + ownProps: SelectedERC20AssetAmountInputProps, ): FinalProps => { return { ...ownProps, @@ -116,8 +117,8 @@ const mergeProps = ( }; }; -export const SelectedAssetAmountInput: React.ComponentClass<SelectedAssetAmountInputProps> = connect( +export const SelectedERC20AssetAmountInput: React.ComponentClass<SelectedERC20AssetAmountInputProps> = connect( mapStateToProps, mapDispatchToProps, mergeProps, -)(AssetAmountInput); +)(ERC20AssetAmountInput); diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts index 5a4099f15..46045024b 100644 --- a/packages/instant/src/redux/actions.ts +++ b/packages/instant/src/redux/actions.ts @@ -2,6 +2,8 @@ import { BuyQuote } from '@0x/asset-buyer'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; +import { BigNumberInput } from '../util/big_number_input'; + import { ActionsUnion, OrderState } from '../types'; export interface PlainAction<T extends string> { @@ -36,7 +38,8 @@ export enum ActionTypes { export const actions = { updateEthUsdPrice: (price?: BigNumber) => createAction(ActionTypes.UPDATE_ETH_USD_PRICE, price), - updateSelectedAssetAmount: (amount?: BigNumber) => createAction(ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT, amount), + updateSelectedAssetAmount: (amount?: BigNumberInput) => + createAction(ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT, amount), updateBuyOrderState: (orderState: OrderState) => createAction(ActionTypes.UPDATE_BUY_ORDER_STATE, orderState), updateLatestBuyQuote: (buyQuote?: BuyQuote) => createAction(ActionTypes.UPDATE_LATEST_BUY_QUOTE, buyQuote), updateSelectedAsset: (assetData?: string) => createAction(ActionTypes.UPDATE_SELECTED_ASSET, assetData), diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts index 25d0092b2..d7e5bdfb5 100644 --- a/packages/instant/src/redux/reducer.ts +++ b/packages/instant/src/redux/reducer.ts @@ -14,6 +14,7 @@ import { OrderState, } from '../types'; import { assetUtils } from '../util/asset'; +import { BigNumberInput } from '../util/big_number_input'; import { Action, ActionTypes } from './actions'; @@ -22,7 +23,7 @@ export interface State { assetBuyer?: AssetBuyer; assetMetaDataMap: ObjectMap<AssetMetaData>; selectedAsset?: Asset; - selectedAssetAmount?: BigNumber; + selectedAssetAmount?: BigNumberInput; buyOrderState: OrderState; ethUsdPrice?: BigNumber; latestBuyQuote?: BuyQuote; diff --git a/packages/instant/src/style/fonts.ts b/packages/instant/src/style/fonts.ts index 975a30a61..92450502d 100644 --- a/packages/instant/src/style/fonts.ts +++ b/packages/instant/src/style/fonts.ts @@ -1,10 +1,10 @@ -import { injectGlobal } from './theme'; - export const fonts = { include: () => { // Inject the inter-ui font into the page - return injectGlobal` - @import url('https://rsms.me/inter/inter-ui.css'); - `; + const appendTo = document.head || document.getElementsByTagName('head')[0] || document.body; + const style = document.createElement('style'); + style.type = 'text/css'; + style.appendChild(document.createTextNode(`@import url('https://rsms.me/inter/inter-ui.css')`)); + appendTo.appendChild(style); }, }; diff --git a/packages/instant/src/style/theme.ts b/packages/instant/src/style/theme.ts index d26c816c1..6575ff9f4 100644 --- a/packages/instant/src/style/theme.ts +++ b/packages/instant/src/style/theme.ts @@ -1,6 +1,6 @@ import * as styledComponents from 'styled-components'; -const { default: styled, css, injectGlobal, keyframes, ThemeProvider } = styledComponents; +const { default: styled, css, keyframes, ThemeProvider } = styledComponents; export type Theme = { [key in ColorOption]: string }; @@ -28,4 +28,6 @@ export const theme: Theme = { darkOrange: '#F2994C', }; -export { styled, css, injectGlobal, keyframes, ThemeProvider }; +export const transparentWhite = 'rgba(255,255,255,0.3)'; + +export { styled, css, keyframes, ThemeProvider }; diff --git a/packages/instant/src/util/asset.ts b/packages/instant/src/util/asset.ts index 4e3b2b946..2c5b6325d 100644 --- a/packages/instant/src/util/asset.ts +++ b/packages/instant/src/util/asset.ts @@ -2,7 +2,7 @@ import { AssetProxyId, ObjectMap } from '@0x/types'; import * as _ from 'lodash'; import { assetDataNetworkMapping } from '../data/asset_data_network_mapping'; -import { Asset, AssetMetaData, Network, ZeroExInstantError } from '../types'; +import { Asset, AssetMetaData, ERC20Asset, Network, ZeroExInstantError } from '../types'; export const assetUtils = { createAssetFromAssetData: ( @@ -43,6 +43,16 @@ export const assetUtils = { return defaultName; } }, + formattedSymbolForAsset: (asset?: ERC20Asset, defaultName: string = '???'): string => { + if (_.isUndefined(asset)) { + return defaultName; + } + const symbol = asset.metaData.symbol; + if (symbol.length <= 5) { + return symbol; + } + return `${symbol.slice(0, 3)}…`; + }, getAssociatedAssetDataIfExists: (assetData: string, network: Network): string | undefined => { const assetDataGroupIfExists = _.find(assetDataNetworkMapping, value => value[network] === assetData); if (_.isUndefined(assetDataGroupIfExists)) { diff --git a/packages/instant/src/util/big_number_input.ts b/packages/instant/src/util/big_number_input.ts new file mode 100644 index 000000000..d2a9a8dc5 --- /dev/null +++ b/packages/instant/src/util/big_number_input.ts @@ -0,0 +1,29 @@ +import { BigNumber } from '@0x/utils'; +import * as _ from 'lodash'; + +/** + * A BigNumber extension that is more flexible about decimal strings. + * Such as allowing: + * new BigNumberInput('0.') => 0 + * new BigNumberInput('1.') => 1 + * new BigNumberInput('1..') => still throws + */ +export class BigNumberInput extends BigNumber { + private readonly _isEndingWithDecimal: boolean; + constructor(bigNumberString: string) { + const hasDecimalPeriod = _.endsWith(bigNumberString, '.'); + let internalString = bigNumberString; + if (hasDecimalPeriod) { + internalString = bigNumberString.slice(0, -1); + } + super(internalString); + this._isEndingWithDecimal = hasDecimalPeriod; + } + public toDisplayString(): string { + const internalString = super.toString(); + if (this._isEndingWithDecimal) { + return `${internalString}.`; + } + return internalString; + } +} diff --git a/packages/instant/src/util/format.ts b/packages/instant/src/util/format.ts index 8482b1526..ca7c01359 100644 --- a/packages/instant/src/util/format.ts +++ b/packages/instant/src/util/format.ts @@ -24,7 +24,7 @@ export const format = { if (_.isUndefined(ethUnitAmount)) { return defaultText; } - const roundedAmount = ethUnitAmount.round(decimalPlaces); + const roundedAmount = ethUnitAmount.round(decimalPlaces).toDigits(decimalPlaces); return `${roundedAmount} ETH`; }, ethBaseAmountInUsd: ( diff --git a/packages/instant/test/util/format.test.ts b/packages/instant/test/util/format.test.ts index 141df9275..2c9294c78 100644 --- a/packages/instant/test/util/format.test.ts +++ b/packages/instant/test/util/format.test.ts @@ -20,8 +20,8 @@ describe('format', () => { it('converts .432414 ETH in base units to the string `.4324 ETH`', () => { expect(format.ethBaseAmount(DECIMAL_ETH_IN_BASE_UNITS)).toBe('0.4324 ETH'); }); - it('converts 5.3014059295032 ETH in base units to the string `5.3014 ETH`', () => { - expect(format.ethBaseAmount(IRRATIONAL_ETH_IN_BASE_UNITS)).toBe('5.3014 ETH'); + it('converts 5.3014059295032 ETH in base units to the string `5.301 ETH`', () => { + expect(format.ethBaseAmount(IRRATIONAL_ETH_IN_BASE_UNITS)).toBe('5.301 ETH'); }); it('returns defaultText param when ethBaseAmount is not defined', () => { const defaultText = 'defaultText'; @@ -38,8 +38,8 @@ describe('format', () => { it('converts BigNumer(.432414) to the string `.4324 ETH`', () => { expect(format.ethUnitAmount(BIG_NUMBER_DECIMAL)).toBe('0.4324 ETH'); }); - it('converts BigNumber(5.3014059295032) to the string `5.3014 ETH`', () => { - expect(format.ethUnitAmount(BIG_NUMBER_IRRATIONAL)).toBe('5.3014 ETH'); + it('converts BigNumber(5.3014059295032) to the string `5.301 ETH`', () => { + expect(format.ethUnitAmount(BIG_NUMBER_IRRATIONAL)).toBe('5.301 ETH'); }); it('returns defaultText param when ethUnitAmount is not defined', () => { const defaultText = 'defaultText'; @@ -484,6 +484,12 @@ dependencies: "@babel/highlight" "^7.0.0" +"@babel/helper-annotate-as-pure@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" + dependencies: + "@babel/types" "^7.0.0" + "@babel/highlight@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" @@ -504,6 +510,24 @@ dependencies: regenerator-runtime "^0.12.0" +"@babel/types@^7.0.0": + version "7.1.3" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.1.3.tgz#3a767004567060c2f40fca49a304712c525ee37d" + dependencies: + esutils "^2.0.2" + lodash "^4.17.10" + to-fast-properties "^2.0.0" + +"@emotion/is-prop-valid@^0.6.8": + version "0.6.8" + resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.6.8.tgz#68ad02831da41213a2089d2cab4e8ac8b30cbd85" + dependencies: + "@emotion/memoize" "^0.6.6" + +"@emotion/memoize@^0.6.6": + version "0.6.6" + resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.6.6.tgz#004b98298d04c7ca3b4f50ca2035d4f60d2eed1b" + "@ledgerhq/hw-app-eth@^4.3.0": version "4.7.3" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-4.7.3.tgz#d352e19658ae296532e522c53c8ec2a1a77b64e5" @@ -1579,6 +1603,13 @@ "@types/node" "*" "@types/react" "*" +"@types/styled-components@^4.0.1": + version "4.0.1" + resolved "https://registry.npmjs.org/@types/styled-components/-/styled-components-4.0.1.tgz#5eb9a5474dbde3becab2bcc8f04e0b8e8dcf8c06" + dependencies: + "@types/node" "*" + "@types/react" "*" + "@types/tmp@^0.0.33": version "0.0.33" resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.0.33.tgz#1073c4bc824754ae3d10cfab88ab0237ba964e4d" @@ -2532,6 +2563,13 @@ babel-plugin-jest-hoist@^23.2.0: version "23.2.0" resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz#e61fae05a1ca8801aadee57a6d66b8cefaf44167" +"babel-plugin-styled-components@>= 1": + version "1.8.0" + resolved "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.8.0.tgz#9dd054c8e86825203449a852a5746f29f2dab857" + dependencies: + "@babel/helper-annotate-as-pure" "^7.0.0" + lodash "^4.17.10" + babel-plugin-syntax-async-functions@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" @@ -4581,6 +4619,14 @@ css-to-react-native@^2.0.3: fbjs "^0.8.5" postcss-value-parser "^3.3.0" +css-to-react-native@^2.2.2: + version "2.2.2" + resolved "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-2.2.2.tgz#c077d0f7bf3e6c915a539e7325821c9dd01f9965" + dependencies: + css-color-keywords "^1.0.0" + fbjs "^0.8.5" + postcss-value-parser "^3.3.0" + css-vendor@^0.3.8: version "0.3.8" resolved "https://registry.npmjs.org/css-vendor/-/css-vendor-0.3.8.tgz#6421cfd3034ce664fe7673972fd0119fc28941fa" @@ -6705,7 +6751,7 @@ ganache-core@0xProject/ganache-core#monorepo-dep: ethereumjs-tx "0xProject/ethereumjs-tx#fake-tx-include-signature-by-default" ethereumjs-util "^5.2.0" ethereumjs-vm "2.3.5" - ethereumjs-wallet "0.6.0" + ethereumjs-wallet "~0.6.0" fake-merkle-patricia-tree "~1.0.1" heap "~0.2.6" js-scrypt "^0.2.0" @@ -14394,19 +14440,18 @@ styled-components@^3.3.3: stylis-rule-sheet "^0.0.10" supports-color "^3.2.3" -styled-components@^3.4.9: - version "3.4.10" - resolved "https://registry.npmjs.org/styled-components/-/styled-components-3.4.10.tgz#9a654c50ea2b516c36ade57ddcfa296bf85c96e1" +styled-components@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/styled-components/-/styled-components-4.0.2.tgz#7d4409ada019cdd34c25ba68c4577ea95dbcf0c5" dependencies: - buffer "^5.0.3" - css-to-react-native "^2.0.3" - fbjs "^0.8.16" - hoist-non-react-statics "^2.5.0" + "@emotion/is-prop-valid" "^0.6.8" + babel-plugin-styled-components ">= 1" + css-to-react-native "^2.2.2" + memoize-one "^4.0.0" prop-types "^15.5.4" react-is "^16.3.1" stylis "^3.5.0" stylis-rule-sheet "^0.0.10" - supports-color "^3.2.3" stylis-rule-sheet@^0.0.10: version "0.0.10" @@ -14849,6 +14894,10 @@ to-fast-properties@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + to-no-case@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/to-no-case/-/to-no-case-1.0.2.tgz#c722907164ef6b178132c8e69930212d1b4aa16a" |