Repository: adobe/react-spectrum
File: packages/@react-aria/numberfield/src/useNumberField.ts (TypeScript)

Code before:
      inputMode = 'decimal';
    }
  }

  let onChange = value => {
    if (state.validate(value)) {
      state.setInputValue(value);
    }
  };

  let onPaste: ClipboardEventHandler<HTMLInputElement> = (e: ClipboardEvent<HTMLInputElement>) => {
    props.onPaste?.(e);
    let inputElement = e.target as HTMLInputElement;
    if (
      inputElement &&
      (
        ((inputElement.selectionEnd ?? -1) - (inputElement.selectionStart ?? 0)) === inputElement.value.length
      )
    ) {
      e.preventDefault();
      let pastedText = e.clipboardData?.getData?.('text/plain')?.trim() ?? '';
      let isValid = state.validate(pastedText);
      if (isValid) {
        let value = state.parser.parse(pastedText);
        let reformattedValue = numberFormatter.format(value);
        if (state.validate(reformattedValue)) {
          state.setInputValue(reformattedValue);
          if (reformattedValue !== pastedText) {
            announce(stringFormatter.format('pastedValue', {value: reformattedValue}), 'polite');
          }
        }
      } else {
        announce(stringFormatter.format('couldNotParseValue', {value: pastedText}), 'polite');
      }
    }
  };

  let domProps = filterDOMProps(props);
  let onKeyDownEnter = useCallback((e) => {
    if (e.nativeEvent.isComposing) {
      return;
    }

    if (e.key === 'Enter') {
      commit();
      commitValidation();
    } else {
      e.continuePropagation();
    }
  }, [commit, commitValidation]);


Review comment (suggestion):
Does there exist a case where the `pastedText` is considered valid but is unable to be parsed? The description of `parse` seems to suggest that might be the case, but digging to see if that is the case. Just concerned if there are any cases where we make it here but end up not announcing that we couldn't parse the value

Code after:
    if (hasNegative) {
      inputMode = 'text';
    } else if (hasDecimals) {
      inputMode = 'decimal';
    }
  } else if (isAndroid()) {
    // Android numeric has both a decimal point and minus key.
    // decimal does not have a minus key.
    if (hasNegative) {
      inputMode = 'numeric';
    } else if (hasDecimals) {
      inputMode = 'decimal';
    }
  }

  let onChange = value => {
    if (state.validate(value)) {
      state.setInputValue(value);
    }
  };

  let onPaste: ClipboardEventHandler<HTMLInputElement> = (e: ClipboardEvent<HTMLInputElement>) => {
    props.onPaste?.(e);
    let inputElement = e.target as HTMLInputElement;
    // we can only handle the case where the paste takes over the entire input, otherwise things get very complicated
    // trying to calculate the new string based on what the paste is replacing and where in the source string it is
    if (inputElement &&
      ((inputElement.selectionEnd ?? -1) - (inputElement.selectionStart ?? 0)) === inputElement.value.length
    ) {
      e.preventDefault();
      // commit so that the user gets to see what it formats to immediately
      // paste happens before inputRef's value is updated, so have to prevent the default and do it ourselves
      // spin button will then handle announcing the new value, this should work with controlled state as well
      // because the announcement is done as a result of the new rendered input value if there is one
      commit(e.clipboardData?.getData?.('text/plain')?.trim() ?? '');
    }
  };

  let domProps = filterDOMProps(props);
  let onKeyDownEnter = useCallback((e) => {
    if (e.nativeEvent.isComposing) {
      return;
    }

    if (e.key === 'Enter') {
      commit();
      commitValidation();
    } else {
      e.continuePropagation();
    }
  }, [commit, commitValidation]);