aboutsummaryrefslogblamecommitdiffstats
path: root/packages/instant/src/components/scaling_input.tsx
blob: 7824c10f9c34a0e2671ee19d630075373c0f7b17 (plain) (tree)


















                                             
                          
                   

                                                                   

                            
                       
 
 
                                    


                                    





                                                                                         

                                  
                                         
                      
      
















                                                                                                         




















                                                                                          

                                                                    

                                                                       


                                                                                                           
                                                              







                                                                                        



                                                     
     
                                      
                                                                                  
                                                                 

                  
                                           
                                     
                                   

                                         
                                                                            
                                              
                                     


              

                                                                 



                                                               


                                                                










                                           



                                                         
         

                                                          
 
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import * as React from 'react';
import * as ReactDOM from 'react-dom';

import { ColorOption } from '../style/theme';
import { util } from '../util/util';

import { Container, Input } from './ui';

export enum ScalingInputPhase {
    Start,
    Scaling,
    End,
}

export interface ScalingInputProps {
    startWidthCh: number;
    endWidthCh: number;
    maxFontSizePx: number;
    value?: string;
    onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
    onFontSizeChange: (fontSizePx: number) => void;
    fontColor?: ColorOption;
    placeholder?: string;
    maxLength?: number;
}

export interface ScalingInputState {
    fixedWidthInPxIfExists?: number;
}

export interface ScalingInputSnapshot {
    inputWidthPx: number;
}
// This is a magic number that was determined experimentally.
const percentageToReduceByPerCharacter = 0.15;
export class ScalingInput extends React.Component<ScalingInputProps, ScalingInputState> {
    public static defaultProps = {
        onChange: util.boundNoop,
        onFontSizeChange: util.boundNoop,
        maxLength: 10,
    };
    public state = {
        fixedWidthInPxIfExists: undefined,
    };
    private _inputRef = React.createRef();
    public static getPhase(startWidthCh: number, endWidthCh: number, value?: string): ScalingInputPhase {
        if (_.isUndefined(value) || value.length <= startWidthCh) {
            return ScalingInputPhase.Start;
        }
        if (value.length > startWidthCh && value.length <= endWidthCh) {
            return ScalingInputPhase.Scaling;
        }
        return ScalingInputPhase.End;
    }
    public static getPhaseFromProps(props: ScalingInputProps): ScalingInputPhase {
        const { value, startWidthCh, endWidthCh } = props;
        return ScalingInput.getPhase(startWidthCh, endWidthCh, value);
    }
    public static calculateFontSize(props: ScalingInputProps): number {
        const { startWidthCh, endWidthCh, value, maxFontSizePx } = props;
        const phase = ScalingInput.getPhase(startWidthCh, endWidthCh, value);
        if (_.isUndefined(value) || phase !== ScalingInputPhase.End) {
            return maxFontSizePx;
        }
        const charactersOverMax = value.length - endWidthCh;
        const scalingFactor = (1 - percentageToReduceByPerCharacter) ** charactersOverMax;
        const fontSize = scalingFactor * maxFontSizePx;
        return fontSize;
    }
    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);
        const prevFontSize = ScalingInput.calculateFontSize(prevProps);
        const curFontSize = ScalingInput.calculateFontSize(this.props);
        // if we went from anything else to end, fix to the current width as it shouldn't change as we grow
        if (prevPhase !== ScalingInputPhase.End && curPhase === ScalingInputPhase.End) {
            this.setState({
                fixedWidthInPxIfExists: snapshot.inputWidthPx,
            });
        }
        // if we end from end to to anything else, un-fix the width
        if (prevPhase === ScalingInputPhase.End && curPhase !== ScalingInputPhase.End) {
            this.setState({
                fixedWidthInPxIfExists: undefined,
            });
        }
        // 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={`${ScalingInput.calculateFontSize(this.props)}px`}
                width={this._calculateWidth()}
                maxLength={maxLength}
            />
        );
    }
    private readonly _calculateWidth = (): string => {
        const phase = ScalingInput.getPhaseFromProps(this.props);
        const { value, startWidthCh, endWidthCh } = this.props;
        if (_.isUndefined(value)) {
            return `${startWidthCh}ch`;
        }
        if (!_.isUndefined(this.state.fixedWidthInPxIfExists)) {
            return `${this.state.fixedWidthInPxIfExists}px`;
        }
        switch (phase) {
            case ScalingInputPhase.Start:
                return `${startWidthCh}ch`;
            case ScalingInputPhase.Scaling:
                return `${value.length}ch`;
            case ScalingInputPhase.End:
                return `${endWidthCh}ch`;
            default:
                return `${startWidthCh}ch`;
        }
    };
    private readonly _getInputWidthInPx = (): number => {
        const ref = this._inputRef.current;
        if (!ref) {
            return 0;
        }
        return (ref as any).getBoundingClientRect().width;
    };
}