diff options
Diffstat (limited to 'packages/instant/src/components')
-rw-r--r-- | packages/instant/src/components/amount_input.tsx | 49 | ||||
-rw-r--r-- | packages/instant/src/components/animations/slide_animations.tsx | 10 | ||||
-rw-r--r-- | packages/instant/src/components/asset_amount_input.tsx | 39 | ||||
-rw-r--r-- | packages/instant/src/components/erc20_asset_amount_input.tsx | 84 | ||||
-rw-r--r-- | packages/instant/src/components/instant_heading.tsx | 6 | ||||
-rw-r--r-- | packages/instant/src/components/scaling_amount_input.tsx | 52 | ||||
-rw-r--r-- | packages/instant/src/components/scaling_input.tsx | 170 | ||||
-rw-r--r-- | packages/instant/src/components/ui/container.tsx | 15 | ||||
-rw-r--r-- | packages/instant/src/components/ui/flex.tsx | 13 | ||||
-rw-r--r-- | packages/instant/src/components/ui/input.tsx | 9 | ||||
-rw-r--r-- | packages/instant/src/components/ui/text.tsx | 11 |
11 files changed, 339 insertions, 119 deletions
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}; |