@@ -26,26 +26,25 @@ import {
26
26
import intlMessages from '../intl/*.json' ;
27
27
import { isAndroid , isIOS , isIPhone , mergeProps , useId } from '@react-aria/utils' ;
28
28
import { NumberFieldState } from '@react-stately/numberfield' ;
29
- import { SpinButtonProps , useSpinButton } from '@react-aria/spinbutton' ;
30
29
import { TextInputDOMProps } from '@react-types/shared' ;
31
30
import { useFocus } from '@react-aria/interactions' ;
32
31
import {
33
32
useMessageFormatter ,
34
33
useNumberFormatter
35
34
} from '@react-aria/i18n' ;
35
+ import { useSpinButton } from '@react-aria/spinbutton' ;
36
36
import { useTextField } from '@react-aria/textfield' ;
37
37
38
- interface NumberFieldProps extends AriaNumberFieldProps , SpinButtonProps {
39
- inputRef ?: RefObject < HTMLInputElement > ,
40
- decrementAriaLabel ?: string ,
41
- incrementAriaLabel ?: string
42
- }
43
-
44
38
interface NumberFieldAria {
39
+ /** Props for the label element. */
45
40
labelProps : LabelHTMLAttributes < HTMLLabelElement > ,
46
- inputFieldProps : InputHTMLAttributes < HTMLInputElement > ,
47
- numberFieldProps : HTMLAttributes < HTMLDivElement > ,
41
+ /** Props for the group wrapper around the input and stepper buttons. */
42
+ groupProps : HTMLAttributes < HTMLElement > ,
43
+ /** Props for the input element. */
44
+ inputProps : InputHTMLAttributes < HTMLInputElement > ,
45
+ /** Props for the increment button, to be passed to [useButton](useButton.html). */
48
46
incrementButtonProps : AriaButtonProps ,
47
+ /** Props for the decrement button, to be passed to [useButton](useButton.html). */
49
48
decrementButtonProps : AriaButtonProps
50
49
}
51
50
@@ -56,7 +55,7 @@ function supportsNativeBeforeInputEvent() {
56
55
typeof InputEvent . prototype . getTargetRanges === 'function' ;
57
56
}
58
57
59
- export function useNumberField ( props : NumberFieldProps , state : NumberFieldState ) : NumberFieldAria {
58
+ export function useNumberField ( props : AriaNumberFieldProps , state : NumberFieldState , inputRef : RefObject < HTMLInputElement > ) : NumberFieldAria {
60
59
let {
61
60
decrementAriaLabel,
62
61
incrementAriaLabel,
@@ -68,8 +67,7 @@ export function useNumberField(props: NumberFieldProps, state: NumberFieldState)
68
67
autoFocus,
69
68
validationState,
70
69
label,
71
- formatOptions,
72
- inputRef
70
+ formatOptions
73
71
} = props ;
74
72
75
73
let {
@@ -83,7 +81,7 @@ export function useNumberField(props: NumberFieldProps, state: NumberFieldState)
83
81
84
82
const formatMessage = useMessageFormatter ( intlMessages ) ;
85
83
86
- const inputId = useId ( ) ;
84
+ let inputId = useId ( ) ;
87
85
88
86
let { focusProps} = useFocus ( {
89
87
onBlur : ( ) => {
@@ -92,7 +90,7 @@ export function useNumberField(props: NumberFieldProps, state: NumberFieldState)
92
90
}
93
91
} ) ;
94
92
95
- const {
93
+ let {
96
94
spinButtonProps,
97
95
incrementButtonProps : incButtonProps ,
98
96
decrementButtonProps : decButtonProps
@@ -132,7 +130,7 @@ export function useNumberField(props: NumberFieldProps, state: NumberFieldState)
132
130
incrementAriaLabel = incrementAriaLabel || formatMessage ( 'Increment' ) ;
133
131
decrementAriaLabel = decrementAriaLabel || formatMessage ( 'Decrement' ) ;
134
132
135
- const incrementButtonProps : AriaButtonProps = mergeProps ( incButtonProps , {
133
+ let incrementButtonProps : AriaButtonProps = mergeProps ( incButtonProps , {
136
134
'aria-label' : incrementAriaLabel ,
137
135
'aria-controls' : inputId ,
138
136
excludeFromTabOrder : true ,
@@ -141,7 +139,7 @@ export function useNumberField(props: NumberFieldProps, state: NumberFieldState)
141
139
onPressStart : onButtonPressStart
142
140
} ) ;
143
141
144
- const decrementButtonProps : AriaButtonProps = mergeProps ( decButtonProps , {
142
+ let decrementButtonProps : AriaButtonProps = mergeProps ( decButtonProps , {
145
143
'aria-label' : decrementAriaLabel ,
146
144
'aria-controls' : inputId ,
147
145
excludeFromTabOrder : true ,
@@ -285,54 +283,53 @@ export function useNumberField(props: NumberFieldProps, state: NumberFieldState)
285
283
} ;
286
284
287
285
let compositionStartState = useRef ( null ) ;
288
- let { labelProps, inputProps} = useTextField (
289
- {
290
- label,
291
- autoFocus,
292
- isDisabled,
293
- isReadOnly,
294
- isRequired,
295
- validationState,
296
- value : state . inputValue ,
297
- autoComplete : 'off' ,
298
- 'aria-label' : props [ 'aria-label' ] || null ,
299
- 'aria-labelledby' : props [ 'aria-labelledby' ] || null ,
300
- id : inputId ,
301
- type : 'text' , // Can't use type="number" because then we can't have things like $ in the field.
302
- inputMode,
303
- onChange,
304
- onBeforeInput,
305
- onCompositionStart ( ) {
306
- // Chrome does not implement Input Events Level 2, which specifies the insertFromComposition
307
- // and deleteByComposition inputType values for the beforeinput event. These are meant to occur
308
- // at the end of a composition (e.g. Pinyin IME, Android auto correct, etc.), and crucially, are
309
- // cancelable. The insertCompositionText and deleteCompositionText input types are not cancelable,
310
- // nor would we want to cancel them because the input from the user is incomplete at that point.
311
- // In Safari, insertFromComposition/deleteFromComposition will fire, however, allowing us to cancel
312
- // the final composition result if it is invalid. As a fallback for Chrome and Firefox, which either
313
- // don't support Input Events Level 2, or beforeinput at all, we store the state of the input when
314
- // the compositionstart event fires, and undo the changes in compositionend (below) if it is invalid.
315
- // Unfortunately, this messes up the undo/redo stack, but until insertFromComposition/deleteByComposition
316
- // are implemented, there is no other way to prevent composed input.
317
- // See https://bugs.chromium.org/p/chromium/issues/detail?id=1022204
318
- let { value, selectionStart, selectionEnd} = inputRef . current ;
319
- compositionStartState . current = { value, selectionStart, selectionEnd} ;
320
- } ,
321
- onCompositionEnd ( ) {
322
- if ( ! state . validate ( inputRef . current . value ) ) {
323
- // Restore the input value in the DOM immediately so we can synchronously update the selection position.
324
- // But also update the value in React state as well so it is correct for future updates.
325
- let { value, selectionStart, selectionEnd} = compositionStartState . current ;
326
- inputRef . current . value = value ;
327
- inputRef . current . setSelectionRange ( selectionStart , selectionEnd ) ;
328
- state . setInputValue ( value ) ;
329
- }
286
+ let { labelProps, inputProps : textFieldProps } = useTextField ( {
287
+ label,
288
+ autoFocus,
289
+ isDisabled,
290
+ isReadOnly,
291
+ isRequired,
292
+ validationState,
293
+ value : state . inputValue ,
294
+ autoComplete : 'off' ,
295
+ 'aria-label' : props [ 'aria-label' ] || null ,
296
+ 'aria-labelledby' : props [ 'aria-labelledby' ] || null ,
297
+ id : inputId ,
298
+ type : 'text' , // Can't use type="number" because then we can't have things like $ in the field.
299
+ inputMode,
300
+ onChange,
301
+ onBeforeInput,
302
+ onCompositionStart ( ) {
303
+ // Chrome does not implement Input Events Level 2, which specifies the insertFromComposition
304
+ // and deleteByComposition inputType values for the beforeinput event. These are meant to occur
305
+ // at the end of a composition (e.g. Pinyin IME, Android auto correct, etc.), and crucially, are
306
+ // cancelable. The insertCompositionText and deleteCompositionText input types are not cancelable,
307
+ // nor would we want to cancel them because the input from the user is incomplete at that point.
308
+ // In Safari, insertFromComposition/deleteFromComposition will fire, however, allowing us to cancel
309
+ // the final composition result if it is invalid. As a fallback for Chrome and Firefox, which either
310
+ // don't support Input Events Level 2, or beforeinput at all, we store the state of the input when
311
+ // the compositionstart event fires, and undo the changes in compositionend (below) if it is invalid.
312
+ // Unfortunately, this messes up the undo/redo stack, but until insertFromComposition/deleteByComposition
313
+ // are implemented, there is no other way to prevent composed input.
314
+ // See https://bugs.chromium.org/p/chromium/issues/detail?id=1022204
315
+ let { value, selectionStart, selectionEnd} = inputRef . current ;
316
+ compositionStartState . current = { value, selectionStart, selectionEnd} ;
317
+ } ,
318
+ onCompositionEnd ( ) {
319
+ if ( ! state . validate ( inputRef . current . value ) ) {
320
+ // Restore the input value in the DOM immediately so we can synchronously update the selection position.
321
+ // But also update the value in React state as well so it is correct for future updates.
322
+ let { value, selectionStart, selectionEnd} = compositionStartState . current ;
323
+ inputRef . current . value = value ;
324
+ inputRef . current . setSelectionRange ( selectionStart , selectionEnd ) ;
325
+ state . setInputValue ( value ) ;
330
326
}
331
- } , inputRef ) ;
327
+ }
328
+ } , inputRef ) ;
332
329
333
- const inputFieldProps = mergeProps (
330
+ let inputProps = mergeProps (
334
331
spinButtonProps ,
335
- inputProps ,
332
+ textFieldProps ,
336
333
focusProps ,
337
334
{
338
335
// override the spinbutton role, we can't focus a spin button with VO
@@ -347,14 +344,15 @@ export function useNumberField(props: NumberFieldProps, state: NumberFieldState)
347
344
spellCheck : 'false'
348
345
}
349
346
) ;
347
+
350
348
return {
351
- numberFieldProps : {
349
+ groupProps : {
352
350
role : 'group' ,
353
351
'aria-disabled' : isDisabled ,
354
352
'aria-invalid' : validationState === 'invalid' ? 'true' : undefined
355
353
} ,
356
354
labelProps,
357
- inputFieldProps ,
355
+ inputProps ,
358
356
incrementButtonProps,
359
357
decrementButtonProps
360
358
} ;
0 commit comments