This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
The MIT License (MIT)
Copyright (c) 2014-present yiminghe
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+277
View File
@@ -0,0 +1,277 @@
# @rc-component/input-number
Input number control.
[![NPM version][npm-image]][npm-url]
[![npm download][download-image]][download-url]
[![build status][github-actions-image]][github-actions-url]
[![Codecov][codecov-image]][codecov-url]
[![bundle size][bundlephobia-image]][bundlephobia-url]
[![dumi][dumi-image]][dumi-url]
[npm-image]: http://img.shields.io/npm/v/@rc-component/input-number.svg?style=flat-square
[npm-url]: http://npmjs.org/package/@rc-component/input-number
[travis-image]: https://img.shields.io/travis/react-component/input-number/master?style=flat-square
[travis-url]: https://travis-ci.com/react-component/input-number
[github-actions-image]: https://github.com/react-component/input-number/actions/workflows/react-component-ci.yml/badge.svg
[github-actions-url]: https://github.com/react-component/input-number/actions/workflows/react-component-ci.yml
[codecov-image]: https://img.shields.io/codecov/c/github/react-component/input-number/master.svg?style=flat-square
[codecov-url]: https://app.codecov.io/gh/react-component/input-number
[david-url]: https://david-dm.org/react-component/input-number
[david-image]: https://david-dm.org/react-component/input-number/status.svg?style=flat-square
[david-dev-url]: https://david-dm.org/react-component/input-number?type=dev
[david-dev-image]: https://david-dm.org/react-component/input-number/dev-status.svg?style=flat-square
[download-image]: https://img.shields.io/npm/dm/@rc-component/input-number.svg?style=flat-square
[download-url]: https://npmjs.org/package/@rc-component/input-number
[bundlephobia-url]: https://bundlephobia.com/package/@rc-component/input-number
[bundlephobia-image]: https://badgen.net/bundlephobia/minzip/@rc-component/input-number
[dumi-url]: https://github.com/umijs/dumi
[dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square
## Screenshots
<img src="https://user-images.githubusercontent.com/507615/83162463-61414a80-a13c-11ea-9420-971f8697d490.png" width="288"/>
## Install
[![@rc-component/input-number](https://nodei.co/npm/@rc-component/input-number.png)](https://npmjs.org/package/@rc-component/input-number)
## Usage
```js
import InputNumber from '@rc-component/input-number';
export default () => <InputNumber defaultValue={10} />;
```
## Development
```
npm install
npm start
```
## Example
http://127.0.0.1:8000/examples/
online example: https://input-number.vercel.app/
## API
### props
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="width: 100px;">name</th>
<th style="width: 50px;">type</th>
<th style="width: 50px;">default</th>
<th>description</th>
</tr>
</thead>
<tbody>
<tr>
<td>prefixCls</td>
<td>string</td>
<td>rc-input-number</td>
<td>Specifies the class prefix</td>
</tr>
<tr>
<td>min</td>
<td>Number</td>
<td></td>
<td>Specifies the minimum value</td>
</tr>
<tr>
<td>onClick</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>placeholder</td>
<td>string</td>
<td></td>
<td></td>
</tr>
<tr>
<td>max</td>
<td>Number</td>
<td></td>
<td>Specifies the maximum value</td>
</tr>
<tr>
<td>step</td>
<td>Number or String</td>
<td>1</td>
<td>Specifies the legal number intervals</td>
</tr>
<tr>
<td>precision</td>
<td>Number</td>
<td></td>
<td>Specifies the precision length of value</td>
</tr>
<tr>
<td>disabled</td>
<td>Boolean</td>
<td>false</td>
<td>Specifies that an InputNumber should be disabled</td>
</tr>
<tr>
<td>required</td>
<td>Boolean</td>
<td>false</td>
<td>Specifies that an InputNumber is required</td>
</tr>
<tr>
<td>autoFocus</td>
<td>Boolean</td>
<td>false</td>
<td>Specifies that an InputNumber should automatically get focus when the page loads</td>
</tr>
<tr>
<td>readOnly</td>
<td>Boolean</td>
<td>false</td>
<td>Specifies that an InputNumber is read only </td>
</tr>
<tr>
<td>controls</td>
<td>Boolean</td>
<td>true</td>
<td>Whether to enable the control buttons</td>
</tr>
<tr>
<td>name</td>
<td>String</td>
<td></td>
<td>Specifies the name of an InputNumber</td>
</tr>
<tr>
<td>id</td>
<td>String</td>
<td></td>
<td>Specifies the id of an InputNumber</td>
</tr>
<tr>
<td>value</td>
<td>Number</td>
<td></td>
<td>Specifies the value of an InputNumber</td>
</tr>
<tr>
<td>defaultValue</td>
<td>Number</td>
<td></td>
<td>Specifies the defaultValue of an InputNumber</td>
</tr>
<tr>
<td>onChange</td>
<td>Function</td>
<td></td>
<td>Called when value of an InputNumber changed</td>
</tr>
<tr>
<td>onBlur</td>
<td>Function</td>
<td></td>
<td>Called when user leaves an input field</td>
</tr>
<tr>
<td>onPressEnter</td>
<td>Function</td>
<td></td>
<td>The callback function that is triggered when Enter key is pressed.</td>
</tr>
<tr>
<td>onFocus</td>
<td>Function</td>
<td></td>
<td>Called when an element gets focus</td>
</tr>
<tr>
<td>style</td>
<td>Object</td>
<td></td>
<td>root style. such as {width:100}</td>
</tr>
<tr>
<td>upHandler</td>
<td>React.Node</td>
<td></td>
<td>custom the up step element</td>
</tr>
<tr>
<td>downHandler</td>
<td>React.Node</td>
<td></td>
<td>custom the down step element</td>
</tr>
<tr>
<td>formatter</td>
<td>(value: number|string): displayValue: string</td>
<td></td>
<td>Specifies the format of the value presented</td>
</tr>
<tr>
<td>parser</td>
<td>(displayValue: string) => value: number</td>
<td>`input => input.replace(/[^\w\.-]*/g, '')`</td>
<td>Specifies the value extracted from formatter</td>
</tr>
<tr>
<td>pattern</td>
<td>string</td>
<td></td>
<td>Specifies a regex pattern to be added to the input number element - useful for forcing iOS to open the number pad instead of the normal keyboard (supply a regex of "\d*" to do this) or form validation</td>
</tr>
<tr>
<td>decimalSeparator</td>
<td>string</td>
<td></td>
<td>Specifies the decimal separator</td>
</tr>
<tr>
<td>inputMode</td>
<td>string</td>
<td></td>
<td>Specifies the inputmode of input</td>
</tr>
<tr>
<td>wheel</td>
<td>Boolean</td>
<td>true</td>
<td>Allows changing value with mouse wheel</td>
</tr>
</tbody>
</table>
## Keyboard Navigation
* When you hit the <kbd>⬆</kbd> or <kbd>⬇</kbd> key, the input value will be increased or decreased by `step`
* With the <kbd>Shift</kbd> key (<kbd>Shift+⬆</kbd>, <kbd>Shift+⬇</kbd>), the input value will be changed by `10 * step`
* With the <kbd>Ctrl</kbd> or <kbd>⌘</kbd> key (<kbd>Ctrl+⬆</kbd> or <kbd>⌘+⬆</kbd> or <kbd>Ctrl+⬇</kbd> or <kbd>⌘+⬇</kbd> ), the input value will be changed by `0.1 * step`
## Mouse Wheel
* When you scroll up or down, the input value will be increased or decreased by `step`
* Scrolling with the <kbd>Shift</kbd> key, the input value will be changed by `10 * step`
## Test Case
```
npm test
npm run chrome-test
```
## Coverage
```
npm run coverage
```
open coverage/ dir
## License
@rc-component/input-number is released under the MIT license.
@@ -0,0 +1,123 @@
.rc-input-number {
display: inline-flex;
flex-wrap: nowrap;
height: 26px;
margin: 0;
padding: 0;
font-size: 12px;
line-height: 26px;
vertical-align: middle;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: all 0.3s;
}
.rc-input-number-focused {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.rc-input-number-out-of-range input {
color: red;
}
.rc-input-number-action {
display: block;
height: 12px;
overflow: hidden;
line-height: 12px;
text-align: center;
touch-action: none;
}
.rc-input-number-action-active {
background: #ddd;
}
.rc-input-number-action-up-inner,
.rc-input-number-action-down-inner {
color: #666666;
-webkit-user-select: none;
user-select: none;
}
.rc-input-number:hover {
border-color: #1890ff;
}
.rc-input-number:hover .rc-input-number-action-up,
.rc-input-number:hover .rc-input-number-actions {
border-color: #1890ff;
}
.rc-input-number-disabled:hover {
border-color: #d9d9d9;
}
.rc-input-number-disabled:hover .rc-input-number-action-up,
.rc-input-number-disabled:hover .rc-input-number-actions {
border-color: #d9d9d9;
}
.rc-input-number-input {
width: 100%;
height: 100%;
padding: 0;
color: #666666;
line-height: 26px;
text-align: center;
border: 0;
border-radius: 4px;
outline: 0;
transition: all 0.3s ease;
transition: all 0.3s;
-moz-appearance: textfield;
}
.rc-input-number-actions {
width: 20px;
height: 100%;
border-left: 1px solid #d9d9d9;
transition: all 0.3s;
}
.rc-input-number-action-up {
padding-top: 1px;
border-bottom: 1px solid #d9d9d9;
transition: all 0.3s;
}
.rc-input-number-action-up-inner:after {
content: '+';
}
.rc-input-number-action-down {
transition: all 0.3s;
}
.rc-input-number-action-down-inner:after {
content: '-';
}
.rc-input-number-action-down-disabled,
.rc-input-number-action-up-disabled {
opacity: 0.3;
}
.rc-input-number-action-down-disabled:hover,
.rc-input-number-action-up-disabled:hover {
color: #999;
border-color: #d9d9d9;
}
.rc-input-number-disabled .rc-input-number-input {
background-color: #f3f3f3;
cursor: not-allowed;
opacity: 0.72;
}
.rc-input-number-disabled .rc-input-number-action {
opacity: 0.3;
}
.rc-input-number-disabled .rc-input-number-action:hover {
color: #999;
border-color: #d9d9d9;
}
.rc-input-number-mode-spinner {
display: inline-flex;
align-items: center;
}
.rc-input-number-mode-spinner .rc-input-number-action {
flex: 0 0 20px;
height: 100%;
line-height: 26px;
}
.rc-input-number-mode-spinner .rc-input-number-action-up {
border-bottom: 0;
border-left: 1px solid #d9d9d9;
}
.rc-input-number-mode-spinner .rc-input-number-action-down {
border-top: 0;
border-right: 1px solid #d9d9d9;
}
@@ -0,0 +1,65 @@
import { ValueType } from '@rc-component/mini-decimal';
import * as React from 'react';
import { type InputFocusOptions } from '@rc-component/util/lib/Dom/focus';
export type { ValueType };
export interface InputNumberRef extends HTMLInputElement {
focus: (options?: InputFocusOptions) => void;
blur: () => void;
nativeElement: HTMLElement;
}
type SemanticName = 'root' | 'actions' | 'input' | 'action' | 'prefix' | 'suffix';
export interface InputNumberProps<T extends ValueType = ValueType> extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'defaultValue' | 'onInput' | 'onChange' | 'prefix' | 'suffix' | 'onMouseDown' | 'onClick' | 'onMouseUp' | 'onMouseLeave' | 'onMouseMove' | 'onMouseEnter' | 'onMouseOut'>, Pick<React.HTMLAttributes<HTMLDivElement>, 'onMouseDown' | 'onClick' | 'onMouseUp' | 'onMouseLeave' | 'onMouseMove' | 'onMouseEnter' | 'onMouseOut'> {
disabled?: boolean;
readOnly?: boolean;
/** value will show as string */
stringMode?: boolean;
mode?: 'input' | 'spinner';
defaultValue?: T;
value?: T | null;
prefixCls?: string;
className?: string;
style?: React.CSSProperties;
min?: T;
max?: T;
step?: ValueType;
tabIndex?: number;
controls?: boolean;
prefix?: React.ReactNode;
suffix?: React.ReactNode;
classNames?: Partial<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
upHandler?: React.ReactNode;
downHandler?: React.ReactNode;
keyboard?: boolean;
changeOnWheel?: boolean;
/** Parse display value to validate number */
parser?: (displayValue: string | undefined) => T;
/** Transform `value` to display value show in input */
formatter?: (value: T | undefined, info: {
userTyping: boolean;
input: string;
}) => string;
/** Syntactic sugar of `formatter`. Config precision of display. */
precision?: number;
/** Syntactic sugar of `formatter`. Config decimal separator of display. */
decimalSeparator?: string;
onInput?: (text: string) => void;
onChange?: (value: T | null) => void;
onPressEnter?: React.KeyboardEventHandler<HTMLInputElement>;
onStep?: (value: T, info: {
offset: ValueType;
type: 'up' | 'down';
emitter: 'handler' | 'keyboard' | 'wheel';
}) => void;
/**
* Trigger change onBlur event.
* If disabled, user must press enter or click handler to confirm the value update
*/
changeOnBlur?: boolean;
}
declare const InputNumber: (<T extends ValueType = ValueType>(props: React.PropsWithChildren<InputNumberProps<T>> & {
ref?: React.Ref<HTMLInputElement>;
}) => React.ReactElement) & {
displayName?: string;
};
export default InputNumber;
@@ -0,0 +1,555 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import getMiniDecimal, { getNumberPrecision, num2str, toFixed, validateNumber } from '@rc-component/mini-decimal';
import { useLayoutUpdateEffect } from "@rc-component/util/es/hooks/useLayoutEffect";
import proxyObject from "@rc-component/util/es/proxyObject";
import { clsx } from 'clsx';
import * as React from 'react';
import useCursor from "./hooks/useCursor";
import StepHandler from "./StepHandler";
import { getDecupleSteps } from "./utils/numberUtil";
import { useEvent } from '@rc-component/util';
import { triggerFocus } from "@rc-component/util/es/Dom/focus";
import useFrame from "./hooks/useFrame";
/**
* We support `stringMode` which need handle correct type when user call in onChange
* format max or min value
* 1. if isInvalid return null
* 2. if precision is undefined, return decimal
* 3. format with precision
* I. if max > 0, round down with precision. Example: max= 3.5, precision=0 afterFormat: 3
* II. if max < 0, round up with precision. Example: max= -3.5, precision=0 afterFormat: -4
* III. if min > 0, round up with precision. Example: min= 3.5, precision=0 afterFormat: 4
* IV. if min < 0, round down with precision. Example: max= -3.5, precision=0 afterFormat: -3
*/
const getDecimalValue = (stringMode, decimalValue) => {
if (stringMode || decimalValue.isEmpty()) {
return decimalValue.toString();
}
return decimalValue.toNumber();
};
const getDecimalIfValidate = value => {
const decimal = getMiniDecimal(value);
return decimal.isInvalidate() ? null : decimal;
};
const InputNumber = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
mode = 'input',
prefixCls = 'rc-input-number',
className,
style,
classNames,
styles,
min,
max,
step = 1,
defaultValue,
value,
disabled,
readOnly,
upHandler,
downHandler,
keyboard,
changeOnWheel = false,
controls = true,
prefix,
suffix,
stringMode,
parser,
formatter,
precision,
decimalSeparator,
onChange,
onInput,
onPressEnter,
onStep,
// Mouse Events
onMouseDown,
onClick,
onMouseUp,
onMouseLeave,
onMouseMove,
onMouseEnter,
onMouseOut,
changeOnBlur = true,
...restProps
} = props;
const [focus, setFocus] = React.useState(false);
const userTypingRef = React.useRef(false);
const compositionRef = React.useRef(false);
const shiftKeyRef = React.useRef(false);
// ============================= Refs =============================
const rootRef = React.useRef(null);
const inputRef = React.useRef(null);
React.useImperativeHandle(ref, () => proxyObject(inputRef.current, {
focus: option => {
triggerFocus(inputRef.current, option);
},
blur: () => {
inputRef.current?.blur();
},
nativeElement: rootRef.current
}));
// ============================ Value =============================
// Real value control
const [decimalValue, setDecimalValue] = React.useState(() => getMiniDecimal(value ?? defaultValue));
function setUncontrolledDecimalValue(newDecimal) {
if (value === undefined) {
setDecimalValue(newDecimal);
}
}
// ====================== Parser & Formatter ======================
/**
* `precision` is used for formatter & onChange.
* It will auto generate by `value` & `step`.
* But it will not block user typing.
*
* Note: Auto generate `precision` is used for legacy logic.
* We should remove this since we already support high precision with BigInt.
*
* @param number Provide which number should calculate precision
* @param userTyping Change by user typing
*/
const getPrecision = React.useCallback((numStr, userTyping) => {
if (userTyping) {
return undefined;
}
if (precision >= 0) {
return precision;
}
return Math.max(getNumberPrecision(numStr), getNumberPrecision(step));
}, [precision, step]);
// >>> Parser
const mergedParser = React.useCallback(num => {
const numStr = String(num);
if (parser) {
return parser(numStr);
}
let parsedStr = numStr;
if (decimalSeparator) {
parsedStr = parsedStr.replace(decimalSeparator, '.');
}
// [Legacy] We still support auto convert `$ 123,456` to `123456`
return parsedStr.replace(/[^\w.-]+/g, '');
}, [parser, decimalSeparator]);
// >>> Formatter
const inputValueRef = React.useRef('');
const mergedFormatter = React.useCallback((number, userTyping) => {
if (formatter) {
return formatter(number, {
userTyping,
input: String(inputValueRef.current)
});
}
let str = typeof number === 'number' ? num2str(number) : number;
// User typing will not auto format with precision directly
if (!userTyping) {
const mergedPrecision = getPrecision(str, userTyping);
if (validateNumber(str) && (decimalSeparator || mergedPrecision >= 0)) {
// Separator
const separatorStr = decimalSeparator || '.';
str = toFixed(str, separatorStr, mergedPrecision);
}
}
return str;
}, [formatter, getPrecision, decimalSeparator]);
// ========================== InputValue ==========================
/**
* Input text value control
*
* User can not update input content directly. It updates with follow rules by priority:
* 1. controlled `value` changed
* * [SPECIAL] Typing like `1.` should not immediately convert to `1`
* 2. User typing with format (not precision)
* 3. Blur or Enter trigger revalidate
*/
const [inputValue, setInternalInputValue] = React.useState(() => {
const initValue = defaultValue ?? value;
if (decimalValue.isInvalidate() && ['string', 'number'].includes(typeof initValue)) {
return Number.isNaN(initValue) ? '' : initValue;
}
return mergedFormatter(decimalValue.toString(), false);
});
inputValueRef.current = inputValue;
// Should always be string
function setInputValue(newValue, userTyping) {
setInternalInputValue(mergedFormatter(
// Invalidate number is sometime passed by external control, we should let it go
// Otherwise is controlled by internal interactive logic which check by userTyping
// You can ref 'show limited value when input is not focused' test for more info.
newValue.isInvalidate() ? newValue.toString(false) : newValue.toString(!userTyping), userTyping));
}
// >>> Max & Min limit
const maxDecimal = React.useMemo(() => getDecimalIfValidate(max), [max, precision]);
const minDecimal = React.useMemo(() => getDecimalIfValidate(min), [min, precision]);
const upDisabled = React.useMemo(() => {
if (!maxDecimal || !decimalValue || decimalValue.isInvalidate()) {
return false;
}
return maxDecimal.lessEquals(decimalValue);
}, [maxDecimal, decimalValue]);
const downDisabled = React.useMemo(() => {
if (!minDecimal || !decimalValue || decimalValue.isInvalidate()) {
return false;
}
return decimalValue.lessEquals(minDecimal);
}, [minDecimal, decimalValue]);
// Cursor controller
const [recordCursor, restoreCursor] = useCursor(inputRef.current, focus);
// ============================= Data =============================
/**
* Find target value closet within range.
* e.g. [11, 28]:
* 3 => 11
* 23 => 23
* 99 => 28
*/
const getRangeValue = target => {
// target > max
if (maxDecimal && !target.lessEquals(maxDecimal)) {
return maxDecimal;
}
// target < min
if (minDecimal && !minDecimal.lessEquals(target)) {
return minDecimal;
}
return null;
};
/**
* Check value is in [min, max] range
*/
const isInRange = target => !getRangeValue(target);
/**
* Trigger `onChange` if value validated and not equals of origin.
* Return the value that re-align in range.
*/
const triggerValueUpdate = (newValue, userTyping) => {
let updateValue = newValue;
let isRangeValidate = isInRange(updateValue) || updateValue.isEmpty();
// Skip align value when trigger value is empty.
// We just trigger onChange(null)
// This should not block user typing
if (!updateValue.isEmpty() && !userTyping) {
// Revert value in range if needed
updateValue = getRangeValue(updateValue) || updateValue;
isRangeValidate = true;
}
if (!readOnly && !disabled && isRangeValidate) {
const numStr = updateValue.toString();
const mergedPrecision = getPrecision(numStr, userTyping);
if (mergedPrecision >= 0) {
updateValue = getMiniDecimal(toFixed(numStr, '.', mergedPrecision));
// When to fixed. The value may out of min & max range.
// 4 in [0, 3.8] => 3.8 => 4 (toFixed)
if (!isInRange(updateValue)) {
updateValue = getMiniDecimal(toFixed(numStr, '.', mergedPrecision, true));
}
}
// Trigger event
if (!updateValue.equals(decimalValue)) {
setUncontrolledDecimalValue(updateValue);
onChange?.(updateValue.isEmpty() ? null : getDecimalValue(stringMode, updateValue));
// Reformat input if value is not controlled
if (value === undefined) {
setInputValue(updateValue, userTyping);
}
}
return updateValue;
}
return decimalValue;
};
// ========================== User Input ==========================
const onNextPromise = useFrame();
// >>> Collect input value
const collectInputValue = inputStr => {
recordCursor();
// Update inputValue in case input can not parse as number
// Refresh ref value immediately since it may used by formatter
inputValueRef.current = inputStr;
setInternalInputValue(inputStr);
// Parse number
if (!compositionRef.current) {
const finalValue = mergedParser(inputStr);
const finalDecimal = getMiniDecimal(finalValue);
if (!finalDecimal.isNaN()) {
triggerValueUpdate(finalDecimal, true);
}
}
// Trigger onInput later to let user customize value if they want to handle something after onChange
onInput?.(inputStr);
// optimize for chinese input experience
// https://github.com/ant-design/ant-design/issues/8196
onNextPromise(() => {
let nextInputStr = inputStr;
if (!parser) {
nextInputStr = inputStr.replace(/。/g, '.');
}
if (nextInputStr !== inputStr) {
collectInputValue(nextInputStr);
}
});
};
// >>> Composition
const onCompositionStart = () => {
compositionRef.current = true;
};
const onCompositionEnd = () => {
compositionRef.current = false;
collectInputValue(inputRef.current.value);
};
// >>> Input
const onInternalInput = e => {
collectInputValue(e.target.value);
};
// ============================= Step =============================
const onInternalStep = useEvent((up, emitter) => {
// Ignore step since out of range
if (up && upDisabled || !up && downDisabled) {
return;
}
// Clear typing status since it may be caused by up & down key.
// We should sync with input value.
userTypingRef.current = false;
let stepDecimal = getMiniDecimal(shiftKeyRef.current ? getDecupleSteps(step) : step);
if (!up) {
stepDecimal = stepDecimal.negate();
}
const target = (decimalValue || getMiniDecimal(0)).add(stepDecimal.toString());
const updatedValue = triggerValueUpdate(target, false);
onStep?.(getDecimalValue(stringMode, updatedValue), {
offset: shiftKeyRef.current ? getDecupleSteps(step) : step,
type: up ? 'up' : 'down',
emitter
});
inputRef.current?.focus();
});
// ============================ Flush =============================
/**
* Flush current input content to trigger value change & re-formatter input if needed.
* This will always flush input value for update.
* If it's invalidate, will fallback to last validate value.
*/
const flushInputValue = userTyping => {
const parsedValue = getMiniDecimal(mergedParser(inputValue));
let formatValue;
if (!parsedValue.isNaN()) {
// Only validate value or empty value can be re-fill to inputValue
// Reassign the formatValue within ranged of trigger control
formatValue = triggerValueUpdate(parsedValue, userTyping);
} else {
formatValue = triggerValueUpdate(decimalValue, userTyping);
}
if (value !== undefined) {
// Reset back with controlled value first
setInputValue(decimalValue, false);
} else if (!formatValue.isNaN()) {
// Reset input back since no validate value
setInputValue(formatValue, false);
}
};
// Solve the issue of the event triggering sequence when entering numbers in chinese input (Safari)
const onBeforeInput = () => {
userTypingRef.current = true;
};
const onKeyDown = event => {
const {
key,
shiftKey
} = event;
userTypingRef.current = true;
shiftKeyRef.current = shiftKey;
if (key === 'Enter') {
if (!compositionRef.current) {
userTypingRef.current = false;
}
flushInputValue(false);
onPressEnter?.(event);
}
if (keyboard === false) {
return;
}
// Do step
if (!compositionRef.current && ['Up', 'ArrowUp', 'Down', 'ArrowDown'].includes(key)) {
onInternalStep(key === 'Up' || key === 'ArrowUp', 'keyboard');
event.preventDefault();
}
};
const onKeyUp = () => {
userTypingRef.current = false;
shiftKeyRef.current = false;
};
React.useEffect(() => {
if (changeOnWheel && focus) {
const onWheel = event => {
// moving mouse wheel rises wheel event with deltaY < 0
// scroll value grows from top to bottom, as screen Y coordinate
onInternalStep(event.deltaY < 0, 'wheel');
event.preventDefault();
};
const input = inputRef.current;
if (input) {
// React onWheel is passive and we can't preventDefault() in it.
// That's why we should subscribe with DOM listener
// https://stackoverflow.com/questions/63663025/react-onwheel-handler-cant-preventdefault-because-its-a-passive-event-listenev
input.addEventListener('wheel', onWheel, {
passive: false
});
return () => input.removeEventListener('wheel', onWheel);
}
}
});
// >>> Focus & Blur
const onBlur = () => {
if (changeOnBlur) {
flushInputValue(false);
}
setFocus(false);
userTypingRef.current = false;
};
// >>> Mouse events
const onInternalMouseDown = event => {
if (inputRef.current && event.target !== inputRef.current) {
inputRef.current.focus();
event.preventDefault();
}
onMouseDown?.(event);
};
// ========================== Controlled ==========================
// Input by precision & formatter
useLayoutUpdateEffect(() => {
if (!decimalValue.isInvalidate()) {
setInputValue(decimalValue, false);
}
}, [precision, formatter]);
// Input by value
useLayoutUpdateEffect(() => {
const newValue = getMiniDecimal(value);
setDecimalValue(newValue);
const currentParsedValue = getMiniDecimal(mergedParser(inputValue));
// When user typing from `1.2` to `1.`, we should not convert to `1` immediately.
// But let it go if user set `formatter`
if (!newValue.equals(currentParsedValue) || !userTypingRef.current || formatter) {
// Update value as effect
setInputValue(newValue, userTypingRef.current);
}
}, [value]);
// ============================ Cursor ============================
useLayoutUpdateEffect(() => {
if (formatter) {
restoreCursor();
}
}, [inputValue]);
// ============================ Render ============================
// >>>>>> Handler
const sharedHandlerProps = {
prefixCls,
onStep: onInternalStep,
className: classNames?.action,
style: styles?.action
};
const upNode = /*#__PURE__*/React.createElement(StepHandler, _extends({}, sharedHandlerProps, {
action: "up",
disabled: upDisabled
}), upHandler);
const downNode = /*#__PURE__*/React.createElement(StepHandler, _extends({}, sharedHandlerProps, {
action: "down",
disabled: downDisabled
}), downHandler);
// >>>>>> Render
return /*#__PURE__*/React.createElement("div", {
ref: rootRef,
className: clsx(prefixCls, `${prefixCls}-mode-${mode}`, className, classNames?.root, {
[`${prefixCls}-focused`]: focus,
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-readonly`]: readOnly,
[`${prefixCls}-not-a-number`]: decimalValue.isNaN(),
[`${prefixCls}-out-of-range`]: !decimalValue.isInvalidate() && !isInRange(decimalValue)
}),
style: {
...styles?.root,
...style
},
onMouseDown: onInternalMouseDown,
onMouseUp: onMouseUp,
onMouseLeave: onMouseLeave,
onMouseMove: onMouseMove,
onMouseEnter: onMouseEnter,
onMouseOut: onMouseOut,
onClick: onClick,
onFocus: () => {
setFocus(true);
},
onBlur: onBlur,
onKeyDown: onKeyDown,
onKeyUp: onKeyUp,
onCompositionStart: onCompositionStart,
onCompositionEnd: onCompositionEnd,
onBeforeInput: onBeforeInput
}, mode === 'spinner' && controls && downNode, prefix !== undefined && /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-prefix`, classNames?.prefix),
style: styles?.prefix
}, prefix), /*#__PURE__*/React.createElement("input", _extends({
autoComplete: "off",
role: "spinbutton",
"aria-valuemin": min,
"aria-valuemax": max,
"aria-valuenow": decimalValue.isInvalidate() ? null : decimalValue.toString(),
step: step,
ref: inputRef,
className: clsx(`${prefixCls}-input`, classNames?.input),
style: styles?.input,
value: inputValue,
onChange: onInternalInput,
disabled: disabled,
readOnly: readOnly
}, restProps)), suffix !== undefined && /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-suffix`, classNames?.suffix),
style: styles?.suffix
}, suffix), mode === 'spinner' && controls && upNode, mode === 'input' && controls && /*#__PURE__*/React.createElement("div", {
className: clsx(`${prefixCls}-actions`, classNames?.actions),
style: styles?.actions
}, upNode, downNode));
});
if (process.env.NODE_ENV !== 'production') {
InputNumber.displayName = 'InputNumber';
}
export default InputNumber;
@@ -0,0 +1,11 @@
import * as React from 'react';
export interface StepHandlerProps {
prefixCls: string;
action: 'up' | 'down';
children?: React.ReactNode;
disabled?: boolean;
className?: string;
style?: React.CSSProperties;
onStep: (up: boolean, emitter: 'handler' | 'keyboard' | 'wheel') => void;
}
export default function StepHandler({ prefixCls, action, children, disabled, className, style, onStep, }: StepHandlerProps): React.JSX.Element;
@@ -0,0 +1,84 @@
/* eslint-disable react/no-unknown-property */
import raf from "@rc-component/util/es/raf";
import { clsx } from 'clsx';
import * as React from 'react';
/**
* When click and hold on a button - the speed of auto changing the value.
*/
const STEP_INTERVAL = 200;
/**
* When click and hold on a button - the delay before auto changing the value.
*/
const STEP_DELAY = 600;
export default function StepHandler({
prefixCls,
action,
children,
disabled,
className,
style,
onStep
}) {
// ======================== MISC ========================
const isUpAction = action === 'up';
// ======================== Step ========================
const stepTimeoutRef = React.useRef();
const frameIds = React.useRef([]);
const onStopStep = () => {
clearTimeout(stepTimeoutRef.current);
};
// We will interval update step when hold mouse down
const onStepMouseDown = e => {
e.preventDefault();
onStopStep();
onStep(isUpAction, 'handler');
// Loop step for interval
function loopStep() {
onStep(isUpAction, 'handler');
stepTimeoutRef.current = setTimeout(loopStep, STEP_INTERVAL);
}
// First time press will wait some time to trigger loop step update
stepTimeoutRef.current = setTimeout(loopStep, STEP_DELAY);
};
React.useEffect(() => () => {
onStopStep();
frameIds.current.forEach(id => {
raf.cancel(id);
});
}, []);
// ======================= Render =======================
const actionClassName = `${prefixCls}-action`;
const mergedClassName = clsx(actionClassName, `${actionClassName}-${action}`, {
[`${actionClassName}-${action}-disabled`]: disabled
}, className);
// fix: https://github.com/ant-design/ant-design/issues/43088
// In Safari, When we fire onmousedown and onmouseup events in quick succession,
// there may be a problem that the onmouseup events are executed first,
// resulting in a disordered program execution.
// So, we need to use requestAnimationFrame to ensure that the onmouseup event is executed after the onmousedown event.
const safeOnStopStep = () => frameIds.current.push(raf(onStopStep));
return /*#__PURE__*/React.createElement("span", {
unselectable: "on",
role: "button",
onMouseUp: safeOnStopStep,
onMouseLeave: safeOnStopStep,
onMouseDown: e => {
onStepMouseDown(e);
},
"aria-label": isUpAction ? 'Increase Value' : 'Decrease Value',
"aria-disabled": disabled,
className: mergedClassName,
style: style
}, children || /*#__PURE__*/React.createElement("span", {
unselectable: "on",
className: `${prefixCls}-action-${action}-inner`
}));
}
@@ -0,0 +1,5 @@
/**
* Keep input cursor in the correct position if possible.
* Is this necessary since we have `formatter` which may mass the content?
*/
export default function useCursor(input: HTMLInputElement, focused: boolean): [() => void, () => void];
@@ -0,0 +1,68 @@
import { useRef } from 'react';
import warning from "@rc-component/util/es/warning";
/**
* Keep input cursor in the correct position if possible.
* Is this necessary since we have `formatter` which may mass the content?
*/
export default function useCursor(input, focused) {
const selectionRef = useRef(null);
function recordCursor() {
// Record position
try {
const {
selectionStart: start,
selectionEnd: end,
value
} = input;
const beforeTxt = value.substring(0, start);
const afterTxt = value.substring(end);
selectionRef.current = {
start,
end,
value,
beforeTxt,
afterTxt
};
} catch (e) {
// Fix error in Chrome:
// Failed to read the 'selectionStart' property from 'HTMLInputElement'
// http://stackoverflow.com/q/21177489/3040605
}
}
/**
* Restore logic:
* 1. back string same
* 2. start string same
*/
function restoreCursor() {
if (input && selectionRef.current && focused) {
try {
const {
value
} = input;
const {
beforeTxt,
afterTxt,
start
} = selectionRef.current;
let startPos = value.length;
if (value.startsWith(beforeTxt)) {
startPos = beforeTxt.length;
} else if (value.endsWith(afterTxt)) {
startPos = value.length - selectionRef.current.afterTxt.length;
} else {
const beforeLastChar = beforeTxt[start - 1];
const newIndex = value.indexOf(beforeLastChar, start - 1);
if (newIndex !== -1) {
startPos = newIndex + 1;
}
}
input.setSelectionRange(startPos, startPos);
} catch (e) {
warning(false, `Something warning of cursor restore. Please fire issue about this: ${e.message}`);
}
}
}
return [recordCursor, restoreCursor];
}
@@ -0,0 +1,5 @@
/**
* Always trigger latest once when call multiple time
*/
declare const _default: () => (callback: () => void) => void;
export default _default;
@@ -0,0 +1,19 @@
import { useRef, useEffect } from 'react';
import raf from "@rc-component/util/es/raf";
/**
* Always trigger latest once when call multiple time
*/
export default (() => {
const idRef = useRef(0);
const cleanUp = () => {
raf.cancel(idRef.current);
};
useEffect(() => cleanUp, []);
return callback => {
cleanUp();
idRef.current = raf(() => {
callback();
});
};
});
@@ -0,0 +1,4 @@
import type { InputNumberProps, ValueType, InputNumberRef } from './InputNumber';
import InputNumber from './InputNumber';
export type { InputNumberProps, ValueType, InputNumberRef };
export default InputNumber;
+2
View File
@@ -0,0 +1,2 @@
import InputNumber from "./InputNumber";
export default InputNumber;
@@ -0,0 +1 @@
export declare function getDecupleSteps(step: string | number): string;
@@ -0,0 +1,9 @@
import { trimNumber, num2str } from '@rc-component/mini-decimal';
export function getDecupleSteps(step) {
const stepStr = typeof step === 'number' ? num2str(step) : trimNumber(step).fullStr;
const hasPoint = stepStr.includes('.');
if (!hasPoint) {
return step + '0';
}
return trimNumber(stepStr.replace(/(\d)\.(\d)/g, '$1$2.')).fullStr;
}
@@ -0,0 +1,65 @@
import { ValueType } from '@rc-component/mini-decimal';
import * as React from 'react';
import { type InputFocusOptions } from '@rc-component/util/lib/Dom/focus';
export type { ValueType };
export interface InputNumberRef extends HTMLInputElement {
focus: (options?: InputFocusOptions) => void;
blur: () => void;
nativeElement: HTMLElement;
}
type SemanticName = 'root' | 'actions' | 'input' | 'action' | 'prefix' | 'suffix';
export interface InputNumberProps<T extends ValueType = ValueType> extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'defaultValue' | 'onInput' | 'onChange' | 'prefix' | 'suffix' | 'onMouseDown' | 'onClick' | 'onMouseUp' | 'onMouseLeave' | 'onMouseMove' | 'onMouseEnter' | 'onMouseOut'>, Pick<React.HTMLAttributes<HTMLDivElement>, 'onMouseDown' | 'onClick' | 'onMouseUp' | 'onMouseLeave' | 'onMouseMove' | 'onMouseEnter' | 'onMouseOut'> {
disabled?: boolean;
readOnly?: boolean;
/** value will show as string */
stringMode?: boolean;
mode?: 'input' | 'spinner';
defaultValue?: T;
value?: T | null;
prefixCls?: string;
className?: string;
style?: React.CSSProperties;
min?: T;
max?: T;
step?: ValueType;
tabIndex?: number;
controls?: boolean;
prefix?: React.ReactNode;
suffix?: React.ReactNode;
classNames?: Partial<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
upHandler?: React.ReactNode;
downHandler?: React.ReactNode;
keyboard?: boolean;
changeOnWheel?: boolean;
/** Parse display value to validate number */
parser?: (displayValue: string | undefined) => T;
/** Transform `value` to display value show in input */
formatter?: (value: T | undefined, info: {
userTyping: boolean;
input: string;
}) => string;
/** Syntactic sugar of `formatter`. Config precision of display. */
precision?: number;
/** Syntactic sugar of `formatter`. Config decimal separator of display. */
decimalSeparator?: string;
onInput?: (text: string) => void;
onChange?: (value: T | null) => void;
onPressEnter?: React.KeyboardEventHandler<HTMLInputElement>;
onStep?: (value: T, info: {
offset: ValueType;
type: 'up' | 'down';
emitter: 'handler' | 'keyboard' | 'wheel';
}) => void;
/**
* Trigger change onBlur event.
* If disabled, user must press enter or click handler to confirm the value update
*/
changeOnBlur?: boolean;
}
declare const InputNumber: (<T extends ValueType = ValueType>(props: React.PropsWithChildren<InputNumberProps<T>> & {
ref?: React.Ref<HTMLInputElement>;
}) => React.ReactElement) & {
displayName?: string;
};
export default InputNumber;
@@ -0,0 +1,564 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _miniDecimal = _interopRequireWildcard(require("@rc-component/mini-decimal"));
var _useLayoutEffect = require("@rc-component/util/lib/hooks/useLayoutEffect");
var _proxyObject = _interopRequireDefault(require("@rc-component/util/lib/proxyObject"));
var _clsx = require("clsx");
var React = _interopRequireWildcard(require("react"));
var _useCursor = _interopRequireDefault(require("./hooks/useCursor"));
var _StepHandler = _interopRequireDefault(require("./StepHandler"));
var _numberUtil = require("./utils/numberUtil");
var _util = require("@rc-component/util");
var _focus = require("@rc-component/util/lib/Dom/focus");
var _useFrame = _interopRequireDefault(require("./hooks/useFrame"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
/**
* We support `stringMode` which need handle correct type when user call in onChange
* format max or min value
* 1. if isInvalid return null
* 2. if precision is undefined, return decimal
* 3. format with precision
* I. if max > 0, round down with precision. Example: max= 3.5, precision=0 afterFormat: 3
* II. if max < 0, round up with precision. Example: max= -3.5, precision=0 afterFormat: -4
* III. if min > 0, round up with precision. Example: min= 3.5, precision=0 afterFormat: 4
* IV. if min < 0, round down with precision. Example: max= -3.5, precision=0 afterFormat: -3
*/
const getDecimalValue = (stringMode, decimalValue) => {
if (stringMode || decimalValue.isEmpty()) {
return decimalValue.toString();
}
return decimalValue.toNumber();
};
const getDecimalIfValidate = value => {
const decimal = (0, _miniDecimal.default)(value);
return decimal.isInvalidate() ? null : decimal;
};
const InputNumber = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
mode = 'input',
prefixCls = 'rc-input-number',
className,
style,
classNames,
styles,
min,
max,
step = 1,
defaultValue,
value,
disabled,
readOnly,
upHandler,
downHandler,
keyboard,
changeOnWheel = false,
controls = true,
prefix,
suffix,
stringMode,
parser,
formatter,
precision,
decimalSeparator,
onChange,
onInput,
onPressEnter,
onStep,
// Mouse Events
onMouseDown,
onClick,
onMouseUp,
onMouseLeave,
onMouseMove,
onMouseEnter,
onMouseOut,
changeOnBlur = true,
...restProps
} = props;
const [focus, setFocus] = React.useState(false);
const userTypingRef = React.useRef(false);
const compositionRef = React.useRef(false);
const shiftKeyRef = React.useRef(false);
// ============================= Refs =============================
const rootRef = React.useRef(null);
const inputRef = React.useRef(null);
React.useImperativeHandle(ref, () => (0, _proxyObject.default)(inputRef.current, {
focus: option => {
(0, _focus.triggerFocus)(inputRef.current, option);
},
blur: () => {
inputRef.current?.blur();
},
nativeElement: rootRef.current
}));
// ============================ Value =============================
// Real value control
const [decimalValue, setDecimalValue] = React.useState(() => (0, _miniDecimal.default)(value ?? defaultValue));
function setUncontrolledDecimalValue(newDecimal) {
if (value === undefined) {
setDecimalValue(newDecimal);
}
}
// ====================== Parser & Formatter ======================
/**
* `precision` is used for formatter & onChange.
* It will auto generate by `value` & `step`.
* But it will not block user typing.
*
* Note: Auto generate `precision` is used for legacy logic.
* We should remove this since we already support high precision with BigInt.
*
* @param number Provide which number should calculate precision
* @param userTyping Change by user typing
*/
const getPrecision = React.useCallback((numStr, userTyping) => {
if (userTyping) {
return undefined;
}
if (precision >= 0) {
return precision;
}
return Math.max((0, _miniDecimal.getNumberPrecision)(numStr), (0, _miniDecimal.getNumberPrecision)(step));
}, [precision, step]);
// >>> Parser
const mergedParser = React.useCallback(num => {
const numStr = String(num);
if (parser) {
return parser(numStr);
}
let parsedStr = numStr;
if (decimalSeparator) {
parsedStr = parsedStr.replace(decimalSeparator, '.');
}
// [Legacy] We still support auto convert `$ 123,456` to `123456`
return parsedStr.replace(/[^\w.-]+/g, '');
}, [parser, decimalSeparator]);
// >>> Formatter
const inputValueRef = React.useRef('');
const mergedFormatter = React.useCallback((number, userTyping) => {
if (formatter) {
return formatter(number, {
userTyping,
input: String(inputValueRef.current)
});
}
let str = typeof number === 'number' ? (0, _miniDecimal.num2str)(number) : number;
// User typing will not auto format with precision directly
if (!userTyping) {
const mergedPrecision = getPrecision(str, userTyping);
if ((0, _miniDecimal.validateNumber)(str) && (decimalSeparator || mergedPrecision >= 0)) {
// Separator
const separatorStr = decimalSeparator || '.';
str = (0, _miniDecimal.toFixed)(str, separatorStr, mergedPrecision);
}
}
return str;
}, [formatter, getPrecision, decimalSeparator]);
// ========================== InputValue ==========================
/**
* Input text value control
*
* User can not update input content directly. It updates with follow rules by priority:
* 1. controlled `value` changed
* * [SPECIAL] Typing like `1.` should not immediately convert to `1`
* 2. User typing with format (not precision)
* 3. Blur or Enter trigger revalidate
*/
const [inputValue, setInternalInputValue] = React.useState(() => {
const initValue = defaultValue ?? value;
if (decimalValue.isInvalidate() && ['string', 'number'].includes(typeof initValue)) {
return Number.isNaN(initValue) ? '' : initValue;
}
return mergedFormatter(decimalValue.toString(), false);
});
inputValueRef.current = inputValue;
// Should always be string
function setInputValue(newValue, userTyping) {
setInternalInputValue(mergedFormatter(
// Invalidate number is sometime passed by external control, we should let it go
// Otherwise is controlled by internal interactive logic which check by userTyping
// You can ref 'show limited value when input is not focused' test for more info.
newValue.isInvalidate() ? newValue.toString(false) : newValue.toString(!userTyping), userTyping));
}
// >>> Max & Min limit
const maxDecimal = React.useMemo(() => getDecimalIfValidate(max), [max, precision]);
const minDecimal = React.useMemo(() => getDecimalIfValidate(min), [min, precision]);
const upDisabled = React.useMemo(() => {
if (!maxDecimal || !decimalValue || decimalValue.isInvalidate()) {
return false;
}
return maxDecimal.lessEquals(decimalValue);
}, [maxDecimal, decimalValue]);
const downDisabled = React.useMemo(() => {
if (!minDecimal || !decimalValue || decimalValue.isInvalidate()) {
return false;
}
return decimalValue.lessEquals(minDecimal);
}, [minDecimal, decimalValue]);
// Cursor controller
const [recordCursor, restoreCursor] = (0, _useCursor.default)(inputRef.current, focus);
// ============================= Data =============================
/**
* Find target value closet within range.
* e.g. [11, 28]:
* 3 => 11
* 23 => 23
* 99 => 28
*/
const getRangeValue = target => {
// target > max
if (maxDecimal && !target.lessEquals(maxDecimal)) {
return maxDecimal;
}
// target < min
if (minDecimal && !minDecimal.lessEquals(target)) {
return minDecimal;
}
return null;
};
/**
* Check value is in [min, max] range
*/
const isInRange = target => !getRangeValue(target);
/**
* Trigger `onChange` if value validated and not equals of origin.
* Return the value that re-align in range.
*/
const triggerValueUpdate = (newValue, userTyping) => {
let updateValue = newValue;
let isRangeValidate = isInRange(updateValue) || updateValue.isEmpty();
// Skip align value when trigger value is empty.
// We just trigger onChange(null)
// This should not block user typing
if (!updateValue.isEmpty() && !userTyping) {
// Revert value in range if needed
updateValue = getRangeValue(updateValue) || updateValue;
isRangeValidate = true;
}
if (!readOnly && !disabled && isRangeValidate) {
const numStr = updateValue.toString();
const mergedPrecision = getPrecision(numStr, userTyping);
if (mergedPrecision >= 0) {
updateValue = (0, _miniDecimal.default)((0, _miniDecimal.toFixed)(numStr, '.', mergedPrecision));
// When to fixed. The value may out of min & max range.
// 4 in [0, 3.8] => 3.8 => 4 (toFixed)
if (!isInRange(updateValue)) {
updateValue = (0, _miniDecimal.default)((0, _miniDecimal.toFixed)(numStr, '.', mergedPrecision, true));
}
}
// Trigger event
if (!updateValue.equals(decimalValue)) {
setUncontrolledDecimalValue(updateValue);
onChange?.(updateValue.isEmpty() ? null : getDecimalValue(stringMode, updateValue));
// Reformat input if value is not controlled
if (value === undefined) {
setInputValue(updateValue, userTyping);
}
}
return updateValue;
}
return decimalValue;
};
// ========================== User Input ==========================
const onNextPromise = (0, _useFrame.default)();
// >>> Collect input value
const collectInputValue = inputStr => {
recordCursor();
// Update inputValue in case input can not parse as number
// Refresh ref value immediately since it may used by formatter
inputValueRef.current = inputStr;
setInternalInputValue(inputStr);
// Parse number
if (!compositionRef.current) {
const finalValue = mergedParser(inputStr);
const finalDecimal = (0, _miniDecimal.default)(finalValue);
if (!finalDecimal.isNaN()) {
triggerValueUpdate(finalDecimal, true);
}
}
// Trigger onInput later to let user customize value if they want to handle something after onChange
onInput?.(inputStr);
// optimize for chinese input experience
// https://github.com/ant-design/ant-design/issues/8196
onNextPromise(() => {
let nextInputStr = inputStr;
if (!parser) {
nextInputStr = inputStr.replace(/。/g, '.');
}
if (nextInputStr !== inputStr) {
collectInputValue(nextInputStr);
}
});
};
// >>> Composition
const onCompositionStart = () => {
compositionRef.current = true;
};
const onCompositionEnd = () => {
compositionRef.current = false;
collectInputValue(inputRef.current.value);
};
// >>> Input
const onInternalInput = e => {
collectInputValue(e.target.value);
};
// ============================= Step =============================
const onInternalStep = (0, _util.useEvent)((up, emitter) => {
// Ignore step since out of range
if (up && upDisabled || !up && downDisabled) {
return;
}
// Clear typing status since it may be caused by up & down key.
// We should sync with input value.
userTypingRef.current = false;
let stepDecimal = (0, _miniDecimal.default)(shiftKeyRef.current ? (0, _numberUtil.getDecupleSteps)(step) : step);
if (!up) {
stepDecimal = stepDecimal.negate();
}
const target = (decimalValue || (0, _miniDecimal.default)(0)).add(stepDecimal.toString());
const updatedValue = triggerValueUpdate(target, false);
onStep?.(getDecimalValue(stringMode, updatedValue), {
offset: shiftKeyRef.current ? (0, _numberUtil.getDecupleSteps)(step) : step,
type: up ? 'up' : 'down',
emitter
});
inputRef.current?.focus();
});
// ============================ Flush =============================
/**
* Flush current input content to trigger value change & re-formatter input if needed.
* This will always flush input value for update.
* If it's invalidate, will fallback to last validate value.
*/
const flushInputValue = userTyping => {
const parsedValue = (0, _miniDecimal.default)(mergedParser(inputValue));
let formatValue;
if (!parsedValue.isNaN()) {
// Only validate value or empty value can be re-fill to inputValue
// Reassign the formatValue within ranged of trigger control
formatValue = triggerValueUpdate(parsedValue, userTyping);
} else {
formatValue = triggerValueUpdate(decimalValue, userTyping);
}
if (value !== undefined) {
// Reset back with controlled value first
setInputValue(decimalValue, false);
} else if (!formatValue.isNaN()) {
// Reset input back since no validate value
setInputValue(formatValue, false);
}
};
// Solve the issue of the event triggering sequence when entering numbers in chinese input (Safari)
const onBeforeInput = () => {
userTypingRef.current = true;
};
const onKeyDown = event => {
const {
key,
shiftKey
} = event;
userTypingRef.current = true;
shiftKeyRef.current = shiftKey;
if (key === 'Enter') {
if (!compositionRef.current) {
userTypingRef.current = false;
}
flushInputValue(false);
onPressEnter?.(event);
}
if (keyboard === false) {
return;
}
// Do step
if (!compositionRef.current && ['Up', 'ArrowUp', 'Down', 'ArrowDown'].includes(key)) {
onInternalStep(key === 'Up' || key === 'ArrowUp', 'keyboard');
event.preventDefault();
}
};
const onKeyUp = () => {
userTypingRef.current = false;
shiftKeyRef.current = false;
};
React.useEffect(() => {
if (changeOnWheel && focus) {
const onWheel = event => {
// moving mouse wheel rises wheel event with deltaY < 0
// scroll value grows from top to bottom, as screen Y coordinate
onInternalStep(event.deltaY < 0, 'wheel');
event.preventDefault();
};
const input = inputRef.current;
if (input) {
// React onWheel is passive and we can't preventDefault() in it.
// That's why we should subscribe with DOM listener
// https://stackoverflow.com/questions/63663025/react-onwheel-handler-cant-preventdefault-because-its-a-passive-event-listenev
input.addEventListener('wheel', onWheel, {
passive: false
});
return () => input.removeEventListener('wheel', onWheel);
}
}
});
// >>> Focus & Blur
const onBlur = () => {
if (changeOnBlur) {
flushInputValue(false);
}
setFocus(false);
userTypingRef.current = false;
};
// >>> Mouse events
const onInternalMouseDown = event => {
if (inputRef.current && event.target !== inputRef.current) {
inputRef.current.focus();
event.preventDefault();
}
onMouseDown?.(event);
};
// ========================== Controlled ==========================
// Input by precision & formatter
(0, _useLayoutEffect.useLayoutUpdateEffect)(() => {
if (!decimalValue.isInvalidate()) {
setInputValue(decimalValue, false);
}
}, [precision, formatter]);
// Input by value
(0, _useLayoutEffect.useLayoutUpdateEffect)(() => {
const newValue = (0, _miniDecimal.default)(value);
setDecimalValue(newValue);
const currentParsedValue = (0, _miniDecimal.default)(mergedParser(inputValue));
// When user typing from `1.2` to `1.`, we should not convert to `1` immediately.
// But let it go if user set `formatter`
if (!newValue.equals(currentParsedValue) || !userTypingRef.current || formatter) {
// Update value as effect
setInputValue(newValue, userTypingRef.current);
}
}, [value]);
// ============================ Cursor ============================
(0, _useLayoutEffect.useLayoutUpdateEffect)(() => {
if (formatter) {
restoreCursor();
}
}, [inputValue]);
// ============================ Render ============================
// >>>>>> Handler
const sharedHandlerProps = {
prefixCls,
onStep: onInternalStep,
className: classNames?.action,
style: styles?.action
};
const upNode = /*#__PURE__*/React.createElement(_StepHandler.default, _extends({}, sharedHandlerProps, {
action: "up",
disabled: upDisabled
}), upHandler);
const downNode = /*#__PURE__*/React.createElement(_StepHandler.default, _extends({}, sharedHandlerProps, {
action: "down",
disabled: downDisabled
}), downHandler);
// >>>>>> Render
return /*#__PURE__*/React.createElement("div", {
ref: rootRef,
className: (0, _clsx.clsx)(prefixCls, `${prefixCls}-mode-${mode}`, className, classNames?.root, {
[`${prefixCls}-focused`]: focus,
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-readonly`]: readOnly,
[`${prefixCls}-not-a-number`]: decimalValue.isNaN(),
[`${prefixCls}-out-of-range`]: !decimalValue.isInvalidate() && !isInRange(decimalValue)
}),
style: {
...styles?.root,
...style
},
onMouseDown: onInternalMouseDown,
onMouseUp: onMouseUp,
onMouseLeave: onMouseLeave,
onMouseMove: onMouseMove,
onMouseEnter: onMouseEnter,
onMouseOut: onMouseOut,
onClick: onClick,
onFocus: () => {
setFocus(true);
},
onBlur: onBlur,
onKeyDown: onKeyDown,
onKeyUp: onKeyUp,
onCompositionStart: onCompositionStart,
onCompositionEnd: onCompositionEnd,
onBeforeInput: onBeforeInput
}, mode === 'spinner' && controls && downNode, prefix !== undefined && /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.clsx)(`${prefixCls}-prefix`, classNames?.prefix),
style: styles?.prefix
}, prefix), /*#__PURE__*/React.createElement("input", _extends({
autoComplete: "off",
role: "spinbutton",
"aria-valuemin": min,
"aria-valuemax": max,
"aria-valuenow": decimalValue.isInvalidate() ? null : decimalValue.toString(),
step: step,
ref: inputRef,
className: (0, _clsx.clsx)(`${prefixCls}-input`, classNames?.input),
style: styles?.input,
value: inputValue,
onChange: onInternalInput,
disabled: disabled,
readOnly: readOnly
}, restProps)), suffix !== undefined && /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.clsx)(`${prefixCls}-suffix`, classNames?.suffix),
style: styles?.suffix
}, suffix), mode === 'spinner' && controls && upNode, mode === 'input' && controls && /*#__PURE__*/React.createElement("div", {
className: (0, _clsx.clsx)(`${prefixCls}-actions`, classNames?.actions),
style: styles?.actions
}, upNode, downNode));
});
if (process.env.NODE_ENV !== 'production') {
InputNumber.displayName = 'InputNumber';
}
var _default = exports.default = InputNumber;
@@ -0,0 +1,11 @@
import * as React from 'react';
export interface StepHandlerProps {
prefixCls: string;
action: 'up' | 'down';
children?: React.ReactNode;
disabled?: boolean;
className?: string;
style?: React.CSSProperties;
onStep: (up: boolean, emitter: 'handler' | 'keyboard' | 'wheel') => void;
}
export default function StepHandler({ prefixCls, action, children, disabled, className, style, onStep, }: StepHandlerProps): React.JSX.Element;
@@ -0,0 +1,93 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = StepHandler;
var _raf = _interopRequireDefault(require("@rc-component/util/lib/raf"));
var _clsx = require("clsx");
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/* eslint-disable react/no-unknown-property */
/**
* When click and hold on a button - the speed of auto changing the value.
*/
const STEP_INTERVAL = 200;
/**
* When click and hold on a button - the delay before auto changing the value.
*/
const STEP_DELAY = 600;
function StepHandler({
prefixCls,
action,
children,
disabled,
className,
style,
onStep
}) {
// ======================== MISC ========================
const isUpAction = action === 'up';
// ======================== Step ========================
const stepTimeoutRef = React.useRef();
const frameIds = React.useRef([]);
const onStopStep = () => {
clearTimeout(stepTimeoutRef.current);
};
// We will interval update step when hold mouse down
const onStepMouseDown = e => {
e.preventDefault();
onStopStep();
onStep(isUpAction, 'handler');
// Loop step for interval
function loopStep() {
onStep(isUpAction, 'handler');
stepTimeoutRef.current = setTimeout(loopStep, STEP_INTERVAL);
}
// First time press will wait some time to trigger loop step update
stepTimeoutRef.current = setTimeout(loopStep, STEP_DELAY);
};
React.useEffect(() => () => {
onStopStep();
frameIds.current.forEach(id => {
_raf.default.cancel(id);
});
}, []);
// ======================= Render =======================
const actionClassName = `${prefixCls}-action`;
const mergedClassName = (0, _clsx.clsx)(actionClassName, `${actionClassName}-${action}`, {
[`${actionClassName}-${action}-disabled`]: disabled
}, className);
// fix: https://github.com/ant-design/ant-design/issues/43088
// In Safari, When we fire onmousedown and onmouseup events in quick succession,
// there may be a problem that the onmouseup events are executed first,
// resulting in a disordered program execution.
// So, we need to use requestAnimationFrame to ensure that the onmouseup event is executed after the onmousedown event.
const safeOnStopStep = () => frameIds.current.push((0, _raf.default)(onStopStep));
return /*#__PURE__*/React.createElement("span", {
unselectable: "on",
role: "button",
onMouseUp: safeOnStopStep,
onMouseLeave: safeOnStopStep,
onMouseDown: e => {
onStepMouseDown(e);
},
"aria-label": isUpAction ? 'Increase Value' : 'Decrease Value',
"aria-disabled": disabled,
className: mergedClassName,
style: style
}, children || /*#__PURE__*/React.createElement("span", {
unselectable: "on",
className: `${prefixCls}-action-${action}-inner`
}));
}
@@ -0,0 +1,5 @@
/**
* Keep input cursor in the correct position if possible.
* Is this necessary since we have `formatter` which may mass the content?
*/
export default function useCursor(input: HTMLInputElement, focused: boolean): [() => void, () => void];
@@ -0,0 +1,75 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useCursor;
var _react = require("react");
var _warning = _interopRequireDefault(require("@rc-component/util/lib/warning"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* Keep input cursor in the correct position if possible.
* Is this necessary since we have `formatter` which may mass the content?
*/
function useCursor(input, focused) {
const selectionRef = (0, _react.useRef)(null);
function recordCursor() {
// Record position
try {
const {
selectionStart: start,
selectionEnd: end,
value
} = input;
const beforeTxt = value.substring(0, start);
const afterTxt = value.substring(end);
selectionRef.current = {
start,
end,
value,
beforeTxt,
afterTxt
};
} catch (e) {
// Fix error in Chrome:
// Failed to read the 'selectionStart' property from 'HTMLInputElement'
// http://stackoverflow.com/q/21177489/3040605
}
}
/**
* Restore logic:
* 1. back string same
* 2. start string same
*/
function restoreCursor() {
if (input && selectionRef.current && focused) {
try {
const {
value
} = input;
const {
beforeTxt,
afterTxt,
start
} = selectionRef.current;
let startPos = value.length;
if (value.startsWith(beforeTxt)) {
startPos = beforeTxt.length;
} else if (value.endsWith(afterTxt)) {
startPos = value.length - selectionRef.current.afterTxt.length;
} else {
const beforeLastChar = beforeTxt[start - 1];
const newIndex = value.indexOf(beforeLastChar, start - 1);
if (newIndex !== -1) {
startPos = newIndex + 1;
}
}
input.setSelectionRange(startPos, startPos);
} catch (e) {
(0, _warning.default)(false, `Something warning of cursor restore. Please fire issue about this: ${e.message}`);
}
}
}
return [recordCursor, restoreCursor];
}
@@ -0,0 +1,5 @@
/**
* Always trigger latest once when call multiple time
*/
declare const _default: () => (callback: () => void) => void;
export default _default;
@@ -0,0 +1,26 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = require("react");
var _raf = _interopRequireDefault(require("@rc-component/util/lib/raf"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* Always trigger latest once when call multiple time
*/
var _default = () => {
const idRef = (0, _react.useRef)(0);
const cleanUp = () => {
_raf.default.cancel(idRef.current);
};
(0, _react.useEffect)(() => cleanUp, []);
return callback => {
cleanUp();
idRef.current = (0, _raf.default)(() => {
callback();
});
};
};
exports.default = _default;
@@ -0,0 +1,4 @@
import type { InputNumberProps, ValueType, InputNumberRef } from './InputNumber';
import InputNumber from './InputNumber';
export type { InputNumberProps, ValueType, InputNumberRef };
export default InputNumber;
+9
View File
@@ -0,0 +1,9 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _InputNumber = _interopRequireDefault(require("./InputNumber"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var _default = exports.default = _InputNumber.default;
@@ -0,0 +1 @@
export declare function getDecupleSteps(step: string | number): string;
@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getDecupleSteps = getDecupleSteps;
var _miniDecimal = require("@rc-component/mini-decimal");
function getDecupleSteps(step) {
const stepStr = typeof step === 'number' ? (0, _miniDecimal.num2str)(step) : (0, _miniDecimal.trimNumber)(step).fullStr;
const hasPoint = stepStr.includes('.');
if (!hasPoint) {
return step + '0';
}
return (0, _miniDecimal.trimNumber)(stepStr.replace(/(\d)\.(\d)/g, '$1$2.')).fullStr;
}
+84
View File
@@ -0,0 +1,84 @@
{
"name": "@rc-component/input-number",
"version": "1.6.2",
"description": "React input-number component",
"keywords": [
"react",
"react-component",
"react-input-number",
"input-number"
],
"homepage": "https://github.com/react-component/input-number",
"bugs": {
"url": "https://github.com/react-component/input-number/issues"
},
"repository": {
"type": "git",
"url": "git@github.com:react-component/input-number.git"
},
"license": "MIT",
"author": "tsjxyz@gmail.com",
"main": "./lib/index",
"module": "./es/index",
"types": "./es/index.d.ts",
"files": [
"lib",
"es",
"assets/*.css"
],
"scripts": {
"compile": "father build && lessc assets/index.less assets/index.css",
"coverage": "rc-test --coverage",
"docs:build": "dumi build",
"docs:deploy": "gh-pages -d docs-dist",
"lint": "eslint src/ --ext .ts,.tsx,.jsx,.js,.md",
"prepare": "husky install",
"prepublishOnly": "npm run compile && rc-np",
"prettier": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
"start": "dumi dev",
"test": "rc-test"
},
"lint-staged": {
"**/*.{js,jsx,tsx,ts,md,json}": [
"prettier --write",
"git add"
]
},
"dependencies": {
"@rc-component/mini-decimal": "^1.0.1",
"@rc-component/util": "^1.4.0",
"clsx": "^2.1.1"
},
"devDependencies": {
"@rc-component/father-plugin": "^2.0.2",
"@rc-component/np": "^1.0.3",
"@swc-node/jest": "^1.5.5",
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^16.0.0",
"@types/jest": "^29.2.4",
"@types/node": "^24.5.2",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@types/responselike": "^1.0.0",
"@umijs/fabric": "^4.0.1",
"@umijs/test": "^4.0.36",
"dumi": "^2.0.13",
"eslint": "^8.54.0",
"father": "^4.5.5",
"glob": "^11.0.0",
"husky": "^9.1.7",
"jest-environment-jsdom": "^29.3.1",
"less": "^4.1.3",
"lint-staged": "^15.1.0",
"rc-test": "^7.0.14",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"regenerator-runtime": "^0.14.1",
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
}