|
13 | 13 | import {announce} from '@react-aria/live-announcer'; |
14 | 14 | import {AriaButtonProps} from '@react-types/button'; |
15 | 15 | import {AriaNumberFieldProps} from '@react-types/numberfield'; |
16 | | -import {chain, filterDOMProps, getActiveElement, getEventTarget, isAndroid, isIOS, isIPhone, mergeProps, useFormReset, useId} from '@react-aria/utils'; |
| 16 | +import {chain, filterDOMProps, getActiveElement, getEventTarget, isAndroid, isIOS, isIPhone, mergeProps, useFormReset, useId, useLayoutEffect} from '@react-aria/utils'; |
17 | 17 | import { |
18 | 18 | type ClipboardEvent, |
19 | 19 | type ClipboardEventHandler, |
@@ -262,6 +262,7 @@ export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldSt |
262 | 262 | }, state, inputRef); |
263 | 263 |
|
264 | 264 | useFormReset(inputRef, state.defaultNumberValue, state.setNumberValue); |
| 265 | + useNativeValidation(state, props.validationBehavior, props.commitBehavior, inputRef, state.minValue, state.maxValue, props.step, state.numberValue); |
265 | 266 |
|
266 | 267 | let inputProps: InputHTMLAttributes<HTMLInputElement> = mergeProps( |
267 | 268 | spinButtonProps, |
@@ -363,3 +364,69 @@ export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldSt |
363 | 364 | validationDetails |
364 | 365 | }; |
365 | 366 | } |
| 367 | + |
| 368 | +let numberInput: HTMLInputElement | null = null; |
| 369 | + |
| 370 | +function useNativeValidation( |
| 371 | + state: NumberFieldState, |
| 372 | + validationBehavior: 'native' | 'aria' | undefined, |
| 373 | + commitBehavior: 'snap' | 'validate' | undefined, |
| 374 | + inputRef: RefObject<HTMLInputElement | null>, |
| 375 | + min: number | undefined, |
| 376 | + max: number | undefined, |
| 377 | + step: number | undefined, |
| 378 | + value: number | undefined |
| 379 | +) { |
| 380 | + useLayoutEffect(() => { |
| 381 | + let input = inputRef.current; |
| 382 | + if (commitBehavior !== 'validate' || state.realtimeValidation.isInvalid || !input || input.disabled) { |
| 383 | + return; |
| 384 | + } |
| 385 | + |
| 386 | + // Create a native number input and use it to implement validation of min/max/step. |
| 387 | + // This lets us get the native validation message provided by the browser instead of needing our own translations. |
| 388 | + if (!numberInput && typeof document !== 'undefined') { |
| 389 | + numberInput = document.createElement('input'); |
| 390 | + numberInput.type = 'number'; |
| 391 | + } |
| 392 | + |
| 393 | + if (!numberInput) { |
| 394 | + // For TypeScript. |
| 395 | + return; |
| 396 | + } |
| 397 | + |
| 398 | + numberInput.min = min != null && !isNaN(min) ? String(min) : ''; |
| 399 | + numberInput.max = max != null && !isNaN(max) ? String(max) : ''; |
| 400 | + numberInput.step = step != null && !isNaN(step) ? String(step) : ''; |
| 401 | + numberInput.value = value != null && !isNaN(value) ? String(value) : ''; |
| 402 | + |
| 403 | + // Merge validity with the visible text input (for other validations like required). |
| 404 | + let valid = input.validity.valid && numberInput.validity.valid; |
| 405 | + let validationMessage = input.validationMessage || numberInput.validationMessage; |
| 406 | + let validity = { |
| 407 | + isInvalid: !valid, |
| 408 | + validationErrors: validationMessage ? [validationMessage] : [], |
| 409 | + validationDetails: { |
| 410 | + badInput: input.validity.badInput, |
| 411 | + customError: input.validity.customError, |
| 412 | + patternMismatch: input.validity.patternMismatch, |
| 413 | + rangeOverflow: numberInput.validity.rangeOverflow, |
| 414 | + rangeUnderflow: numberInput.validity.rangeUnderflow, |
| 415 | + stepMismatch: numberInput.validity.stepMismatch, |
| 416 | + tooLong: input.validity.tooLong, |
| 417 | + tooShort: input.validity.tooShort, |
| 418 | + typeMismatch: input.validity.typeMismatch, |
| 419 | + valueMissing: input.validity.valueMissing, |
| 420 | + valid |
| 421 | + } |
| 422 | + }; |
| 423 | + |
| 424 | + state.updateValidation(validity); |
| 425 | + |
| 426 | + // Block form submission if validation behavior is native. |
| 427 | + // This won't overwrite any user-defined validation message because we checked realtimeValidation above. |
| 428 | + if (validationBehavior === 'native' && !numberInput.validity.valid) { |
| 429 | + input.setCustomValidity(numberInput.validationMessage); |
| 430 | + } |
| 431 | + }); |
| 432 | +} |
0 commit comments