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
This commit is contained in:
Francesco Agosti 2018-10-26 11:14:00 -07:00 committed by GitHub
commit 4c5b26db18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 473 additions and 157 deletions

View File

@ -58,7 +58,7 @@
"react-redux": "^5.0.7", "react-redux": "^5.0.7",
"redux": "^4.0.0", "redux": "^4.0.0",
"redux-devtools-extension": "^2.13.5", "redux-devtools-extension": "^2.13.5",
"styled-components": "^3.4.9", "styled-components": "^4.0.2",
"ts-optchain": "^0.1.1" "ts-optchain": "^0.1.1"
}, },
"devDependencies": { "devDependencies": {
@ -73,6 +73,7 @@
"@types/react-dom": "^16.0.8", "@types/react-dom": "^16.0.8",
"@types/react-redux": "^6.0.9", "@types/react-redux": "^6.0.9",
"@types/redux": "^3.6.0", "@types/redux": "^3.6.0",
"@types/styled-components": "^4.0.1",
"awesome-typescript-loader": "^5.2.1", "awesome-typescript-loader": "^5.2.1",
"enzyme": "^3.6.0", "enzyme": "^3.6.0",
"enzyme-adapter-react-16": "^1.5.0", "enzyme-adapter-react-16": "^1.5.0",

View File

@ -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);
};
}

View File

@ -1,6 +1,7 @@
import * as React from 'react'; 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` const slideKeyframeGenerator = (fromY: string, toY: string) => keyframes`
from { from {
@ -15,7 +16,7 @@ const slideKeyframeGenerator = (fromY: string, toY: string) => keyframes`
`; `;
export interface SlideAnimationProps { export interface SlideAnimationProps {
keyframes: string; keyframes: Keyframes;
animationType: string; animationType: string;
animationDirection?: string; animationDirection?: string;
} }
@ -24,7 +25,10 @@ export const SlideAnimation =
styled.div < styled.div <
SlideAnimationProps > SlideAnimationProps >
` `
animation-name: ${props => props.keyframes}; animation-name: ${props =>
css`
${props.keyframes};
`};
animation-duration: 0.3s; animation-duration: 0.3s;
animation-timing-function: ${props => props.animationType}; animation-timing-function: ${props => props.animationType};
animation-delay: 0s; animation-delay: 0s;

View File

@ -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);
};
}

View File

@ -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;
};
}

View File

@ -2,7 +2,7 @@ import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as React from 'react'; 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 { ColorOption } from '../style/theme';
import { AsyncProcessState, OrderProcessState, OrderState } from '../types'; import { AsyncProcessState, OrderProcessState, OrderState } from '../types';
import { format } from '../util/format'; import { format } from '../util/format';
@ -48,7 +48,9 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> {
</Text> </Text>
</Container> </Container>
<Flex direction="row" justify="space-between"> <Flex direction="row" justify="space-between">
<SelectedAssetAmountInput fontSize="45px" /> <Flex height="60px">
<SelectedERC20AssetAmountInput startingFontSizePx={38} />
</Flex>
<Flex direction="column" justify="space-between"> <Flex direction="column" justify="space-between">
{iconOrAmounts} {iconOrAmounts}
</Flex> </Flex>

View File

@ -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);
};
}

View File

@ -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;
};
}

View File

@ -1,5 +1,3 @@
import * as React from 'react';
import { ColorOption, styled } from '../../style/theme'; import { ColorOption, styled } from '../../style/theme';
import { cssRuleIfExists } from '../../style/util'; import { cssRuleIfExists } from '../../style/util';
@ -11,6 +9,7 @@ export interface ContainerProps {
bottom?: string; bottom?: string;
left?: string; left?: string;
width?: string; width?: string;
height?: string;
maxWidth?: string; maxWidth?: string;
margin?: string; margin?: string;
marginTop?: string; marginTop?: string;
@ -27,14 +26,14 @@ export interface ContainerProps {
backgroundColor?: ColorOption; backgroundColor?: ColorOption;
hasBoxShadow?: boolean; hasBoxShadow?: boolean;
zIndex?: number; zIndex?: number;
whiteSpace?: string;
opacity?: number; opacity?: number;
} }
const PlainContainer: React.StatelessComponent<ContainerProps> = ({ children, className }) => ( export const Container =
<div className={className}>{children}</div> styled.div <
); ContainerProps >
`
export const Container = styled(PlainContainer)`
box-sizing: border-box; box-sizing: border-box;
${props => cssRuleIfExists(props, 'display')} ${props => cssRuleIfExists(props, 'display')}
${props => cssRuleIfExists(props, 'position')} ${props => cssRuleIfExists(props, 'position')}
@ -43,6 +42,7 @@ export const Container = styled(PlainContainer)`
${props => cssRuleIfExists(props, 'bottom')} ${props => cssRuleIfExists(props, 'bottom')}
${props => cssRuleIfExists(props, 'left')} ${props => cssRuleIfExists(props, 'left')}
${props => cssRuleIfExists(props, 'width')} ${props => cssRuleIfExists(props, 'width')}
${props => cssRuleIfExists(props, 'height')}
${props => cssRuleIfExists(props, 'max-width')} ${props => cssRuleIfExists(props, 'max-width')}
${props => cssRuleIfExists(props, 'margin')} ${props => cssRuleIfExists(props, 'margin')}
${props => cssRuleIfExists(props, 'margin-top')} ${props => cssRuleIfExists(props, 'margin-top')}
@ -55,6 +55,7 @@ export const Container = styled(PlainContainer)`
${props => cssRuleIfExists(props, 'border-top')} ${props => cssRuleIfExists(props, 'border-top')}
${props => cssRuleIfExists(props, 'border-bottom')} ${props => cssRuleIfExists(props, 'border-bottom')}
${props => cssRuleIfExists(props, 'z-index')} ${props => cssRuleIfExists(props, 'z-index')}
${props => cssRuleIfExists(props, 'white-space')}
${props => cssRuleIfExists(props, 'opacity')} ${props => cssRuleIfExists(props, 'opacity')}
${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')}; ${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')};
background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')}; background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')};

View File

@ -1,5 +1,3 @@
import * as React from 'react';
import { ColorOption, styled } from '../../style/theme'; import { ColorOption, styled } from '../../style/theme';
import { cssRuleIfExists } from '../../style/util'; import { cssRuleIfExists } from '../../style/util';
@ -9,21 +7,22 @@ export interface FlexProps {
justify?: 'flex-start' | 'center' | 'space-around' | 'space-between' | 'space-evenly' | 'flex-end'; justify?: 'flex-start' | 'center' | 'space-around' | 'space-between' | 'space-evenly' | 'flex-end';
align?: '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; width?: string;
height?: string;
backgroundColor?: ColorOption; backgroundColor?: ColorOption;
className?: string; className?: string;
} }
const PlainFlex: React.StatelessComponent<FlexProps> = ({ children, className }) => ( export const Flex =
<div className={className}>{children}</div> styled.div <
); FlexProps >
`
export const Flex = styled(PlainFlex)`
display: flex; display: flex;
flex-direction: ${props => props.direction}; flex-direction: ${props => props.direction};
flex-wrap: ${props => props.flexWrap}; flex-wrap: ${props => props.flexWrap};
justify-content: ${props => props.justify}; justify-content: ${props => props.justify};
align-items: ${props => props.align}; align-items: ${props => props.align};
${props => cssRuleIfExists(props, 'width')} ${props => cssRuleIfExists(props, 'width')}
${props => cssRuleIfExists(props, 'height')}
background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')}; background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')};
`; `;

View File

@ -12,11 +12,10 @@ export interface InputProps {
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void; onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
} }
const PlainInput: React.StatelessComponent<InputProps> = ({ value, className, placeholder, onChange }) => ( export const Input =
<input className={className} value={value} onChange={onChange} placeholder={placeholder} /> styled.input <
); InputProps >
`
export const Input = styled(PlainInput)`
font-size: ${props => props.fontSize}; font-size: ${props => props.fontSize};
width: ${props => props.width}; width: ${props => props.width};
padding: 0.1em 0em; padding: 0.1em 0em;

View File

@ -23,14 +23,11 @@ export interface TextProps {
display?: string; display?: string;
} }
const PlainText: React.StatelessComponent<TextProps> = ({ children, className, onClick }) => (
<div className={className} onClick={onClick}>
{children}
</div>
);
const darkenOnHoverAmount = 0.3; const darkenOnHoverAmount = 0.3;
export const Text = styled(PlainText)` export const Text =
styled.div <
TextProps >
`
font-family: ${props => props.fontFamily}; font-family: ${props => props.fontFamily};
font-style: ${props => props.fontStyle}; font-style: ${props => props.fontStyle};
font-weight: ${props => props.fontWeight}; font-weight: ${props => props.fontWeight};

View File

@ -11,34 +11,35 @@ import { Action, actions } from '../redux/actions';
import { State } from '../redux/reducer'; import { State } from '../redux/reducer';
import { ColorOption } from '../style/theme'; import { ColorOption } from '../style/theme';
import { ERC20Asset, OrderProcessState } from '../types'; import { ERC20Asset, OrderProcessState } from '../types';
import { BigNumberInput } from '../util/big_number_input';
import { errorUtil } from '../util/error'; 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; fontColor?: ColorOption;
fontSize?: string; startingFontSizePx: number;
} }
interface ConnectedState { interface ConnectedState {
assetBuyer?: AssetBuyer; assetBuyer?: AssetBuyer;
value?: BigNumber; value?: BigNumberInput;
asset?: ERC20Asset; asset?: ERC20Asset;
} }
interface ConnectedDispatch { interface ConnectedDispatch {
updateBuyQuote: (assetBuyer?: AssetBuyer, value?: BigNumber, asset?: ERC20Asset) => void; updateBuyQuote: (assetBuyer?: AssetBuyer, value?: BigNumberInput, asset?: ERC20Asset) => void;
} }
interface ConnectedProps { interface ConnectedProps {
value?: BigNumber; value?: BigNumberInput;
asset?: ERC20Asset; 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; const selectedAsset = state.selectedAsset;
if (_.isUndefined(selectedAsset) || selectedAsset.metaData.assetProxyId !== AssetProxyId.ERC20) { if (_.isUndefined(selectedAsset) || selectedAsset.metaData.assetProxyId !== AssetProxyId.ERC20) {
return { return {
@ -82,7 +83,7 @@ const debouncedUpdateBuyQuoteAsync = _.debounce(updateBuyQuoteAsync, 200, { trai
const mapDispatchToProps = ( const mapDispatchToProps = (
dispatch: Dispatch<Action>, dispatch: Dispatch<Action>,
_ownProps: SelectedAssetAmountInputProps, _ownProps: SelectedERC20AssetAmountInputProps,
): ConnectedDispatch => ({ ): ConnectedDispatch => ({
updateBuyQuote: (assetBuyer, value, asset) => { updateBuyQuote: (assetBuyer, value, asset) => {
// Update the input // Update the input
@ -104,7 +105,7 @@ const mapDispatchToProps = (
const mergeProps = ( const mergeProps = (
connectedState: ConnectedState, connectedState: ConnectedState,
connectedDispatch: ConnectedDispatch, connectedDispatch: ConnectedDispatch,
ownProps: SelectedAssetAmountInputProps, ownProps: SelectedERC20AssetAmountInputProps,
): FinalProps => { ): FinalProps => {
return { return {
...ownProps, ...ownProps,
@ -116,8 +117,8 @@ const mergeProps = (
}; };
}; };
export const SelectedAssetAmountInput: React.ComponentClass<SelectedAssetAmountInputProps> = connect( export const SelectedERC20AssetAmountInput: React.ComponentClass<SelectedERC20AssetAmountInputProps> = connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps, mapDispatchToProps,
mergeProps, mergeProps,
)(AssetAmountInput); )(ERC20AssetAmountInput);

View File

@ -2,6 +2,8 @@ import { BuyQuote } from '@0x/asset-buyer';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { BigNumberInput } from '../util/big_number_input';
import { ActionsUnion, OrderState } from '../types'; import { ActionsUnion, OrderState } from '../types';
export interface PlainAction<T extends string> { export interface PlainAction<T extends string> {
@ -36,7 +38,8 @@ export enum ActionTypes {
export const actions = { export const actions = {
updateEthUsdPrice: (price?: BigNumber) => createAction(ActionTypes.UPDATE_ETH_USD_PRICE, price), 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), updateBuyOrderState: (orderState: OrderState) => createAction(ActionTypes.UPDATE_BUY_ORDER_STATE, orderState),
updateLatestBuyQuote: (buyQuote?: BuyQuote) => createAction(ActionTypes.UPDATE_LATEST_BUY_QUOTE, buyQuote), updateLatestBuyQuote: (buyQuote?: BuyQuote) => createAction(ActionTypes.UPDATE_LATEST_BUY_QUOTE, buyQuote),
updateSelectedAsset: (assetData?: string) => createAction(ActionTypes.UPDATE_SELECTED_ASSET, assetData), updateSelectedAsset: (assetData?: string) => createAction(ActionTypes.UPDATE_SELECTED_ASSET, assetData),

View File

@ -14,6 +14,7 @@ import {
OrderState, OrderState,
} from '../types'; } from '../types';
import { assetUtils } from '../util/asset'; import { assetUtils } from '../util/asset';
import { BigNumberInput } from '../util/big_number_input';
import { Action, ActionTypes } from './actions'; import { Action, ActionTypes } from './actions';
@ -22,7 +23,7 @@ export interface State {
assetBuyer?: AssetBuyer; assetBuyer?: AssetBuyer;
assetMetaDataMap: ObjectMap<AssetMetaData>; assetMetaDataMap: ObjectMap<AssetMetaData>;
selectedAsset?: Asset; selectedAsset?: Asset;
selectedAssetAmount?: BigNumber; selectedAssetAmount?: BigNumberInput;
buyOrderState: OrderState; buyOrderState: OrderState;
ethUsdPrice?: BigNumber; ethUsdPrice?: BigNumber;
latestBuyQuote?: BuyQuote; latestBuyQuote?: BuyQuote;

View File

@ -1,10 +1,10 @@
import { injectGlobal } from './theme';
export const fonts = { export const fonts = {
include: () => { include: () => {
// Inject the inter-ui font into the page // Inject the inter-ui font into the page
return injectGlobal` const appendTo = document.head || document.getElementsByTagName('head')[0] || document.body;
@import url('https://rsms.me/inter/inter-ui.css'); 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);
}, },
}; };

View File

@ -1,6 +1,6 @@
import * as styledComponents from 'styled-components'; 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 }; export type Theme = { [key in ColorOption]: string };
@ -28,4 +28,6 @@ export const theme: Theme = {
darkOrange: '#F2994C', darkOrange: '#F2994C',
}; };
export { styled, css, injectGlobal, keyframes, ThemeProvider }; export const transparentWhite = 'rgba(255,255,255,0.3)';
export { styled, css, keyframes, ThemeProvider };

View File

@ -2,7 +2,7 @@ import { AssetProxyId, ObjectMap } from '@0x/types';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { assetDataNetworkMapping } from '../data/asset_data_network_mapping'; 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 = { export const assetUtils = {
createAssetFromAssetData: ( createAssetFromAssetData: (
@ -43,6 +43,16 @@ export const assetUtils = {
return defaultName; 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 => { getAssociatedAssetDataIfExists: (assetData: string, network: Network): string | undefined => {
const assetDataGroupIfExists = _.find(assetDataNetworkMapping, value => value[network] === assetData); const assetDataGroupIfExists = _.find(assetDataNetworkMapping, value => value[network] === assetData);
if (_.isUndefined(assetDataGroupIfExists)) { if (_.isUndefined(assetDataGroupIfExists)) {

View File

@ -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;
}
}

View File

@ -24,7 +24,7 @@ export const format = {
if (_.isUndefined(ethUnitAmount)) { if (_.isUndefined(ethUnitAmount)) {
return defaultText; return defaultText;
} }
const roundedAmount = ethUnitAmount.round(decimalPlaces); const roundedAmount = ethUnitAmount.round(decimalPlaces).toDigits(decimalPlaces);
return `${roundedAmount} ETH`; return `${roundedAmount} ETH`;
}, },
ethBaseAmountInUsd: ( ethBaseAmountInUsd: (

View File

@ -20,8 +20,8 @@ describe('format', () => {
it('converts .432414 ETH in base units to the string `.4324 ETH`', () => { it('converts .432414 ETH in base units to the string `.4324 ETH`', () => {
expect(format.ethBaseAmount(DECIMAL_ETH_IN_BASE_UNITS)).toBe('0.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`', () => { it('converts 5.3014059295032 ETH in base units to the string `5.301 ETH`', () => {
expect(format.ethBaseAmount(IRRATIONAL_ETH_IN_BASE_UNITS)).toBe('5.3014 ETH'); expect(format.ethBaseAmount(IRRATIONAL_ETH_IN_BASE_UNITS)).toBe('5.301 ETH');
}); });
it('returns defaultText param when ethBaseAmount is not defined', () => { it('returns defaultText param when ethBaseAmount is not defined', () => {
const defaultText = 'defaultText'; const defaultText = 'defaultText';
@ -38,8 +38,8 @@ describe('format', () => {
it('converts BigNumer(.432414) to the string `.4324 ETH`', () => { it('converts BigNumer(.432414) to the string `.4324 ETH`', () => {
expect(format.ethUnitAmount(BIG_NUMBER_DECIMAL)).toBe('0.4324 ETH'); expect(format.ethUnitAmount(BIG_NUMBER_DECIMAL)).toBe('0.4324 ETH');
}); });
it('converts BigNumber(5.3014059295032) to the string `5.3014 ETH`', () => { it('converts BigNumber(5.3014059295032) to the string `5.301 ETH`', () => {
expect(format.ethUnitAmount(BIG_NUMBER_IRRATIONAL)).toBe('5.3014 ETH'); expect(format.ethUnitAmount(BIG_NUMBER_IRRATIONAL)).toBe('5.301 ETH');
}); });
it('returns defaultText param when ethUnitAmount is not defined', () => { it('returns defaultText param when ethUnitAmount is not defined', () => {
const defaultText = 'defaultText'; const defaultText = 'defaultText';

View File

@ -484,6 +484,12 @@
dependencies: dependencies:
"@babel/highlight" "^7.0.0" "@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": "@babel/highlight@^7.0.0":
version "7.0.0" version "7.0.0"
resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4"
@ -504,6 +510,24 @@
dependencies: dependencies:
regenerator-runtime "^0.12.0" 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": "@ledgerhq/hw-app-eth@^4.3.0":
version "4.7.3" version "4.7.3"
resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-4.7.3.tgz#d352e19658ae296532e522c53c8ec2a1a77b64e5" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-eth/-/hw-app-eth-4.7.3.tgz#d352e19658ae296532e522c53c8ec2a1a77b64e5"
@ -1579,6 +1603,13 @@
"@types/node" "*" "@types/node" "*"
"@types/react" "*" "@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": "@types/tmp@^0.0.33":
version "0.0.33" version "0.0.33"
resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.0.33.tgz#1073c4bc824754ae3d10cfab88ab0237ba964e4d" 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" version "23.2.0"
resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz#e61fae05a1ca8801aadee57a6d66b8cefaf44167" 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: babel-plugin-syntax-async-functions@^6.8.0:
version "6.13.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" 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" fbjs "^0.8.5"
postcss-value-parser "^3.3.0" 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: css-vendor@^0.3.8:
version "0.3.8" version "0.3.8"
resolved "https://registry.npmjs.org/css-vendor/-/css-vendor-0.3.8.tgz#6421cfd3034ce664fe7673972fd0119fc28941fa" 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-tx "0xProject/ethereumjs-tx#fake-tx-include-signature-by-default"
ethereumjs-util "^5.2.0" ethereumjs-util "^5.2.0"
ethereumjs-vm "2.3.5" ethereumjs-vm "2.3.5"
ethereumjs-wallet "0.6.0" ethereumjs-wallet "~0.6.0"
fake-merkle-patricia-tree "~1.0.1" fake-merkle-patricia-tree "~1.0.1"
heap "~0.2.6" heap "~0.2.6"
js-scrypt "^0.2.0" js-scrypt "^0.2.0"
@ -14394,19 +14440,18 @@ styled-components@^3.3.3:
stylis-rule-sheet "^0.0.10" stylis-rule-sheet "^0.0.10"
supports-color "^3.2.3" supports-color "^3.2.3"
styled-components@^3.4.9: styled-components@^4.0.2:
version "3.4.10" version "4.0.2"
resolved "https://registry.npmjs.org/styled-components/-/styled-components-3.4.10.tgz#9a654c50ea2b516c36ade57ddcfa296bf85c96e1" resolved "https://registry.npmjs.org/styled-components/-/styled-components-4.0.2.tgz#7d4409ada019cdd34c25ba68c4577ea95dbcf0c5"
dependencies: dependencies:
buffer "^5.0.3" "@emotion/is-prop-valid" "^0.6.8"
css-to-react-native "^2.0.3" babel-plugin-styled-components ">= 1"
fbjs "^0.8.16" css-to-react-native "^2.2.2"
hoist-non-react-statics "^2.5.0" memoize-one "^4.0.0"
prop-types "^15.5.4" prop-types "^15.5.4"
react-is "^16.3.1" react-is "^16.3.1"
stylis "^3.5.0" stylis "^3.5.0"
stylis-rule-sheet "^0.0.10" stylis-rule-sheet "^0.0.10"
supports-color "^3.2.3"
stylis-rule-sheet@^0.0.10: stylis-rule-sheet@^0.0.10:
version "0.0.10" version "0.0.10"
@ -14849,6 +14894,10 @@ to-fast-properties@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" 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: to-no-case@^1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/to-no-case/-/to-no-case-1.0.2.tgz#c722907164ef6b178132c8e69930212d1b4aa16a" resolved "https://registry.yarnpkg.com/to-no-case/-/to-no-case-1.0.2.tgz#c722907164ef6b178132c8e69930212d1b4aa16a"