Skip to content

Commit 3b04aec

Browse files
authored
Add aria and stately docs for numberfield (#1523)
* Add aria and stately docs for numberfield * Update example and add a bit more text * Clarify supported notation and update sales tax to be more realistic
1 parent abcd259 commit 3b04aec

File tree

8 files changed

+508
-24
lines changed

8 files changed

+508
-24
lines changed
Lines changed: 57 additions & 0 deletions
Loading
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
<!-- Copyright 2020 Adobe. All rights reserved.
2+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
3+
you may not use this file except in compliance with the License. You may obtain a copy
4+
of the License at http://www.apache.org/licenses/LICENSE-2.0
5+
Unless required by applicable law or agreed to in writing, software distributed under
6+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
7+
OF ANY KIND, either express or implied. See the License for the specific language
8+
governing permissions and limitations under the License. -->
9+
10+
import {Layout} from '@react-spectrum/docs';
11+
export default Layout;
12+
13+
import docs from 'docs:@react-aria/numberfield';
14+
import statelyDocs from 'docs:@react-stately/numberfield';
15+
import {HeaderInfo, FunctionAPI, TypeContext, InterfaceType, TypeLink} from '@react-spectrum/docs';
16+
import packageData from '@react-aria/numberfield/package.json';
17+
import Anatomy from './anatomy.svg';
18+
19+
```jsx import
20+
import {useNumberField} from '@react-aria/numberfield';
21+
```
22+
23+
---
24+
category: Forms
25+
keywords: [number field, input, form, aria]
26+
---
27+
28+
# useNumberField
29+
30+
<p>{docs.exports.useNumberField.description}</p>
31+
32+
<HeaderInfo
33+
packageData={packageData}
34+
componentNames={['useNumberField']}
35+
sourceData={[
36+
{type: 'W3C', url: 'https://www.w3.org/TR/wai-aria-practices-1.2/#spinbutton'}
37+
]} />
38+
39+
## API
40+
41+
<FunctionAPI function={docs.exports.useNumberField} links={docs.links} />
42+
43+
## Features
44+
45+
Number fields can be built with `<input type="number">`, but the behavior is very inconsistent across
46+
browsers and platforms, it supports limited localized formatting options, and it is challenging to style
47+
the stepper buttons. `useNumberField` helps achieve accessible number fields that support internationalized
48+
formatting options and can be styled as needed.
49+
50+
* Support for internationalized number formatting and parsing including decimals, percentages, currency values, and units
51+
* Support for the Latin, Arabic, and Han decimal numbering systems in [over 30 locales](internationalization.html#supported-locales)
52+
* Automatically detects the numbering system used and supports parsing numbers not in the default numbering system for the locale
53+
* Support for multiple currency formats including symbol, code, and name in standard or accounting notation
54+
* Validates keyboard entry as the user types so that only valid numeric input according to the locale and numbering system is accepted
55+
* Handles composed input from input method editors, e.g. Pinyin
56+
* Automatically selects an appropriate software keyboard for mobile according to the current platform and allowed values
57+
* Supports rounding to a configurable number of fraction digits
58+
* Support for clamping the value between a configurable minimum and maximum, and snapping to a step value
59+
* Support for stepper buttons and arrow keys to increment and decrement the value according to the step value
60+
* Supports pressing and holding the stepper buttons to continuously increment or decrement
61+
* Handles floating point rounding errors when incrementing, decrementing, and snapping to step
62+
* Supports using the scroll wheel to increment and decrement the value
63+
* Exposed to assistive technology as a text field with a custom localized role description using ARIA
64+
* Follows the [spinbutton](https://www.w3.org/TR/wai-aria-practices-1.2/#spinbutton) ARIA pattern
65+
* Works around bugs in VoiceOver with the spinbutton role
66+
* Uses an ARIA live region to ensure that value changes are announced
67+
68+
## Anatomy
69+
70+
<Anatomy />
71+
72+
Number fields consist of an input element that shows the current value and allows the user to type a new value,
73+
optional stepper buttons to increment and decrement the value, a group containing the input and stepper buttons,
74+
and a label.
75+
76+
`useNumberField` returns props for each of these, which you should spread onto the appropriate elements:
77+
78+
<TypeContext.Provider value={docs.links}>
79+
<InterfaceType properties={docs.links[docs.exports.useNumberField.return.id].properties} />
80+
</TypeContext.Provider>
81+
82+
State is managed by the <TypeLink links={statelyDocs.links} type={statelyDocs.exports.useNumberFieldState} />
83+
hook from `@react-stately/numberfield`. The state object should be passed as an option to `useNumberField`
84+
85+
If there is no visual label, an `aria-label` or `aria-labelledby` prop must be passed instead
86+
to identify the element to screen readers.
87+
88+
## Example
89+
90+
The following example shows how to build a simple number field. It includes an input element where the user can
91+
type a number, along with increment and decrement buttons on either side. The buttons are built using
92+
the [useButton](useButton.html) hook, using the props returned by `useNumberField`.
93+
94+
**Note:** Due to [a bug](https://bugs.webkit.org/show_bug.cgi?id=219188) in Safari on macOS, pointer events may not
95+
be dispatched after a `<button>` element is disabled while the mouse is pressed. This may require the user to click
96+
twice when incrementing or decrementing the value from the minimum or maximum value. While out of scope for this example,
97+
you may wish to use a `<div>` element instead of a `<button>` to avoid this issue. See the [useButton docs](useButton.html#custom-element-type)
98+
for an example of a button with a custom element type.
99+
100+
```tsx example export=true
101+
import {useNumberFieldState} from '@react-stately/numberfield';
102+
import {useLocale} from '@react-aria/i18n';
103+
import {useButton} from '@react-aria/button';
104+
105+
function NumberField(props) {
106+
let {locale} = useLocale();
107+
let state = useNumberFieldState({...props, locale});
108+
let inputRef = React.useRef();
109+
let {
110+
labelProps,
111+
groupProps,
112+
inputProps,
113+
incrementButtonProps,
114+
decrementButtonProps
115+
} = useNumberField(props, state, inputRef);
116+
117+
let {buttonProps: incrementProps} = useButton(incrementButtonProps);
118+
let {buttonProps: decrementProps} = useButton(decrementButtonProps);
119+
120+
return (
121+
<div>
122+
<label {...labelProps}>{props.label}</label>
123+
<div {...groupProps}>
124+
<button {...decrementProps}>-</button>
125+
<input {...inputProps} ref={inputRef} />
126+
<button {...incrementProps}>+</button>
127+
</div>
128+
</div>
129+
);
130+
}
131+
132+
<NumberField
133+
label="Price"
134+
defaultValue={6}
135+
formatOptions={{
136+
style: 'currency',
137+
currency: 'USD'
138+
}} />
139+
```
140+
141+
## Usage
142+
143+
The following examples show how to use the `NumberField` component created in the above example.
144+
145+
### Controlled
146+
147+
By default, `NumberField` is uncontrolled. However, when using the `value` prop, it becomes controlled.
148+
This allows you to store the current value in your own state, and use it elsewhere.
149+
150+
The `onChange` event is triggered whenever the number value updates. This happens when the user types a
151+
value and blurs the input, or when incrementing or decrementing the value. It does not happen as the user
152+
types because partial input may not be parseable to a valid number.
153+
154+
```tsx example
155+
function Example() {
156+
let [value, setValue] = React.useState(6);
157+
158+
return (
159+
<>
160+
<NumberField
161+
label="Controlled value"
162+
value={value}
163+
onChange={setValue} />
164+
<div>Current value prop: {value}</div>
165+
</>
166+
);
167+
}
168+
```
169+
170+
### Decimals
171+
172+
The default formatting style for `NumberField` is decimal, but you can configure various aspects via the `formatOptions`
173+
prop. All options supported by [Intl.NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat)
174+
are supported, including configuration of minimum and maximum fraction digits, sign display, grouping separators, etc.
175+
Currently only standard notation is supported, though scientific, engineering, and compact notation may be supported in the future.
176+
177+
The following example uses the `signDisplay` option to include the plus sign for positive numbers, for example to display
178+
an offset from some value. In addition, it always displays a minimum of 1 digit after the decimal point, and allows up to
179+
2 fraction digits. If the user enters more than 2 fraction digits, the result will be rounded.
180+
181+
```tsx example
182+
<NumberField
183+
label="Adjust exposure"
184+
defaultValue={0}
185+
formatOptions={{
186+
signDisplay: 'exceptZero',
187+
minimumFractionDigits: 1,
188+
maximumFractionDigits: 2
189+
}} />
190+
```
191+
192+
### Percentages
193+
194+
The `style: 'percent'` option can be passed to the `formatOptions` prop to treat the value as a percentage. In this mode,
195+
the value is multiplied by 100 before it is displayed, i.e. `0.45` is displayed as `45%`. The reverse is also true: when the
196+
user enters a value, the `onChange` event will be triggered with the entered value divided by 100. When the percent option
197+
is enabled, the default step automatically changes to `0.01` such that incrementing and decrementing occurs by `1%`. This can
198+
be overridden with the `step` prop. [See below](#step-values) for details.
199+
200+
```tsx example
201+
<NumberField
202+
label="Sales tax"
203+
defaultValue={0.05}
204+
formatOptions={{
205+
style: 'percent'
206+
}} />
207+
```
208+
209+
### Currency values
210+
211+
The `style: 'currency'` option can be passed to the `formatOptions` prop to treat the value as a currency value. The `currency`
212+
option must also be passed to set the currency code (e.g. `USD`) to use. In addition, the `currencyDisplay` option can be
213+
used to choose whether to display the currency symbol, currency code, or currency name. Finally, the `currencySign` option
214+
can be set to `accounting` to use accounting notation for negative numbers, which uses parentheses rather than a minus sign
215+
in some locales.
216+
217+
If you need to allow the user to change the currency, you should include a separate dropdown next to the number field.
218+
The number field itself will not determine the currency from the user input.
219+
220+
```tsx example
221+
<NumberField
222+
label="Transaction amount"
223+
defaultValue={45}
224+
formatOptions={{
225+
style: 'currency',
226+
currency: 'EUR',
227+
currencyDisplay: 'code',
228+
currencySign: 'accounting'
229+
}} />
230+
```
231+
232+
### Units
233+
234+
The `style: 'unit'` option can be passed to the `formatOptions` prop to format the value with a unit of measurement. The `unit`
235+
option must also be passed to set which unit to use (e.g. `inch`). In addition, the `unitDisplay` option can be used to choose
236+
whether to display the unit in long, short, or narrow format.
237+
238+
If you need to allow the user to change the unit, you should include a separate dropdown next to the number field.
239+
The number field itself will not determine the unit from the user input.
240+
241+
**Note:** the unit style is not currently supported in Safari. A [polyfill](https://formatjs.io/docs/polyfills/intl-numberformat/)
242+
may be necessary.
243+
244+
```tsx example
245+
<NumberField
246+
label="Package width"
247+
defaultValue={4}
248+
formatOptions={{
249+
style: 'unit',
250+
unit: 'inch',
251+
unitDisplay: 'long'
252+
}} />
253+
```
254+
255+
### Minimum and maximum values
256+
257+
The `minValue` and `maxValue` props can be used to limit the entered value to a specific range. The value will be clamped
258+
when the user blurs the input field. In addition, the increment and decrement buttons will be disabled when the value is
259+
within one `step` value from the bounds ([see below](#step-values) for info about steps). Ranges can be open ended by only
260+
providing either `minValue` or `maxValue` rather than both.
261+
262+
If a valid range is known ahead of time, it is a good idea to provide it to `NumberField` so it can optimize the experience.
263+
For example, when the minimum value is greater than or equal to zero, it is possible to use a numeric keyboard on iOS rather
264+
than a full text keyboard (necessary to enter a minus sign).
265+
266+
```tsx example
267+
<NumberField
268+
label="Enter your age"
269+
minValue={0} />
270+
```
271+
272+
### Step values
273+
274+
The `step` prop can be used to snap the value to certain increments. If there is a `minValue` defined, the steps are calculated
275+
starting from the minimum. For example, if `minValue={2}`, and `step={3}`, the valid step values would be 2, 5, 8, 11, etc. If no
276+
`minValue` is defined, the steps are calculated starting from zero and extending in both directions. In other words, such that the
277+
values are evenly divisible by the step. If no `step` is defined, any decimal value may be typed, but incrementing and decrementing
278+
snaps the value to an integer.
279+
280+
If the user types a value that is between two steps and blurs the input, the value will be snapped to the nearest step. When
281+
incrementing or decrementing, the value is snapped to the nearest step that is higher or lower, respectively.
282+
When incrementing or decrementing from an empty field, the value starts at the `minValue` or `maxValue`, respectively, if defined.
283+
Otherwise, the value starts from `0`.
284+
285+
```tsx example
286+
<NumberField
287+
label="Step"
288+
step={10} />
289+
<NumberField
290+
label="Step + minValue"
291+
minValue={2}
292+
step={3} />
293+
<NumberField
294+
label="Step + minValue + maxValue"
295+
minValue={2}
296+
maxValue={21}
297+
step={3} />
298+
```
299+
300+
### Disabled and read only
301+
302+
The `isDisabled` and `isReadOnly` props can be used prevent the user from editing the value of the number field.
303+
The difference is that `isReadOnly` still allows the input to be focused, while `isDisabled` prevents all user interaction.
304+
305+
```tsx example
306+
<NumberField label="Disabled" isDisabled value={25} />
307+
<NumberField label="Read only" isReadOnly value={32} />
308+
```
309+
310+
## Internationalization
311+
312+
`useNumberField` handles many aspects of internationalization automatically, including formatting and parsing numbers according
313+
to the current locale and numbering system. In addition, the increment and decrement buttons have localized ARIA labels.
314+
You are responsible for localizing the label text passed into the number field.
315+
316+
### RTL
317+
318+
In right-to-left languages, the number field should be mirrored. The order of the input and buttons should be flipped, and the input
319+
text should be right aligned instead of left aligned. Ensure that your CSS accounts for this.

packages/@react-aria/numberfield/src/useNumberField.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ function supportsNativeBeforeInputEvent() {
5555
typeof InputEvent.prototype.getTargetRanges === 'function';
5656
}
5757

58+
/**
59+
* Provides the behavior and accessibility implementation for a number field component.
60+
* Number fields allow users to enter a number, and increment or decrement the value using stepper buttons.
61+
*/
5862
export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldState, inputRef: RefObject<HTMLInputElement>): NumberFieldAria {
5963
let {
6064
id,

0 commit comments

Comments
 (0)