@@ -18,9 +18,7 @@ import {
18
18
LabelHTMLAttributes ,
19
19
RefObject ,
20
20
useCallback ,
21
- useEffect ,
22
21
useMemo ,
23
- useRef ,
24
22
useState
25
23
} from 'react' ;
26
24
// @ts -ignore
@@ -29,12 +27,13 @@ import {isAndroid, isIOS, isIPhone, mergeProps, useId} from '@react-aria/utils';
29
27
import { NumberFieldState } from '@react-stately/numberfield' ;
30
28
import { TextInputDOMProps } from '@react-types/shared' ;
31
29
import { useFocus , useFocusWithin } from '@react-aria/interactions' ;
30
+ import { useFormattedTextField } from '@react-aria/textfield' ;
32
31
import {
33
32
useMessageFormatter ,
34
33
useNumberFormatter
35
34
} from '@react-aria/i18n' ;
35
+ import { useScrollWheel } from '@react-aria/interactions' ;
36
36
import { useSpinButton } from '@react-aria/spinbutton' ;
37
- import { useTextField } from '@react-aria/textfield' ;
38
37
39
38
interface NumberFieldAria {
40
39
/** Props for the label element. */
@@ -49,13 +48,6 @@ interface NumberFieldAria {
49
48
decrementButtonProps : AriaButtonProps
50
49
}
51
50
52
- function supportsNativeBeforeInputEvent ( ) {
53
- return typeof window !== 'undefined' &&
54
- window . InputEvent &&
55
- // @ts -ignore
56
- typeof InputEvent . prototype . getTargetRanges === 'function' ;
57
- }
58
-
59
51
/**
60
52
* Provides the behavior and accessibility implementation for a number field component.
61
53
* Number fields allow users to enter a number, and increment or decrement the value using stepper buttons.
@@ -120,14 +112,6 @@ export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldSt
120
112
let { focusWithinProps} = useFocusWithin ( { isDisabled, onFocusWithinChange : setFocusWithin } ) ;
121
113
122
114
let onWheel = useCallback ( ( e ) => {
123
- // If the ctrlKey is pressed, this is a zoom event, do nothing.
124
- if ( e . ctrlKey ) {
125
- return ;
126
- }
127
-
128
- // stop scrolling the page
129
- e . preventDefault ( ) ;
130
-
131
115
if ( e . deltaY > 0 ) {
132
116
increment ( ) ;
133
117
} else if ( e . deltaY < 0 ) {
@@ -136,7 +120,7 @@ export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldSt
136
120
} , [ decrement , increment ] ) ;
137
121
// If the input isn't supposed to receive input, disable scrolling.
138
122
let scrollingDisabled = isDisabled || isReadOnly || ! focusWithin ;
139
- useScrollWheel ( { onScroll : onWheel , capture : false , isDisabled : scrollingDisabled } , inputRef ) ;
123
+ useScrollWheel ( { onScroll : onWheel , isDisabled : scrollingDisabled } , inputRef ) ;
140
124
141
125
// The inputMode attribute influences the software keyboard that is shown on touch devices.
142
126
// Browsers and operating systems are quite inconsistent about what keys are available, however.
@@ -166,96 +150,11 @@ export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldSt
166
150
}
167
151
}
168
152
169
- let stateRef = useRef ( state ) ;
170
- stateRef . current = state ;
171
-
172
- // All browsers implement the 'beforeinput' event natively except Firefox
173
- // (currently behind a flag as of Firefox 84). React's polyfill does not
174
- // run in all cases that the native event fires, e.g. when deleting text.
175
- // Use the native event if available so that we can prevent invalid deletions.
176
- // We do not attempt to polyfill this in Firefox since it would be very complicated,
177
- // the benefit of doing so is fairly minor, and it's going to be natively supported soon.
178
- useEffect ( ( ) => {
179
- if ( ! supportsNativeBeforeInputEvent ( ) ) {
180
- return ;
181
- }
182
-
183
- let input = inputRef . current ;
184
-
185
- let onBeforeInput = ( e : InputEvent ) => {
186
- let state = stateRef . current ;
187
-
188
- // Compute the next value of the input if the event is allowed to proceed.
189
- // See https://www.w3.org/TR/input-events-2/#interface-InputEvent-Attributes for a full list of input types.
190
- let nextValue : string ;
191
- switch ( e . inputType ) {
192
- case 'historyUndo' :
193
- case 'historyRedo' :
194
- // Explicitly allow undo/redo. e.data is null in this case, but there's no need to validate,
195
- // because presumably the input would have already been validated previously.
196
- return ;
197
- case 'deleteContent' :
198
- case 'deleteByCut' :
199
- case 'deleteByDrag' :
200
- nextValue = input . value . slice ( 0 , input . selectionStart ) + input . value . slice ( input . selectionEnd ) ;
201
- break ;
202
- case 'deleteContentForward' :
203
- // This is potentially incorrect, since the browser may actually delete more than a single UTF-16
204
- // character. In reality, a full Unicode grapheme cluster consisting of multiple UTF-16 characters
205
- // or code points may be deleted. However, in our currently supported locales, there are no such cases.
206
- // If we support additional locales in the future, this may need to change.
207
- nextValue = input . selectionEnd === input . selectionStart
208
- ? input . value . slice ( 0 , input . selectionStart ) + input . value . slice ( input . selectionEnd + 1 )
209
- : input . value . slice ( 0 , input . selectionStart ) + input . value . slice ( input . selectionEnd ) ;
210
- break ;
211
- case 'deleteContentBackward' :
212
- nextValue = input . selectionEnd === input . selectionStart
213
- ? input . value . slice ( 0 , input . selectionStart - 1 ) + input . value . slice ( input . selectionStart )
214
- : input . value . slice ( 0 , input . selectionStart ) + input . value . slice ( input . selectionEnd ) ;
215
- break ;
216
- default :
217
- if ( e . data != null ) {
218
- nextValue =
219
- input . value . slice ( 0 , input . selectionStart ) +
220
- e . data +
221
- input . value . slice ( input . selectionEnd ) ;
222
- }
223
- break ;
224
- }
225
-
226
- // If we did not compute a value, or the new value is invalid, prevent the event
227
- // so that the browser does not update the input text, move the selection, or add to
228
- // the undo/redo stack.
229
- if ( nextValue == null || ! state . validate ( nextValue ) ) {
230
- e . preventDefault ( ) ;
231
- }
232
- } ;
233
-
234
- input . addEventListener ( 'beforeinput' , onBeforeInput , false ) ;
235
- return ( ) => {
236
- input . removeEventListener ( 'beforeinput' , onBeforeInput , false ) ;
237
- } ;
238
- } , [ inputRef , stateRef ] ) ;
239
-
240
- let onBeforeInput = ! supportsNativeBeforeInputEvent ( )
241
- ? e => {
242
- let nextValue =
243
- e . target . value . slice ( 0 , e . target . selectionStart ) +
244
- e . data +
245
- e . target . value . slice ( e . target . selectionEnd ) ;
246
-
247
- if ( ! state . validate ( nextValue ) ) {
248
- e . preventDefault ( ) ;
249
- }
250
- }
251
- : null ;
252
-
253
153
let onChange = value => {
254
154
state . setInputValue ( value ) ;
255
155
} ;
256
156
257
- let compositionStartState = useRef ( null ) ;
258
- let { labelProps, inputProps : textFieldProps } = useTextField ( {
157
+ let { labelProps, inputProps : textFieldProps } = useFormattedTextField ( {
259
158
label,
260
159
autoFocus,
261
160
isDisabled,
@@ -269,35 +168,8 @@ export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldSt
269
168
id : inputId ,
270
169
type : 'text' , // Can't use type="number" because then we can't have things like $ in the field.
271
170
inputMode,
272
- onChange,
273
- onBeforeInput,
274
- onCompositionStart ( ) {
275
- // Chrome does not implement Input Events Level 2, which specifies the insertFromComposition
276
- // and deleteByComposition inputType values for the beforeinput event. These are meant to occur
277
- // at the end of a composition (e.g. Pinyin IME, Android auto correct, etc.), and crucially, are
278
- // cancelable. The insertCompositionText and deleteCompositionText input types are not cancelable,
279
- // nor would we want to cancel them because the input from the user is incomplete at that point.
280
- // In Safari, insertFromComposition/deleteFromComposition will fire, however, allowing us to cancel
281
- // the final composition result if it is invalid. As a fallback for Chrome and Firefox, which either
282
- // don't support Input Events Level 2, or beforeinput at all, we store the state of the input when
283
- // the compositionstart event fires, and undo the changes in compositionend (below) if it is invalid.
284
- // Unfortunately, this messes up the undo/redo stack, but until insertFromComposition/deleteByComposition
285
- // are implemented, there is no other way to prevent composed input.
286
- // See https://bugs.chromium.org/p/chromium/issues/detail?id=1022204
287
- let { value, selectionStart, selectionEnd} = inputRef . current ;
288
- compositionStartState . current = { value, selectionStart, selectionEnd} ;
289
- } ,
290
- onCompositionEnd ( ) {
291
- if ( ! state . validate ( inputRef . current . value ) ) {
292
- // Restore the input value in the DOM immediately so we can synchronously update the selection position.
293
- // But also update the value in React state as well so it is correct for future updates.
294
- let { value, selectionStart, selectionEnd} = compositionStartState . current ;
295
- inputRef . current . value = value ;
296
- inputRef . current . setSelectionRange ( selectionStart , selectionEnd ) ;
297
- state . setInputValue ( value ) ;
298
- }
299
- }
300
- } , inputRef ) ;
171
+ onChange
172
+ } , state , inputRef ) ;
301
173
302
174
let inputProps = mergeProps (
303
175
spinButtonProps ,
@@ -388,20 +260,3 @@ export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldSt
388
260
decrementButtonProps
389
261
} ;
390
262
}
391
-
392
- // scroll wheel needs to be added not passively so it's cancelable, small helper hook to remember that
393
- function useScrollWheel ( { onScroll, capture, isDisabled} : { onScroll : ( e ) => void , capture : boolean , isDisabled : boolean } , ref : RefObject < HTMLElement > ) {
394
- useEffect ( ( ) => {
395
- let elem = ref . current ;
396
-
397
- if ( ! isDisabled ) {
398
- elem . addEventListener ( 'wheel' , onScroll , capture ) ;
399
- }
400
-
401
- return ( ) => {
402
- if ( ! isDisabled ) {
403
- elem . removeEventListener ( 'wheel' , onScroll , capture ) ;
404
- }
405
- } ;
406
- } , [ onScroll , ref , capture , isDisabled ] ) ;
407
- }
0 commit comments