Skip to content

Instantly share code, notes, and snippets.

@honsq90
Last active April 5, 2018 06:22
Show Gist options
  • Save honsq90/5ce8ac8b15c3491e3b1483d61de14399 to your computer and use it in GitHub Desktop.
Save honsq90/5ce8ac8b15c3491e3b1483d61de14399 to your computer and use it in GitHub Desktop.
A cross-browser html type=number input for React.
import React, { Component } from 'react';
const FRACTIONAL_DIGITS = 2;
const MAX_VALUE = 100;
export const isUsingFirefox = () => navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
export const normalize = (value) => {
let normalizedValue;
if (value > MAX_VALUE) {
normalizedValue = Number(MAX_VALUE);
} else {
normalizedValue = Number(value);
}
return normalizedValue.toFixed(FRACTIONAL_DIGITS);
};
/**
* This is a React component that is designed to allow decimals in a HTML number input.
* The use case is a number input that reformats to 2 decimal places when the user focuses out of the input.
*
* Tested in:
* - Chrome
* - Safari
* - Firefox
* - IE11
* - Edge
* - iOS
* - Android
*/
class DecimalInput extends Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.onBlur = this.onBlur.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.state = {
previousKey: null,
};
}
/**
* Do not re-render, let the browser handle the input display.
*
* This prevents an issue with Firefox that happens when a user wants to enter a value like 4.02.
* As the user sequentially types, it goes from 4 -> 4. -> 4.0 -> 4.02.
* When the value is 4.0, re-rendering the input in Firefox reformats the value back to 4.
*/
shouldComponentUpdate() {
return false;
}
onKeyDown(event) {
const { key, target: { value, validity } } = event;
// allow all action keys
// https://github.com/facebook/react/blob/v16.3.1/packages/react-dom/src/events/getEventKey.js
if (key.match(/^\w{2}/)) {
this.setState({ previousKey: key });
return;
}
// reject anything that the browser detects as badInput
if (validity && validity.badInput) {
event.preventDefault();
return;
}
// reject anything that's not a number or period
if (!key.match(/^[\d.]$/)) {
event.preventDefault();
return;
}
/**
* Rejecting duplicate decimal places
* For some reason, html number input has inconsistent browser validation...
* As such, 1.. or 1.00. is allowed in the input field, but is actually silently invalid
*/
const newValue = `${value}${key}`;
if (newValue.match(/\.\d*\./)) {
event.preventDefault();
return;
} else if (key === "." && this.state.previousKey === ".") {
event.preventDefault();
return;
}
this.setState({ previousKey: key });
}
/**
* Previously used in conjunction with redux-form
*/
onChange(event) {
let normalizedValue = normalize(event.target.value);
this.props.onChange(event, normalizedValue);
}
/**
* Previously used in conjunction with redux-form
*/
onBlur(event) {
let normalizedValue = normalize(event.target.value);
this.input.value = normalizedValue;
this.props.onBlur(event, normalizedValue);
}
onFocus() {
/**
* Because Firefox likes to remove trailing zeroes (https://bugzilla.mozilla.org/show_bug.cgi?id=1003896),
* when the normalized value is '4.00', the user actually sees '4'.
*
* This affects the keyDown validation if a user decides to enter another '.',
* as that evaluates to '4.00.', which is considered invalid by the browser
*
* As such, we use parseFloat onFocus so that our keyDown validation can operate as per normal.
*/
const { value } = this.input;
if (isUsingFirefox()) {
this.input.value = parseFloat(value);
}
this.props.onFocus();
}
render() {
const { defaultValue } = this.props;
return (
<input type="number"
defaultValue={ parseFloat(defaultValue).toFixed(FRACTIONAL_DIGITS) }
step="0.01"
pattern="[\d\.]*"
name="decimal"
ref={ (input) => this.input = input }
onBlur={ this.onBlur }
onChange={ this.onChange }
onFocus={ this.onFocus }
onKeyDown={ this.onKeyDown }
/>
);
}
}
export default DecimalInput;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment