Refined InputNumber (#894)
* Refined InputNumber * rm console * rm console * travis-ci * fix unitless input, add isDecimal prop * CI fix (linting)
This commit is contained in:
parent
915fbd0368
commit
7e27d28a8d
@ -32,6 +32,8 @@ type Props = BareProps & {
|
||||
onBlur?: (event: React.KeyboardEvent<Element>) => void,
|
||||
onKeyDown?: (event: React.KeyboardEvent<Element>) => void,
|
||||
onKeyUp?: (event: React.KeyboardEvent<Element>) => void,
|
||||
onKeyPress?: (event: React.KeyboardEvent<Element>) => void,
|
||||
onPaste?: (event: React.ClipboardEvent<Element>) => void,
|
||||
placeholder?: string,
|
||||
tabIndex?: number,
|
||||
type?: Input$Type,
|
||||
@ -43,6 +45,12 @@ type State = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
// Find decimal separator used in current locale
|
||||
const getDecimalSeparator = (): string => Intl.NumberFormat()
|
||||
.formatToParts(1.1)
|
||||
.find(part => part.type === 'decimal')!
|
||||
.value;
|
||||
|
||||
// note: KeyboardEvent.keyCode and KeyboardEvent.which are deprecated
|
||||
const KEYS = {
|
||||
A: 'a',
|
||||
@ -58,7 +66,8 @@ const KEYS = {
|
||||
TAB: 'Tab',
|
||||
V: 'v',
|
||||
X: 'x',
|
||||
ZERO: '0'
|
||||
ZERO: '0',
|
||||
DECIMAL: getDecimalSeparator()
|
||||
};
|
||||
|
||||
const KEYS_PRE: Array<any> = [KEYS.ALT, KEYS.CMD, KEYS.CTRL];
|
||||
@ -134,6 +143,7 @@ export default class Input extends React.PureComponent<Props, State> {
|
||||
? 'new-password'
|
||||
: 'off'
|
||||
}
|
||||
onPaste={this.onPaste}
|
||||
/>
|
||||
{
|
||||
isEditable
|
||||
@ -169,6 +179,14 @@ export default class Input extends React.PureComponent<Props, State> {
|
||||
onKeyUp(event);
|
||||
}
|
||||
}
|
||||
|
||||
private onPaste = (event: React.ClipboardEvent<Element>): void => {
|
||||
const { onPaste } = this.props;
|
||||
|
||||
if (onPaste) {
|
||||
onPaste(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
|
||||
@ -11,7 +11,7 @@ import { formatBalance, isUndefined } from '@polkadot/util';
|
||||
import { classes } from './util';
|
||||
import { BitLengthOption } from './constants';
|
||||
import Dropdown from './Dropdown';
|
||||
import Input, { KEYS, KEYS_PRE, isCopy, isCut, isPaste, isSelectAll } from './Input';
|
||||
import Input, { KEYS, KEYS_PRE } from './Input';
|
||||
import translate from './translate';
|
||||
|
||||
type Props = BareProps & I18nProps & {
|
||||
@ -22,6 +22,7 @@ type Props = BareProps & I18nProps & {
|
||||
isDisabled?: boolean,
|
||||
isError?: boolean,
|
||||
isSi?: boolean,
|
||||
isDecimal?: boolean,
|
||||
label?: any,
|
||||
maxLength?: number,
|
||||
onChange?: (value?: BN) => void,
|
||||
@ -31,16 +32,15 @@ type Props = BareProps & I18nProps & {
|
||||
};
|
||||
|
||||
type State = {
|
||||
defaultValue?: string,
|
||||
isPreKeyDown: boolean,
|
||||
isValid: boolean,
|
||||
siOptions: Array<{ value: string, text: string }>,
|
||||
siUnit: string,
|
||||
value: string,
|
||||
valueBN: BN
|
||||
};
|
||||
|
||||
const DEFAULT_BITLENGTH = BitLengthOption.NORMAL_NUMBERS as BitLength;
|
||||
const KEYS_ALLOWED: Array<any> = [KEYS.ARROW_LEFT, KEYS.ARROW_RIGHT, KEYS.BACKSPACE, KEYS.ENTER, KEYS.ESCAPE, KEYS.TAB];
|
||||
|
||||
class InputNumber extends React.PureComponent<Props, State> {
|
||||
constructor (props: Props) {
|
||||
@ -48,10 +48,10 @@ class InputNumber extends React.PureComponent<Props, State> {
|
||||
|
||||
const { defaultValue, isSi, value } = this.props;
|
||||
let valueBN = new BN(value || 0);
|
||||
const si = formatBalance.calcSi(valueBN.toString());
|
||||
const si = formatBalance.findSi('-');
|
||||
|
||||
this.state = {
|
||||
defaultValue: isSi
|
||||
value: isSi
|
||||
? new BN(defaultValue || valueBN).div(new BN(10).pow(new BN(si.power))).toString()
|
||||
: (defaultValue || valueBN).toString(),
|
||||
isPreKeyDown: false,
|
||||
@ -72,32 +72,26 @@ class InputNumber extends React.PureComponent<Props, State> {
|
||||
InputNumber.units = units;
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps ({ isDisabled, isSi, defaultValue = '0' }: Props, state: State): Partial<State> | null {
|
||||
static getDerivedStateFromProps ({ isDisabled, isSi, defaultValue = '0' }: Props): Partial<State> | null {
|
||||
if (!isDisabled || !isSi) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
defaultValue: formatBalance(defaultValue, false),
|
||||
value: formatBalance(defaultValue, false),
|
||||
siUnit: formatBalance.calcSi(defaultValue.toString(), formatBalance.getDefaults().decimals).value
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
const { bitLength = DEFAULT_BITLENGTH, className, defaultValue = '0', help, isSi, isDisabled, maxLength, style, t } = this.props;
|
||||
const { isValid } = this.state;
|
||||
const { bitLength = DEFAULT_BITLENGTH, className, help, isSi, isDisabled, maxLength, style, t } = this.props;
|
||||
const { isValid, value } = this.state;
|
||||
const maxValueLength = this.maxValue(bitLength).toString().length - 1;
|
||||
const value = this.state.defaultValue || defaultValue;
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...this.props}
|
||||
className={classes('ui--InputNumber', className)}
|
||||
defaultValue={
|
||||
isDisabled
|
||||
? undefined
|
||||
: value
|
||||
}
|
||||
help={help}
|
||||
isAction={isSi}
|
||||
isDisabled={isDisabled}
|
||||
@ -106,28 +100,20 @@ class InputNumber extends React.PureComponent<Props, State> {
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={this.onKeyUp}
|
||||
onPaste={this.onPaste}
|
||||
placeholder={t('Positive number')}
|
||||
style={style}
|
||||
value={
|
||||
isDisabled
|
||||
? value
|
||||
: undefined
|
||||
}
|
||||
value={value}
|
||||
type='text'
|
||||
>
|
||||
{this.renderSiDropdown()}
|
||||
{isSi && this.renderSiDropdown()}
|
||||
</Input>
|
||||
);
|
||||
}
|
||||
|
||||
private renderSiDropdown () {
|
||||
const { isSi } = this.props;
|
||||
const { siOptions, siUnit } = this.state;
|
||||
|
||||
if (!isSi) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
isButton
|
||||
@ -146,40 +132,22 @@ class InputNumber extends React.PureComponent<Props, State> {
|
||||
return value.bitLength() <= (bitLength || DEFAULT_BITLENGTH);
|
||||
}
|
||||
|
||||
private isValidKey = (event: React.KeyboardEvent<Element>, isPreKeyDown: boolean): boolean => {
|
||||
const { value: previousValue } = event.target as HTMLInputElement;
|
||||
// prevents entry of zero if initial digit is zero
|
||||
const isDuplicateZero = previousValue[0] === '0' && event.key === KEYS.ZERO;
|
||||
|
||||
if (isDuplicateZero) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// allow cut/copy/paste combinations but not non-numeric letters (i.e. a, c, x, v) individually
|
||||
if (
|
||||
(isSelectAll(event.key, isPreKeyDown)) ||
|
||||
(isCut(event.key, isPreKeyDown)) ||
|
||||
(isCopy(event.key, isPreKeyDown)) ||
|
||||
(isPaste(event.key, isPreKeyDown))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isNaN(Number(event.key)) && !KEYS_ALLOWED.includes(event.key)) {
|
||||
private isValidNumber (input: BN, bitLength: number = DEFAULT_BITLENGTH): boolean {
|
||||
const maxBN = this.maxValue(bitLength);
|
||||
if (input.lt(new BN(0)) || !input.lt(maxBN) || !this.isValidBitLength(input, bitLength)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private isValidNumber (input: BN, bitLength: number = DEFAULT_BITLENGTH): boolean {
|
||||
const maxBN = this.maxValue(bitLength);
|
||||
|
||||
if (!input.lt(maxBN) || !this.isValidBitLength(input, bitLength)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
private regex = (): RegExp => {
|
||||
const { isDecimal, isSi } = this.props;
|
||||
return new RegExp(
|
||||
(isSi || isDecimal) ?
|
||||
`^(0|[1-9]\\d*)(\\${KEYS.DECIMAL}\\d*)?$` :
|
||||
`^(0|[1-9]\\d*)$`
|
||||
);
|
||||
}
|
||||
|
||||
private onChange = (value: string): void => {
|
||||
@ -187,10 +155,10 @@ class InputNumber extends React.PureComponent<Props, State> {
|
||||
const { siUnit } = this.state;
|
||||
|
||||
try {
|
||||
const valueBN = this.applySi(siUnit, new BN(value || 0));
|
||||
const valueBN = this.inputValueToBn(value, siUnit);
|
||||
const isValid = this.isValidNumber(valueBN, bitLength);
|
||||
|
||||
this.setState({ isValid, valueBN });
|
||||
this.setState({ isValid, value, valueBN });
|
||||
|
||||
onChange && onChange(
|
||||
isValid
|
||||
@ -207,79 +175,110 @@ class InputNumber extends React.PureComponent<Props, State> {
|
||||
|
||||
if (KEYS_PRE.includes(event.key)) {
|
||||
this.setState({ isPreKeyDown: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// restrict input of certain keys
|
||||
const isValid = this.isValidKey(event, isPreKeyDown);
|
||||
if (event.key.length === 1 && !isPreKeyDown) {
|
||||
const { selectionStart: i, selectionEnd: j, value } = event.target as HTMLInputElement;
|
||||
const newValue = `${
|
||||
value.substring(0, i!)
|
||||
}${
|
||||
event.key
|
||||
}${
|
||||
value.substring(j!)
|
||||
}`;
|
||||
|
||||
if (!isValid) {
|
||||
event.preventDefault();
|
||||
if (!this.regex().test(newValue)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onKeyUp = (event: React.KeyboardEvent<Element>): void => {
|
||||
const { value: newValue } = event.target as HTMLInputElement;
|
||||
const isNewValueZero = new BN(newValue).isZero();
|
||||
|
||||
if (KEYS_PRE.includes(event.key)) {
|
||||
this.setState({ isPreKeyDown: false });
|
||||
}
|
||||
|
||||
/* if new value equates to '0' in BN when but it's length is >=1 (i.e. '012', '00', etc)
|
||||
* then replace the input value with just '0'.
|
||||
* otherwise remove the preceding zeros from the new value (i.e. '0123' -> '123')
|
||||
* note: edge case glitch occurs if existing value is '0' and you 'hold down' and keep
|
||||
* pasting a value of '00' after it, then sometimes when you let go the
|
||||
* remaining value shown as '000' or '00000' in the UI, but it's still ok because
|
||||
* the actual BN if the user submitted would still be '0', and if they then press any key
|
||||
* the UI input value resets to '0'
|
||||
*/
|
||||
if (isNewValueZero && newValue.length >= 1) {
|
||||
(event.target as HTMLInputElement).value = '0';
|
||||
} else {
|
||||
(event.target as HTMLInputElement).value = newValue.replace(/^0+/, '');
|
||||
}
|
||||
}
|
||||
|
||||
private applySi (siUnit: string, value: BN): BN {
|
||||
const { isSi } = this.props;
|
||||
private onPaste = (event: React.ClipboardEvent<Element>): void => {
|
||||
const { value: newValue } = event.target as HTMLInputElement;
|
||||
|
||||
if (!isSi) {
|
||||
return value;
|
||||
if (!this.regex().test(newValue)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const si = formatBalance.findSi(siUnit);
|
||||
const power = new BN(formatBalance.getDefaults().decimals + si.power);
|
||||
|
||||
return value.mul(new BN(10).pow(power));
|
||||
}
|
||||
|
||||
private applyNewSi (oldSi: string, newSi: string, value: BN): BN {
|
||||
const si = formatBalance.findSi(oldSi);
|
||||
const power = new BN(formatBalance.getDefaults().decimals + si.power);
|
||||
|
||||
return this.applySi(newSi, value.div(new BN(10).pow(power)));
|
||||
}
|
||||
|
||||
private selectSiUnit = (siUnit: string): void => {
|
||||
this.setState((prevState: State) => {
|
||||
const { bitLength, onChange } = this.props;
|
||||
const valueBN = this.applyNewSi(prevState.siUnit, siUnit, prevState.valueBN);
|
||||
const isValid = this.isValidNumber(valueBN, bitLength);
|
||||
const isValid = this.isValidNumber(prevState.valueBN, bitLength);
|
||||
const value = this.bnToInputValue(prevState.valueBN, siUnit);
|
||||
|
||||
onChange && onChange(
|
||||
isValid
|
||||
? valueBN
|
||||
? prevState.valueBN
|
||||
: undefined
|
||||
);
|
||||
|
||||
return {
|
||||
isValid,
|
||||
siUnit,
|
||||
valueBN
|
||||
value
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private inputValueToBn = (value: string, siUnit: string): BN => {
|
||||
const { isSi } = this.props;
|
||||
const basePower = isSi ? formatBalance.getDefaults().decimals : 0;
|
||||
const siPower = isSi ? formatBalance.findSi(siUnit).power : 0;
|
||||
|
||||
const isDecimalValue = value.match(/^(\d+)\.(\d+)$/);
|
||||
|
||||
if (isDecimalValue) {
|
||||
if (siPower - isDecimalValue[2].length < -basePower) {
|
||||
return new BN(-1);
|
||||
}
|
||||
|
||||
const div = new BN(value.replace(/\.\d*$/, ''));
|
||||
const mod = new BN(value.replace(/^\d+\./, ''));
|
||||
|
||||
return div
|
||||
.mul(new BN(10).pow(new BN(basePower + siPower)))
|
||||
.add(mod.mul(new BN(10).pow((new BN(basePower + siPower - mod.toString().length)))));
|
||||
} else {
|
||||
return new BN(value.replace(/[^\d]/g, ''))
|
||||
.mul(new BN(10).pow(new BN(basePower + siPower)));
|
||||
}
|
||||
}
|
||||
|
||||
private bnToInputValue = (bn: BN, siUnit: string): string => {
|
||||
const { isSi } = this.props;
|
||||
|
||||
const basePower = isSi ? formatBalance.getDefaults().decimals : 0;
|
||||
const siPower = isSi ? formatBalance.findSi(siUnit).power : 0;
|
||||
|
||||
const base = new BN(10).pow(new BN(basePower + siPower));
|
||||
const zero = new BN(0);
|
||||
const div = bn.div(base);
|
||||
const mod = bn.mod(base);
|
||||
|
||||
return `${
|
||||
div.gt(zero) ? div.toString() : '0'
|
||||
}${
|
||||
mod.gt(zero) ?
|
||||
(() => {
|
||||
const padding = Math.max(
|
||||
mod.toString().length,
|
||||
base.toString().length - div.toString().length,
|
||||
bn.toString().length - div.toString().length
|
||||
);
|
||||
return `.${mod.toString(10, padding).replace(/0*$/, '')}`;
|
||||
})() :
|
||||
''
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user