Building a NumberField Part 1: Why not native

By Rob Snow

As with building any component for the web, it is never as simple as you would wish. Browsers today ship with a native number input, <input type=number />. It blocks the user from entering things other than numbers and it comes with stepper buttons on hover as well as keyboard arrow key handling. These controls have a number of issues though, especially around accessibility and custom themeing. When we started this component, a contributor, @deebov, linked us to a great blog post detailing the shortcomings of the native input. These can be read in greater detail here: Why the GOV.UK Design System team changed the input type for numbers In this post, I will go over some of those issues and what conclusions we drew when building our NumberField.

Building a NumberField#


From a glance, a NumberField is pretty straightforward, a textfield with two buttons to increment and decrement the value in the textfield. It has to support a number of interactions, the user can increment and decrement with the buttons, arrow keys, or scroll wheel. Decimals should be supported. It should not allow any characters that are not valid in a number. In mobile, it should pop up a keyboard containing only relevant keys. Numbering systems beyond latn (0-9) should be supported.

Accessibility issues#


Screen readers#

Leading concerns in native select from gov.uk are accessibility. Native number inputs have problems with several screen readers. One of these is that NVDA's object navigation appears as a spin button with an edit field and two buttons inside. Spin button is a pretty obvious solution to use when implementing a NumberField, and in addition to gov.uk's findings, we also found that role=spinbutton has issues with VoiceOver. We were unable to focus the field using the VoiceOver cursor, and we were unable to increment/decrement the value using swipe up and down. It would also read an empty field as 50% regardless of aria-valuenow. In newer versions of VoiceOver/Safari, the cursor issue has been resolved, and the fix for increment/decrement should be landing soon. Gov.uk also noted that some screen readers see type=number as an unlabeled field.

Rounding and readability#

Native number inputs have no ability to format the text, so entering large numbers can be unwieldy. In addition, Gov.uk also found some browsers will try to auto round large values, this can be undesireable without being able to control significant figures.

Scrolling and steppers#

Native number inputs, without adding scroll event listeners and preventing default, can be hard to use in a form that requires scrolling. We also found that not all browsers handle scrolling in the input in the same way. For instance, Chrome and Firefox will not change the internal value because of scrolling ever, but Safari currently will provided you are focused in the input. All three browsers also disagree when to show the spinner, and only Chrome and Safari have the ability to hide it, Firefox will always show it.

Differences in conclusions drawn#


First, we should note the differences in our requirements. We needed to support multiple numbering systems. We also needed to support in-input formatting, units and currency, and we needed to support locale based differences in for each of those. We did not need to support phone numbers or credit cards, we felt that those would be a different input more tailored to their specific needs. We also needed screen readers to be able to increment and decrement by steps easily.

Pattern#

One difference we had in requirements, the ability to use any numbering system and formatting, meant that we couldn't easily provide a pattern to the input. We have to rely on blocking certain input keys, and we decided to handle it in onBeforeInput (more in part 2 - link). An improvement we could make to our component would be to tell the user when a keystroke is discarded. We also do not use NumberField for things like credit cards as incrementing a credit card value does not make sense.

Role#

Because role=spinbutton has so many issues with screen readers, including how it is announced if the user can get to it, we decided to make use of roledescription=number field. This freed us up to use type=text.

Inputmode#

We ran into many of the same issues outlined by gov.uk, but we had some additional issues. iPhone doesn't have a minus sign in either numeric or decimal. As a result, we can only use inputmode=numeric for NumberField we know to be restricted to positive number values. Thanks to our formatting requirements, we can actually go a bit further with this. If the number is allowed to have decimal values, we can actually use inputmode=decimal which will give us the decimal sign in addition to the numeric keyboard. Finally, we must fall back to the full keyboard if we need a negative sign. Note, iPad iOS numeric keyboard always has both decimal and minus sign. We also found that Android has a different set of keyboards not consistent with iPhone iOS. Android numeric has both a decimal point and minus key, while decimal does not have a minus key. So we can use numeric almost all the time, except when we're dealing with a positive only range that allows decimals.

Scrolling#

In some applications it's ok to step on scroll. Think scrubbing quickly and imprecisely. However, this should still only happen when the field has focus. If not, then if the field comes under the cursor while scrolling the page, then the page would stop and the input would begin stepping. For places where accidentally changing the inputs value could have bad effects, such as financial forms, we allow users to disable scroll stepping.

Stepping#

In places where the steppers are visible, a person using a screen reader or mobile device should be able to access those buttons. In the native control, this is impossible in most combinations. By building our own, we were able to create steppers that everyone can use. For screen reader users, we made it so that the buttons couldn't receive focus, but could be navigated to. This enabled us to not include them in the tab stop order because keyboard users can already step using the arrow keys. For mobile users, we created much larger stepper buttons on opposing sides so it was easier to hit the intended target.

Rounding#

Because we aren't using the native input, we have to handle javascripts errors in decimal math. For example, when javascript adds 0.1 + 0.2 it does not compute 0.3, instead, javascript 0.30000000000000004. We handle this a couple of ways. When stepping, we will always try to perform math on integers and then transform it back to decimal. What this looks like is (0.1*10 + 0.2*10)/10. The other form of math we handle is rounding to keep the number we represent in state and what we display as a formatted number are in sync. We do this by taking the number, formatting it through Intl.FormatNumber, and then parsing the result. What this means is that our number will always have the correct number of significant figures and the correct decimal precision. For example, if someone enters 0.0233, we will parse that to that number. However, depending on the formatOptions, maybe what is displayed to the user is 0.02, we don't want to have the extra 0.0033 in our state as that discrepency could change future math. So we format the number back to a string, that string will have the correct number of fraction digits and we can instead parse that for the final result.

Conclusion#


Maybe this is a good place to mention bug disabling form elements while over them (stepper buttons)

In the next part of this series, we’ll cover how React Spectrum NumberField handles numbering systems.

Building a NumberField Part 2: Intro to numbering systems

By Rob Snow

What is a numbering system#


How do numbering systems work with locales#

- -u-nu-
- differences when combined

Manipulating numbers // maybe different title?#


Formatting#

- Intl NumberFormat

Parsing#

- combined numbering system and locale with formatting
- what to do about special characters like arab decimal character
- always allowing certain characters like 'minus' even in accounting where parens are expected

How do we handle input method editors#


- pinyin & composition events