Last active
April 5, 2018 06:22
-
-
Save honsq90/5ce8ac8b15c3491e3b1483d61de14399 to your computer and use it in GitHub Desktop.
A cross-browser html type=number input for React.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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