├── .dumirc.ts
├── .editorconfig
├── .eslintrc.js
├── .fatherrc.ts
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ ├── codeql.yml
│ └── react-component-ci.yml
├── .gitignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── assets
└── index.less
├── bunfig.toml
├── docs
├── api.md
├── demo
│ ├── combination-key-format.tsx
│ ├── custom.tsx
│ ├── debug.tsx
│ ├── decimal.tsx
│ ├── focus.tsx
│ ├── formatter.tsx
│ ├── input-control.tsx
│ ├── on-step.tsx
│ ├── precision.tsx
│ ├── simple.tsx
│ ├── small-step.tsx
│ └── wheel.tsx
├── example.md
└── index.md
├── index.js
├── jest.config.ts
├── now.json
├── package.json
├── src
├── InputNumber.tsx
├── SemanticContext.ts
├── StepHandler.tsx
├── hooks
│ ├── useCursor.ts
│ └── useFrame.ts
├── index.ts
└── utils
│ └── numberUtil.ts
├── tests
├── __snapshots__
│ └── baseInput.test.tsx.snap
├── baseInput.test.tsx
├── click.test.tsx
├── cursor.test.tsx
├── decimal.test.tsx
├── focus.test.tsx
├── formatter.test.tsx
├── github.test.tsx
├── input.test.tsx
├── keyboard.test.tsx
├── longPress.test.tsx
├── mobile.test.tsx
├── precision.test.tsx
├── props.test.tsx
├── semantic.test.tsx
├── setup.js
├── util
│ └── wrapper.ts
└── wheel.test.tsx
├── tsconfig.json
└── update-demo.js
/.dumirc.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'dumi';
2 |
3 | export default defineConfig({
4 | favicons: [
5 | 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4',
6 | ],
7 | themeConfig: {
8 | name: 'InputNumber',
9 | logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4'
10 | },
11 | outputPath: 'docs-dist',
12 | exportStatic: {},
13 | styles: [`body .dumi-default-header-left { width: 230px; } body .dumi-default-hero-title { font-size: 100px; }`],
14 | });
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
15 | [Makefile]
16 | indent_style = tab
17 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [require.resolve('@umijs/fabric/dist/eslint')],
3 | rules: {
4 | 'arrow-parens': 0,
5 | 'default-case': 0,
6 | 'react/no-array-index-key': 0,
7 | 'react/sort-comp': 0,
8 | 'react/no-access-state-in-setstate': 0,
9 | 'react/no-string-refs': 0,
10 | 'react/no-did-update-set-state': 0,
11 | 'react/no-find-dom-node': 0,
12 | '@typescript-eslint/no-explicit-any': 0,
13 | '@typescript-eslint/no-empty-interface': 0,
14 | '@typescript-eslint/no-inferrable-types': 0,
15 | '@typescript-eslint/consistent-type-imports': 0,
16 | 'react/require-default-props': 0,
17 | 'react-hooks/exhaustive-deps': 0,
18 | 'no-confusing-arrow': 0,
19 | 'no-restricted-globals': 0,
20 | 'import/no-named-as-default-member': 0,
21 | 'import/no-extraneous-dependencies': 0,
22 | 'jsx-a11y/label-has-for': 0,
23 | 'jsx-a11y/label-has-associated-control': 0,
24 | 'jsx-a11y/no-autofocus': 0,
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/.fatherrc.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'father';
2 |
3 | export default defineConfig({
4 | plugins: ['@rc-component/father-plugin'],
5 | });
6 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: ant-design # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: ant-design # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "21:00"
8 | open-pull-requests-limit: 10
9 | ignore:
10 | - dependency-name: "@types/react-dom"
11 | versions:
12 | - 17.0.0
13 | - 17.0.1
14 | - 17.0.2
15 | - dependency-name: "@types/react"
16 | versions:
17 | - 17.0.0
18 | - 17.0.1
19 | - 17.0.2
20 | - 17.0.3
21 | - dependency-name: less
22 | versions:
23 | - 4.1.0
24 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 | schedule:
9 | - cron: "19 0 * * 4"
10 |
11 | jobs:
12 | analyze:
13 | name: Analyze
14 | runs-on: ubuntu-latest
15 | permissions:
16 | actions: read
17 | contents: read
18 | security-events: write
19 |
20 | strategy:
21 | fail-fast: false
22 | matrix:
23 | language: [ javascript ]
24 |
25 | steps:
26 | - name: Checkout
27 | uses: actions/checkout@v3
28 |
29 | - name: Initialize CodeQL
30 | uses: github/codeql-action/init@v2
31 | with:
32 | languages: ${{ matrix.language }}
33 | queries: +security-and-quality
34 |
35 | - name: Autobuild
36 | uses: github/codeql-action/autobuild@v2
37 |
38 | - name: Perform CodeQL Analysis
39 | uses: github/codeql-action/analyze@v2
40 | with:
41 | category: "/language:${{ matrix.language }}"
42 |
--------------------------------------------------------------------------------
/.github/workflows/react-component-ci.yml:
--------------------------------------------------------------------------------
1 | name: ✅ test
2 | on: [push, pull_request]
3 | jobs:
4 | test:
5 | uses: react-component/rc-test/.github/workflows/test.yml@main
6 | secrets: inherit
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .storybook
2 | *.iml
3 | *.log
4 | .idea
5 | .ipr
6 | .iws
7 | *~
8 | ~*
9 | *.diff
10 | *.patch
11 | *.bak
12 | .DS_Store
13 | Thumbs.db
14 | .project
15 | .*proj
16 | .svn
17 | *.swp
18 | *.swo
19 | *.pyc
20 | *.pyo
21 | node_modules
22 | .cache
23 | *.css
24 | build
25 | lib
26 | es
27 | coverage
28 | yarn.lock
29 | pnpm-lock.yaml
30 | package-lock.json
31 | docs-dist/
32 |
33 | # umi
34 | .umi
35 | .umi-production
36 | .umi-test
37 | .env.local
38 |
39 | # dumi
40 | .dumi/tmp
41 | .dumi/tmp-production
42 |
43 | bun.lockb
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "proseWrap": "never",
5 | "printWidth": 100
6 | }
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | https://github.com/react-component/input-number/releases
4 |
5 | ## 5.0.0
6 |
7 | - Upgrade `rc-util` to `5.x`.
8 |
9 | ## 4.5.0
10 |
11 | - Fix React lifecycle warning.
12 | - Add `onPressEnter`.
13 |
14 | ## 4.4.0
15 |
16 | - `onChange` will return `null` instead `undefined` when it is empty.
17 |
18 | ## 4.0.0
19 |
20 | - Drop React Native support, please use https://github.com/react-component/m-input-number instead.
21 |
22 | ## 3.5.0
23 |
24 | - Added prop `precision`.
25 |
26 | ## 3.4.0
27 |
28 | - Added prop `parser`.
29 |
30 | ## 3.3.0
31 |
32 | - Added prop `formatter`.
33 | - Support changing radio using ctrl and shift.
34 |
35 | ## 3.2.0
36 |
37 | - Fixed touch events.
38 |
39 | ## 3.1.0
40 |
41 | - Added props `upHanlder` and `downHanlder`.
42 |
43 | ## 3.0.4
44 |
45 | - Fixed long press not working in Android. #42
46 |
47 | ## 3.0.3
48 |
49 | - Fixed https://github.com/ant-design/ant-design/issues/4757
50 |
51 | ## 3.0.2
52 |
53 | - Fixed `onKeyUp`.
54 |
55 | ## 3.0.1
56 |
57 | - Fixed invalid input like '11x'.
58 |
59 | ## 3.0.0
60 |
61 | - Trigger onChange when user input
62 | - support `keyboardType` prop for fixing crash on Android on textInput blur
63 |
64 | ## 2.8.0 / 2016-11-29
65 |
66 | - support tap state by rc-touchable
67 |
68 | ## 2.7.0 / 2016-09-03
69 |
70 | - support long press auto step
71 | - support `readOnly`
72 |
73 | ## 2.6.0 / 2016-07-20
74 |
75 | - support react-native
76 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014-present yiminghe
4 |
5 | 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:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | 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.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rc-input-number
2 |
3 | Input number control.
4 |
5 | [![NPM version][npm-image]][npm-url]
6 | [![npm download][download-image]][download-url]
7 | [![build status][github-actions-image]][github-actions-url]
8 | [![Codecov][codecov-image]][codecov-url]
9 | [![bundle size][bundlephobia-image]][bundlephobia-url]
10 | [![dumi][dumi-image]][dumi-url]
11 |
12 | [npm-image]: http://img.shields.io/npm/v/rc-input-number.svg?style=flat-square
13 | [npm-url]: http://npmjs.org/package/rc-input-number
14 | [travis-image]: https://img.shields.io/travis/react-component/input-number/master?style=flat-square
15 | [travis-url]: https://travis-ci.com/react-component/input-number
16 | [github-actions-image]: https://github.com/react-component/input-number/actions/workflows/react-component-ci.yml/badge.svg
17 | [github-actions-url]: https://github.com/react-component/input-number/actions/workflows/react-component-ci.yml
18 | [codecov-image]: https://img.shields.io/codecov/c/github/react-component/input-number/master.svg?style=flat-square
19 | [codecov-url]: https://app.codecov.io/gh/react-component/input-number
20 | [david-url]: https://david-dm.org/react-component/input-number
21 | [david-image]: https://david-dm.org/react-component/input-number/status.svg?style=flat-square
22 | [david-dev-url]: https://david-dm.org/react-component/input-number?type=dev
23 | [david-dev-image]: https://david-dm.org/react-component/input-number/dev-status.svg?style=flat-square
24 | [download-image]: https://img.shields.io/npm/dm/rc-input-number.svg?style=flat-square
25 | [download-url]: https://npmjs.org/package/rc-input-number
26 | [bundlephobia-url]: https://bundlephobia.com/package/rc-input-number
27 | [bundlephobia-image]: https://badgen.net/bundlephobia/minzip/rc-input-number
28 | [dumi-url]: https://github.com/umijs/dumi
29 | [dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square
30 |
31 | ## Screenshots
32 |
33 |
34 |
35 | ## Install
36 |
37 | [](https://npmjs.org/package/rc-input-number)
38 |
39 | ## Usage
40 |
41 | ```js
42 | import InputNumber from 'rc-input-number';
43 |
44 | export default () => ;
45 | ```
46 |
47 | ## Development
48 |
49 | ```
50 | npm install
51 | npm start
52 | ```
53 |
54 | ## Example
55 |
56 | http://127.0.0.1:8000/examples/
57 |
58 | online example: https://input-number.vercel.app/
59 |
60 | ## API
61 |
62 | ### props
63 |
64 |
65 |
66 |
67 | name
68 | type
69 | default
70 | description
71 |
72 |
73 |
74 |
75 | prefixCls
76 | string
77 | rc-input-number
78 | Specifies the class prefix
79 |
80 |
81 | min
82 | Number
83 |
84 | Specifies the minimum value
85 |
86 |
87 | onClick
88 |
89 |
90 |
91 |
92 |
93 | placeholder
94 | string
95 |
96 |
97 |
98 |
99 | max
100 | Number
101 |
102 | Specifies the maximum value
103 |
104 |
105 | step
106 | Number or String
107 | 1
108 | Specifies the legal number intervals
109 |
110 |
111 | precision
112 | Number
113 |
114 | Specifies the precision length of value
115 |
116 |
117 | disabled
118 | Boolean
119 | false
120 | Specifies that an InputNumber should be disabled
121 |
122 |
123 | required
124 | Boolean
125 | false
126 | Specifies that an InputNumber is required
127 |
128 |
129 | autoFocus
130 | Boolean
131 | false
132 | Specifies that an InputNumber should automatically get focus when the page loads
133 |
134 |
135 | readOnly
136 | Boolean
137 | false
138 | Specifies that an InputNumber is read only
139 |
140 |
141 | controls
142 | Boolean
143 | true
144 | Whether to enable the control buttons
145 |
146 |
147 | name
148 | String
149 |
150 | Specifies the name of an InputNumber
151 |
152 |
153 | id
154 | String
155 |
156 | Specifies the id of an InputNumber
157 |
158 |
159 | value
160 | Number
161 |
162 | Specifies the value of an InputNumber
163 |
164 |
165 | defaultValue
166 | Number
167 |
168 | Specifies the defaultValue of an InputNumber
169 |
170 |
171 | onChange
172 | Function
173 |
174 | Called when value of an InputNumber changed
175 |
176 |
177 | onBlur
178 | Function
179 |
180 | Called when user leaves an input field
181 |
182 |
183 | onPressEnter
184 | Function
185 |
186 | The callback function that is triggered when Enter key is pressed.
187 |
188 |
189 | onFocus
190 | Function
191 |
192 | Called when an element gets focus
193 |
194 |
195 | style
196 | Object
197 |
198 | root style. such as {width:100}
199 |
200 |
201 | upHandler
202 | React.Node
203 |
204 | custom the up step element
205 |
206 |
207 | downHandler
208 | React.Node
209 |
210 | custom the down step element
211 |
212 |
213 | formatter
214 | (value: number|string): displayValue: string
215 |
216 | Specifies the format of the value presented
217 |
218 |
219 | parser
220 | (displayValue: string) => value: number
221 | `input => input.replace(/[^\w\.-]*/g, '')`
222 | Specifies the value extracted from formatter
223 |
224 |
225 | pattern
226 | string
227 |
228 | 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
229 |
230 |
231 | decimalSeparator
232 | string
233 |
234 | Specifies the decimal separator
235 |
236 |
237 | inputMode
238 | string
239 |
240 | Specifies the inputmode of input
241 |
242 |
243 | wheel
244 | Boolean
245 | true
246 | Allows changing value with mouse wheel
247 |
248 |
249 |
250 |
251 | ## Keyboard Navigation
252 | * When you hit the ⬆ or ⬇ key, the input value will be increased or decreased by `step`
253 | * With the Shift key (Shift+⬆ , Shift+⬇ ), the input value will be changed by `10 * step`
254 | * With the Ctrl or ⌘ key (Ctrl+⬆ or ⌘+⬆ or Ctrl+⬇ or ⌘+⬇ ), the input value will be changed by `0.1 * step`
255 |
256 | ## Mouse Wheel
257 | * When you scroll up or down, the input value will be increased or decreased by `step`
258 | * Scrolling with the Shift key, the input value will be changed by `10 * step`
259 |
260 | ## Test Case
261 |
262 | ```
263 | npm test
264 | npm run chrome-test
265 | ```
266 |
267 | ## Coverage
268 |
269 | ```
270 | npm run coverage
271 | ```
272 |
273 | open coverage/ dir
274 |
275 | ## License
276 |
277 | rc-input-number is released under the MIT license.
278 |
--------------------------------------------------------------------------------
/assets/index.less:
--------------------------------------------------------------------------------
1 | @inputNumberPrefixCls: rc-input-number;
2 |
3 | .@{inputNumberPrefixCls} {
4 | display: inline-block;
5 | height: 26px;
6 | margin: 0;
7 | padding: 0;
8 | font-size: 12px;
9 | line-height: 26px;
10 | vertical-align: middle;
11 | border: 1px solid #d9d9d9;
12 | border-radius: 4px;
13 | transition: all 0.3s;
14 |
15 | &-focused {
16 | border-color: #1890ff;
17 | box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
18 | }
19 | &-out-of-range {
20 | input {
21 | color: red;
22 | }
23 | }
24 |
25 | &-handler {
26 | display: block;
27 | height: 12px;
28 | overflow: hidden;
29 | line-height: 12px;
30 | text-align: center;
31 | touch-action: none;
32 |
33 | &-active {
34 | background: #ddd;
35 | }
36 | }
37 |
38 | &-handler-up-inner,
39 | &-handler-down-inner {
40 | color: #666666;
41 | -webkit-user-select: none;
42 | user-select: none;
43 | }
44 |
45 | &:hover {
46 | border-color: #1890ff;
47 |
48 | .@{inputNumberPrefixCls}-handler-up,
49 | .@{inputNumberPrefixCls}-handler-wrap {
50 | border-color: #1890ff;
51 | }
52 | }
53 |
54 | &-disabled:hover {
55 | border-color: #d9d9d9;
56 |
57 | .@{inputNumberPrefixCls}-handler-up,
58 | .@{inputNumberPrefixCls}-handler-wrap {
59 | border-color: #d9d9d9;
60 | }
61 | }
62 |
63 | &-input-wrap {
64 | height: 100%;
65 | overflow: hidden;
66 | }
67 |
68 | &-input {
69 | width: 100%;
70 | height: 100%;
71 | padding: 0;
72 | color: #666666;
73 | line-height: 26px;
74 | text-align: center;
75 | border: 0;
76 | border-radius: 4px;
77 | outline: 0;
78 | transition: all 0.3s ease;
79 | transition: all 0.3s;
80 | -moz-appearance: textfield;
81 | }
82 |
83 | &-handler-wrap {
84 | float: right;
85 | width: 20px;
86 | height: 100%;
87 | border-left: 1px solid #d9d9d9;
88 | transition: all 0.3s;
89 | }
90 |
91 | &-handler-up {
92 | padding-top: 1px;
93 | border-bottom: 1px solid #d9d9d9;
94 | transition: all 0.3s;
95 |
96 | &-inner {
97 | &:after {
98 | content: '+';
99 | }
100 | }
101 | }
102 |
103 | &-handler-down {
104 | transition: all 0.3s;
105 |
106 | &-inner {
107 | &:after {
108 | content: '-';
109 | }
110 | }
111 | }
112 |
113 | .handler-disabled() {
114 | opacity: 0.3;
115 | &:hover {
116 | color: #999;
117 | border-color: #d9d9d9;
118 | }
119 | }
120 |
121 | &-handler-down-disabled,
122 | &-handler-up-disabled {
123 | .handler-disabled();
124 | }
125 |
126 | &-disabled {
127 | .@{inputNumberPrefixCls}-input {
128 | background-color: #f3f3f3;
129 | cursor: not-allowed;
130 | opacity: 0.72;
131 | }
132 | .@{inputNumberPrefixCls}-handler {
133 | .handler-disabled();
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/bunfig.toml:
--------------------------------------------------------------------------------
1 | [install]
2 | peer = false
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: API
3 | nav:
4 | order: 10
5 | title: API
6 | path: /api
7 | ---
8 |
9 |
10 |
11 |
12 | name
13 | type
14 | default
15 | description
16 |
17 |
18 |
19 |
20 | prefixCls
21 | string
22 | rc-input-number
23 | Specifies the class prefix
24 |
25 |
26 | min
27 | Number
28 |
29 | Specifies the minimum value
30 |
31 |
32 | onClick
33 |
34 |
35 |
36 |
37 |
38 | placeholder
39 | string
40 |
41 |
42 |
43 |
44 | max
45 | Number
46 |
47 | Specifies the maximum value
48 |
49 |
50 | step
51 | Number or String
52 | 1
53 | Specifies the legal number intervals
54 |
55 |
56 | precision
57 | Number
58 |
59 | Specifies the precision length of value
60 |
61 |
62 | disabled
63 | Boolean
64 | false
65 | Specifies that an InputNumber should be disabled
66 |
67 |
68 | required
69 | Boolean
70 | false
71 | Specifies that an InputNumber is required
72 |
73 |
74 | autoFocus
75 | Boolean
76 | false
77 | Specifies that an InputNumber should automatically get focus when the page loads
78 |
79 |
80 | readOnly
81 | Boolean
82 | false
83 | Specifies that an InputNumber is read only
84 |
85 |
86 | changeOnWheel
87 | Boolean
88 | false
89 | Specifies that the value is set using the mouse wheel
90 |
91 |
92 | controls
93 | Boolean
94 | true
95 | Whether to enable the control buttons
96 |
97 |
98 | name
99 | String
100 |
101 | Specifies the name of an InputNumber
102 |
103 |
104 | id
105 | String
106 |
107 | Specifies the id of an InputNumber
108 |
109 |
110 | value
111 | Number
112 |
113 | Specifies the value of an InputNumber
114 |
115 |
116 | defaultValue
117 | Number
118 |
119 | Specifies the defaultValue of an InputNumber
120 |
121 |
122 | onChange
123 | Function
124 |
125 | Called when value of an InputNumber changed
126 |
127 |
128 | onBlur
129 | Function
130 |
131 | Called when user leaves an input field
132 |
133 |
134 | onPressEnter
135 | Function
136 |
137 | The callback function that is triggered when Enter key is pressed.
138 |
139 |
140 | onFocus
141 | Function
142 |
143 | Called when an element gets focus
144 |
145 |
146 | onStep
147 | (value: T, info: { offset: ValueType; type: 'up' | 'down', emitter: 'handler' | 'keydown' | 'wheel' }) => void
148 |
149 | Called when the user clicks the arrows on the keyboard or interface and when the mouse wheel is spun.
150 |
151 |
152 | style
153 | Object
154 |
155 | root style. such as {width:100}
156 |
157 |
158 | upHandler
159 | React.Node
160 |
161 | custom the up step element
162 |
163 |
164 | downHandler
165 | React.Node
166 |
167 | custom the down step element
168 |
169 |
170 | formatter
171 | (value: number|string): displayValue: string
172 |
173 | Specifies the format of the value presented
174 |
175 |
176 | parser
177 | (displayValue: string) => value: number
178 | `input => input.replace(/[^\w\.-]*/g, '')`
179 | Specifies the value extracted from formatter
180 |
181 |
182 | pattern
183 | string
184 |
185 | 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
186 |
187 |
188 | decimalSeparator
189 | string
190 |
191 | Specifies the decimal separator
192 |
193 |
194 | inputMode
195 | string
196 |
197 | Specifies the inputmode of input
198 |
199 |
200 |
201 |
202 | ## inputRef
203 |
204 | ```tsx | pure
205 | import InputNumber, { InputNumberRef } from 'rc-input-number';
206 |
207 | const inputRef = useRef(null);
208 |
209 | useEffect(() => {
210 | inputRef.current.focus(); // the input will get focus
211 | inputRef.current.blur(); // the input will lose focus
212 | }, []);
213 | // ....
214 | ;
215 | ```
216 |
217 | | Property | Type | Description |
218 | | -------- | --------------------------------------- | --------------------------------- |
219 | | focus | `(options?: InputFocusOptions) => void` | The input get focus when called |
220 | | blur | `() => void` | The input loses focus when called |
221 |
--------------------------------------------------------------------------------
/docs/demo/combination-key-format.tsx:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | import React from 'react';
3 | import InputNumber from '@rc-component/input-number';
4 | import '../../assets/index.less';
5 |
6 | class Component extends React.Component {
7 | state = {
8 | disabled: false,
9 | readOnly: false,
10 | value: 50000,
11 | };
12 |
13 | onChange = (value) => {
14 | console.log('onChange:', value);
15 | this.setState({ value });
16 | };
17 |
18 | toggleDisabled = () => {
19 | this.setState({
20 | disabled: !this.state.disabled,
21 | });
22 | };
23 |
24 | toggleReadOnly = () => {
25 | this.setState({
26 | readOnly: !this.state.readOnly,
27 | });
28 | };
29 |
30 | numberWithCommas = (x) => {
31 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
32 | };
33 |
34 | format = (num) => {
35 | return `$ ${this.numberWithCommas(num)} boeing737`;
36 | };
37 |
38 | parser = (num: string) => {
39 | const cells = num.toString().split(' ');
40 | if (!cells[1]) {
41 | return num;
42 | }
43 |
44 | const parsed = cells[1].replace(/,*/g, '');
45 |
46 | return parsed;
47 | };
48 |
49 | render() {
50 | return (
51 |
52 |
53 | When number is validate in range, keep formatting.
54 | Else will flush when blur.
55 |
56 |
57 |
71 |
72 |
73 | toggle Disabled
74 |
75 |
76 | toggle readOnly
77 |
78 |
79 |
80 | );
81 | }
82 | }
83 |
84 | export default Component;
85 |
--------------------------------------------------------------------------------
/docs/demo/custom.tsx:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | import React from 'react';
3 | import InputNumber from '@rc-component/input-number';
4 | import '../../assets/index.less';
5 |
6 | class Component extends React.Component {
7 | state = {
8 | disabled: false,
9 | readOnly: false,
10 | value: 5,
11 | };
12 |
13 | onChange = value => {
14 | console.log('onChange:', value);
15 | this.setState({ value });
16 | };
17 |
18 | toggleDisabled = () => {
19 | this.setState({
20 | disabled: !this.state.disabled,
21 | });
22 | };
23 |
24 | toggleReadOnly = () => {
25 | this.setState({
26 | readOnly: !this.state.readOnly,
27 | });
28 | };
29 |
30 | render() {
31 | const upHandler = x
;
32 | const downHandler = V
;
33 | return (
34 |
35 |
47 |
48 | );
49 | }
50 | }
51 |
52 | export default Component;
53 |
--------------------------------------------------------------------------------
/docs/demo/debug.tsx:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | import React, { useEffect } from 'react';
3 | import InputNumber from '@rc-component/input-number';
4 | import '../../assets/index.less';
5 |
6 | export default () => {
7 | const [value, setValue] = React.useState(5);
8 |
9 | useEffect(() => {
10 | function keyDown(event: KeyboardEvent) {
11 | if ((event.ctrlKey === true || event.metaKey) && event.keyCode === 90) {
12 | setValue(3);
13 | }
14 | }
15 | document.addEventListener('keydown', keyDown);
16 |
17 | return () => document.removeEventListener('keydown', keyDown);
18 | }, []);
19 |
20 | return (
21 | <>
22 | {
25 | console.log('Change:', nextValue);
26 | setValue(nextValue);
27 | }}
28 | value={value}
29 | />
30 | {value}
31 | {
33 | setValue(99);
34 | }}
35 | >
36 | Change
37 |
38 | >
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/docs/demo/decimal.tsx:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | import React from 'react';
3 | import InputNumber from '@rc-component/input-number';
4 | import '../../assets/index.less';
5 |
6 | export default class Demo extends React.Component {
7 | state = {
8 | disabled: false,
9 | readOnly: false,
10 | value: 99,
11 | };
12 |
13 | onChange = v => {
14 | console.log('onChange:', v);
15 | this.setState({
16 | value: v,
17 | });
18 | };
19 |
20 | toggleDisabled = () => {
21 | this.setState({
22 | disabled: !this.state.disabled,
23 | });
24 | };
25 |
26 | toggleReadOnly = () => {
27 | this.setState({
28 | readOnly: !this.state.readOnly,
29 | });
30 | };
31 |
32 | render() {
33 | return (
34 |
35 |
Value Range is [-8, 10], initialValue is out of range.
36 |
47 |
48 |
49 | toggle Disabled
50 |
51 |
52 | toggle readOnly
53 |
54 |
55 |
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/docs/demo/focus.tsx:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | import InputNumber, { InputNumberRef } from '@rc-component/input-number';
3 | import React from 'react';
4 | import '../../assets/index.less';
5 |
6 | export default () => {
7 | const inputRef = React.useRef(null);
8 |
9 | return (
10 |
11 |
12 |
13 | inputRef.current?.focus({ cursor: 'start' })}>
14 | focus at start
15 |
16 | inputRef.current?.focus({ cursor: 'end' })}>
17 | focus at end
18 |
19 | inputRef.current?.focus({ cursor: 'all' })}>
20 | focus to select all
21 |
22 | inputRef.current?.focus({ preventScroll: true })}>
23 | focus prevent scroll
24 |
25 |
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/docs/demo/formatter.tsx:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | import React from 'react';
3 | import InputNumber from '@rc-component/input-number';
4 | import '../../assets/index.less';
5 |
6 | function getSum(str) {
7 | let total = 0;
8 | str.split('').forEach((c) => {
9 | const num = Number(c);
10 |
11 | if (!Number.isNaN(num)) {
12 | total += num;
13 | }
14 | });
15 |
16 | return total;
17 | }
18 |
19 | const CHINESE_NUMBERS = '零一二三四五六七八九';
20 |
21 | function chineseParser(text: string) {
22 | const parsed = [...text]
23 | .map((cell) => {
24 | const index = CHINESE_NUMBERS.indexOf(cell);
25 | if (index !== -1) {
26 | return index;
27 | }
28 |
29 | return cell;
30 | })
31 | .join('');
32 |
33 | if (Number.isNaN(Number(parsed))) {
34 | return text;
35 | }
36 |
37 | return parsed;
38 | }
39 |
40 | function chineseFormatter(value: string) {
41 | return [...value]
42 | .map((cell) => {
43 | const index = Number(cell);
44 | if (!Number.isNaN(index)) {
45 | return CHINESE_NUMBERS[index];
46 | }
47 |
48 | return cell;
49 | })
50 | .join('');
51 | }
52 |
53 | class App extends React.Component {
54 | state = {
55 | value: 1000,
56 | };
57 |
58 | render() {
59 | return (
60 |
61 |
`$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
65 | onChange={console.log}
66 | />
67 | `${value}%`}
71 | parser={(value) => value.replace('%', '')}
72 | onChange={console.log}
73 | />
74 | `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
78 | onChange={console.log}
79 | />
80 |
81 |
82 |
In Control
83 | {
87 | // console.log(value);
88 | this.setState({ value });
89 | }}
90 | formatter={(value, { userTyping, input }) => {
91 | if (userTyping) {
92 | return input;
93 | }
94 | return `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
95 | }}
96 | />
97 |
98 |
99 | aria-label="Controlled number input demonstrating a custom format"
100 | value={this.state.value}
101 | onChange={(value) => {
102 | console.log(value);
103 | this.setState({ value });
104 | }}
105 | parser={chineseParser}
106 | formatter={chineseFormatter}
107 | />
108 |
109 |
110 |
111 |
Strange Format
112 | `$ ${value} - ${getSum(value)}`}
116 | parser={(value) => (value.match(/^\$ ([\d.]*) .*$/) || [])[1]}
117 | onChange={console.log}
118 | />
119 |
120 |
121 | );
122 | }
123 | }
124 |
125 | export default App;
126 |
--------------------------------------------------------------------------------
/docs/demo/input-control.tsx:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | import React from 'react';
3 | import type { ValueType} from '@rc-component/input-number'
4 | import InputNumber from '@rc-component/input-number';
5 | import '../../assets/index.less';
6 |
7 | export default () => {
8 | const [value, setValue] = React.useState('aaa');
9 | const [lock, setLock] = React.useState(false);
10 |
11 | return (
12 |
13 |
14 | value={value}
15 | max={999}
16 | onChange={(newValue) => {
17 | console.log('Change:', newValue);
18 | }}
19 | onInput={(text) => {
20 | console.log('Input:', text);
21 | if (!lock) {
22 | setValue(text);
23 | }
24 | }}
25 | />
26 |
27 | setLock(!lock)}>Lock Value ({String(lock)})
28 | setValue('93')}>Change Value
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/docs/demo/on-step.tsx:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | import InputNumber from '@rc-component/input-number';
3 | import React, { useState } from 'react';
4 | import '../../assets/index.less';
5 |
6 | export default () => {
7 | const [emitter, setEmitter] = useState('interface buttons (up)');
8 | const [value, setValue] = React.useState(0);
9 |
10 | const onChange = (val: number) => {
11 | console.warn('onChange:', val, typeof val);
12 | setValue(val);
13 | };
14 |
15 | const onStep = (_: number, info: { offset: number; type: 'up' | 'down', emitter: 'handler' | 'keyboard' | 'wheel' }) => {
16 | if (info.emitter === 'handler') {
17 | setEmitter(`interface buttons (${info.type})`);
18 | }
19 |
20 | if (info.emitter === 'keyboard') {
21 | setEmitter(`keyboard (${info.type})`);
22 | }
23 |
24 | if (info.emitter === 'wheel') {
25 | setEmitter(`mouse wheel (${info.type})`);
26 | }
27 | };
28 |
29 | return (
30 |
31 |
onStep callback
32 |
42 |
43 |
Triggered by: {emitter}
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/docs/demo/precision.tsx:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | import React from 'react';
3 | import InputNumber from '@rc-component/input-number';
4 | import '../../assets/index.less';
5 |
6 | export default () => {
7 | const [value, setValue] = React.useState(null);
8 | const [precision, setPrecision] = React.useState('2');
9 | const [decimalSeparator, setDecimalSeparator] = React.useState(',');
10 |
11 | return (
12 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/docs/demo/simple.tsx:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | import InputNumber from '@rc-component/input-number';
3 | import React from 'react';
4 | import '../../assets/index.less';
5 |
6 | export default () => {
7 | const [disabled, setDisabled] = React.useState(false);
8 | const [readOnly, setReadOnly] = React.useState(false);
9 | const [keyboard, setKeyboard] = React.useState(true);
10 | const [wheel, setWheel] = React.useState(true);
11 | const [stringMode, setStringMode] = React.useState(false);
12 | const [value, setValue] = React.useState(93);
13 |
14 | const onChange = (val: number) => {
15 | console.warn('onChange:', val, typeof val);
16 | setValue(val);
17 | };
18 |
19 | return (
20 |
21 |
Controlled
22 |
35 |
36 | setDisabled(!disabled)}>
37 | toggle Disabled ({String(disabled)})
38 |
39 | setReadOnly(!readOnly)}>
40 | toggle readOnly ({String(readOnly)})
41 |
42 | setKeyboard(!keyboard)}>
43 | toggle keyboard ({String(keyboard)})
44 |
45 | setStringMode(!stringMode)}>
46 | toggle stringMode ({String(stringMode)})
47 |
48 | setWheel(!wheel)}>
49 | toggle wheel ({String(wheel)})
50 |
51 |
52 |
53 |
54 |
Uncontrolled
55 |
62 |
63 |
64 |
!changeOnBlur
65 |
73 |
74 | );
75 | };
76 |
--------------------------------------------------------------------------------
/docs/demo/small-step.tsx:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | import React from 'react';
3 | import InputNum from '@rc-component/input-number';
4 | import '../../assets/index.less';
5 |
6 | export default () => {
7 | const [stringMode, setStringMode] = React.useState(false);
8 | const [value, setValue] = React.useState(0.000000001);
9 |
10 | return (
11 |
12 | {
20 | console.log('onChange:', newValue);
21 | setValue(newValue);
22 | }}
23 | stringMode={stringMode}
24 | />
25 |
26 |
27 | {
31 | setStringMode(!stringMode);
32 | }}
33 | />
34 | String Mode
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/docs/demo/wheel.tsx:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | import InputNumber from '@rc-component/input-number';
3 | import React from 'react';
4 | import '../../assets/index.less';
5 |
6 | export default () => {
7 | return (
8 |
9 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/docs/example.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Example
3 | nav:
4 | title: Example
5 | path: /example
6 | ---
7 |
8 | ## simple
9 |
10 |
11 |
12 | ## combination-key-format
13 |
14 |
15 |
16 | ## custom
17 |
18 |
19 |
20 | ## debug
21 |
22 |
23 |
24 | ## decimal
25 |
26 |
27 |
28 | ## formatter
29 |
30 |
31 |
32 | ## input-control
33 |
34 |
35 |
36 | ## precision
37 |
38 |
39 |
40 | ## small-step
41 |
42 |
43 |
44 | ## on-step
45 |
46 |
47 |
48 | ## wheel
49 |
50 |
51 |
52 | ## focus
53 |
54 |
55 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | hero:
3 | title: rc-input-number
4 | description: React InputNumber Component
5 | ---
6 |
7 | ## Install
8 |
9 | ```sh
10 | # npm
11 | npm install --save rc-input-number
12 |
13 | # yarn
14 | yarn install rc-input-number
15 |
16 | # pnpm
17 | pnpm i rc-input-number
18 | ```
19 |
20 | ## Usage
21 |
22 | ```ts
23 | import InputNumber from 'rc-input-number';
24 |
25 | export default () => ;
26 | ```
27 |
28 | ## Development
29 |
30 | ```sh
31 | npm install
32 | npm start
33 | ```
34 |
35 | ### Keyboard Navigation
36 |
37 | - When you hit the ⬆ or ⬇ key, the input value will be increased or decreased by step
38 | - With the Shift key (Shift+⬆, Shift+⬇), the input value will be changed by 10 * step
39 | - With the Ctrl or ⌘ key (Ctrl+⬆ or ⌘+⬆ or Ctrl+⬇ or ⌘+⬇ ), the input value will be changed by 0.1 * step
40 |
41 | ## Test Case
42 |
43 | ```sh
44 | npm test
45 | ```
46 |
47 | ## Coverage
48 |
49 | ```sh
50 | npm run coverage
51 | ```
52 |
53 | ## License
54 |
55 | rc-input-number is released under the MIT license.
56 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | // export this package's api
2 | import InputNumber from './src/';
3 | export default InputNumber;
4 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import { createConfig, type Config } from '@umijs/test';
2 |
3 | const defaultConfig = createConfig({
4 | target: 'browser',
5 | jsTransformer: 'swc'
6 | });
7 |
8 | const config: Config.InitialOptions = {
9 | ...defaultConfig,
10 | setupFiles: [
11 | ...defaultConfig.setupFiles,
12 | './tests/setup.js'
13 | ]
14 | };
15 |
16 | export default config;
17 |
--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "name": "rc-input-number",
4 | "builds": [
5 | {
6 | "src": "package.json",
7 | "use": "@now/static-build",
8 | "config": { "distDir": "docs-dist" }
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@rc-component/input-number",
3 | "version": "1.2.0",
4 | "description": "React input-number component",
5 | "keywords": [
6 | "react",
7 | "react-component",
8 | "react-input-number",
9 | "input-number"
10 | ],
11 | "homepage": "https://github.com/react-component/input-number",
12 | "bugs": {
13 | "url": "https://github.com/react-component/input-number/issues"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git@github.com:react-component/input-number.git"
18 | },
19 | "license": "MIT",
20 | "author": "tsjxyz@gmail.com",
21 | "main": "./lib/index",
22 | "module": "./es/index",
23 | "types": "./es/index.d.ts",
24 | "files": [
25 | "lib",
26 | "es",
27 | "assets/*.css"
28 | ],
29 | "scripts": {
30 | "compile": "father build && lessc assets/index.less assets/index.css",
31 | "coverage": "rc-test --coverage",
32 | "docs:build": "dumi build",
33 | "docs:deploy": "gh-pages -d docs-dist",
34 | "lint": "eslint src/ --ext .ts,.tsx,.jsx,.js,.md",
35 | "now-build": "npm run docs:build",
36 | "prepare": "husky install",
37 | "prepublishOnly": "npm run compile && rc-np",
38 | "prettier": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
39 | "start": "dumi dev",
40 | "test": "rc-test"
41 | },
42 | "lint-staged": {
43 | "**/*.{js,jsx,tsx,ts,md,json}": [
44 | "prettier --write",
45 | "git add"
46 | ]
47 | },
48 | "dependencies": {
49 | "@rc-component/mini-decimal": "^1.0.1",
50 | "classnames": "^2.2.5",
51 | "@rc-component/input": "~1.0.0",
52 | "@rc-component/util": "^1.2.0"
53 | },
54 | "devDependencies": {
55 | "@rc-component/father-plugin": "^2.0.2",
56 | "@rc-component/np": "^1.0.3",
57 | "@swc-node/jest": "^1.5.5",
58 | "@testing-library/jest-dom": "^6.1.5",
59 | "@testing-library/react": "^16.0.0",
60 | "@types/classnames": "^2.2.9",
61 | "@types/jest": "^29.2.4",
62 | "@types/react": "^18.0.26",
63 | "@types/react-dom": "^18.0.9",
64 | "@types/responselike": "^1.0.0",
65 | "@umijs/fabric": "^4.0.1",
66 | "@umijs/test": "^4.0.36",
67 | "cross-env": "^7.0.3",
68 | "dumi": "^2.0.13",
69 | "eslint": "^8.54.0",
70 | "eslint-plugin-jest": "^28.10.0",
71 | "eslint-plugin-unicorn": "^56.0.0",
72 | "expect.js": "~0.3.1",
73 | "father": "^4.5.5",
74 | "glob": "^11.0.0",
75 | "husky": "^9.1.7",
76 | "jest-environment-jsdom": "^29.3.1",
77 | "less": "^4.1.3",
78 | "lint-staged": "^15.1.0",
79 | "np": "^10.0.5",
80 | "rc-test": "^7.0.14",
81 | "rc-tooltip": "^6.0.1",
82 | "react": "^18.2.0",
83 | "react-dom": "^18.2.0",
84 | "regenerator-runtime": "^0.14.1",
85 | "ts-node": "^10.9.1",
86 | "typescript": "^5.1.6"
87 | },
88 | "peerDependencies": {
89 | "react": ">=16.9.0",
90 | "react-dom": ">=16.9.0"
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/InputNumber.tsx:
--------------------------------------------------------------------------------
1 | import getMiniDecimal, {
2 | DecimalClass,
3 | getNumberPrecision,
4 | num2str,
5 | toFixed,
6 | validateNumber,
7 | ValueType,
8 | } from '@rc-component/mini-decimal';
9 | import clsx from 'classnames';
10 | import { BaseInput } from '@rc-component/input';
11 | import { useLayoutUpdateEffect } from '@rc-component/util/lib/hooks/useLayoutEffect';
12 | import proxyObject from '@rc-component/util/lib/proxyObject';
13 | import { composeRef } from '@rc-component/util/lib/ref';
14 | import * as React from 'react';
15 | import useCursor from './hooks/useCursor';
16 | import StepHandler from './StepHandler';
17 | import { getDecupleSteps } from './utils/numberUtil';
18 | import SemanticContext from './SemanticContext';
19 |
20 | import type { HolderRef } from '@rc-component/input/lib/BaseInput';
21 | import { BaseInputProps } from '@rc-component/input/lib/interface';
22 | import { InputFocusOptions, triggerFocus } from '@rc-component/input/lib/utils/commonUtils';
23 | import useFrame from './hooks/useFrame';
24 |
25 | export type { ValueType };
26 |
27 | export interface InputNumberRef extends HTMLInputElement {
28 | focus: (options?: InputFocusOptions) => void;
29 | blur: () => void;
30 | nativeElement: HTMLElement;
31 | }
32 |
33 | /**
34 | * We support `stringMode` which need handle correct type when user call in onChange
35 | * format max or min value
36 | * 1. if isInvalid return null
37 | * 2. if precision is undefined, return decimal
38 | * 3. format with precision
39 | * I. if max > 0, round down with precision. Example: max= 3.5, precision=0 afterFormat: 3
40 | * II. if max < 0, round up with precision. Example: max= -3.5, precision=0 afterFormat: -4
41 | * III. if min > 0, round up with precision. Example: min= 3.5, precision=0 afterFormat: 4
42 | * IV. if min < 0, round down with precision. Example: max= -3.5, precision=0 afterFormat: -3
43 | */
44 | const getDecimalValue = (stringMode: boolean, decimalValue: DecimalClass) => {
45 | if (stringMode || decimalValue.isEmpty()) {
46 | return decimalValue.toString();
47 | }
48 |
49 | return decimalValue.toNumber();
50 | };
51 |
52 | const getDecimalIfValidate = (value: ValueType) => {
53 | const decimal = getMiniDecimal(value);
54 | return decimal.isInvalidate() ? null : decimal;
55 | };
56 |
57 | type SemanticName = 'actions' | 'input';
58 | export interface InputNumberProps
59 | extends Omit<
60 | React.InputHTMLAttributes,
61 | 'value' | 'defaultValue' | 'onInput' | 'onChange' | 'prefix' | 'suffix'
62 | > {
63 | /** value will show as string */
64 | stringMode?: boolean;
65 |
66 | defaultValue?: T;
67 | value?: T | null;
68 |
69 | prefixCls?: string;
70 | className?: string;
71 | style?: React.CSSProperties;
72 | min?: T;
73 | max?: T;
74 | step?: ValueType;
75 | tabIndex?: number;
76 | controls?: boolean;
77 | prefix?: React.ReactNode;
78 | suffix?: React.ReactNode;
79 | addonBefore?: React.ReactNode;
80 | addonAfter?: React.ReactNode;
81 | classNames?: BaseInputProps['classNames'] & Partial>;
82 | styles?: BaseInputProps['styles'] & Partial>;
83 |
84 | // Customize handler node
85 | upHandler?: React.ReactNode;
86 | downHandler?: React.ReactNode;
87 | keyboard?: boolean;
88 | changeOnWheel?: boolean;
89 |
90 | /** Parse display value to validate number */
91 | parser?: (displayValue: string | undefined) => T;
92 | /** Transform `value` to display value show in input */
93 | formatter?: (value: T | undefined, info: { userTyping: boolean; input: string }) => string;
94 | /** Syntactic sugar of `formatter`. Config precision of display. */
95 | precision?: number;
96 | /** Syntactic sugar of `formatter`. Config decimal separator of display. */
97 | decimalSeparator?: string;
98 |
99 | onInput?: (text: string) => void;
100 | onChange?: (value: T | null) => void;
101 | onPressEnter?: React.KeyboardEventHandler;
102 |
103 | onStep?: (
104 | value: T,
105 | info: { offset: ValueType; type: 'up' | 'down'; emitter: 'handler' | 'keyboard' | 'wheel' },
106 | ) => void;
107 |
108 | /**
109 | * Trigger change onBlur event.
110 | * If disabled, user must press enter or click handler to confirm the value update
111 | */
112 | changeOnBlur?: boolean;
113 | }
114 |
115 | type InternalInputNumberProps = Omit & {
116 | domRef: React.Ref;
117 | };
118 |
119 | const InternalInputNumber = React.forwardRef(
120 | (props: InternalInputNumberProps, ref: React.Ref) => {
121 | const {
122 | prefixCls,
123 | className,
124 | style,
125 | min,
126 | max,
127 | step = 1,
128 | defaultValue,
129 | value,
130 | disabled,
131 | readOnly,
132 | upHandler,
133 | downHandler,
134 | keyboard,
135 | changeOnWheel = false,
136 | controls = true,
137 |
138 | stringMode,
139 |
140 | parser,
141 | formatter,
142 | precision,
143 | decimalSeparator,
144 |
145 | onChange,
146 | onInput,
147 | onPressEnter,
148 | onStep,
149 |
150 | changeOnBlur = true,
151 |
152 | domRef,
153 |
154 | ...inputProps
155 | } = props;
156 |
157 | const inputClassName = `${prefixCls}-input`;
158 |
159 | const inputRef = React.useRef(null);
160 |
161 | const [focus, setFocus] = React.useState(false);
162 |
163 | const userTypingRef = React.useRef(false);
164 | const compositionRef = React.useRef(false);
165 | const shiftKeyRef = React.useRef(false);
166 |
167 | // ============================ Value =============================
168 | // Real value control
169 | const [decimalValue, setDecimalValue] = React.useState(() =>
170 | getMiniDecimal(value ?? defaultValue),
171 | );
172 |
173 | function setUncontrolledDecimalValue(newDecimal: DecimalClass) {
174 | if (value === undefined) {
175 | setDecimalValue(newDecimal);
176 | }
177 | }
178 |
179 | // ====================== Parser & Formatter ======================
180 | /**
181 | * `precision` is used for formatter & onChange.
182 | * It will auto generate by `value` & `step`.
183 | * But it will not block user typing.
184 | *
185 | * Note: Auto generate `precision` is used for legacy logic.
186 | * We should remove this since we already support high precision with BigInt.
187 | *
188 | * @param number Provide which number should calculate precision
189 | * @param userTyping Change by user typing
190 | */
191 | const getPrecision = React.useCallback(
192 | (numStr: string, userTyping: boolean) => {
193 | if (userTyping) {
194 | return undefined;
195 | }
196 |
197 | if (precision >= 0) {
198 | return precision;
199 | }
200 |
201 | return Math.max(getNumberPrecision(numStr), getNumberPrecision(step));
202 | },
203 | [precision, step],
204 | );
205 |
206 | // >>> Parser
207 | const mergedParser = React.useCallback(
208 | (num: string | number) => {
209 | const numStr = String(num);
210 |
211 | if (parser) {
212 | return parser(numStr);
213 | }
214 |
215 | let parsedStr = numStr;
216 | if (decimalSeparator) {
217 | parsedStr = parsedStr.replace(decimalSeparator, '.');
218 | }
219 |
220 | // [Legacy] We still support auto convert `$ 123,456` to `123456`
221 | return parsedStr.replace(/[^\w.-]+/g, '');
222 | },
223 | [parser, decimalSeparator],
224 | );
225 |
226 | // >>> Formatter
227 | const inputValueRef = React.useRef('');
228 | const mergedFormatter = React.useCallback(
229 | (number: string, userTyping: boolean) => {
230 | if (formatter) {
231 | return formatter(number, { userTyping, input: String(inputValueRef.current) });
232 | }
233 |
234 | let str = typeof number === 'number' ? num2str(number) : number;
235 |
236 | // User typing will not auto format with precision directly
237 | if (!userTyping) {
238 | const mergedPrecision = getPrecision(str, userTyping);
239 |
240 | if (validateNumber(str) && (decimalSeparator || mergedPrecision >= 0)) {
241 | // Separator
242 | const separatorStr = decimalSeparator || '.';
243 |
244 | str = toFixed(str, separatorStr, mergedPrecision);
245 | }
246 | }
247 |
248 | return str;
249 | },
250 | [formatter, getPrecision, decimalSeparator],
251 | );
252 |
253 | // ========================== InputValue ==========================
254 | /**
255 | * Input text value control
256 | *
257 | * User can not update input content directly. It updates with follow rules by priority:
258 | * 1. controlled `value` changed
259 | * * [SPECIAL] Typing like `1.` should not immediately convert to `1`
260 | * 2. User typing with format (not precision)
261 | * 3. Blur or Enter trigger revalidate
262 | */
263 | const [inputValue, setInternalInputValue] = React.useState(() => {
264 | const initValue = defaultValue ?? value;
265 | if (decimalValue.isInvalidate() && ['string', 'number'].includes(typeof initValue)) {
266 | return Number.isNaN(initValue) ? '' : initValue;
267 | }
268 | return mergedFormatter(decimalValue.toString(), false);
269 | });
270 | inputValueRef.current = inputValue;
271 |
272 | // Should always be string
273 | function setInputValue(newValue: DecimalClass, userTyping: boolean) {
274 | setInternalInputValue(
275 | mergedFormatter(
276 | // Invalidate number is sometime passed by external control, we should let it go
277 | // Otherwise is controlled by internal interactive logic which check by userTyping
278 | // You can ref 'show limited value when input is not focused' test for more info.
279 | newValue.isInvalidate() ? newValue.toString(false) : newValue.toString(!userTyping),
280 | userTyping,
281 | ),
282 | );
283 | }
284 |
285 | // >>> Max & Min limit
286 | const maxDecimal = React.useMemo(() => getDecimalIfValidate(max), [max, precision]);
287 | const minDecimal = React.useMemo(() => getDecimalIfValidate(min), [min, precision]);
288 |
289 | const upDisabled = React.useMemo(() => {
290 | if (!maxDecimal || !decimalValue || decimalValue.isInvalidate()) {
291 | return false;
292 | }
293 |
294 | return maxDecimal.lessEquals(decimalValue);
295 | }, [maxDecimal, decimalValue]);
296 |
297 | const downDisabled = React.useMemo(() => {
298 | if (!minDecimal || !decimalValue || decimalValue.isInvalidate()) {
299 | return false;
300 | }
301 |
302 | return decimalValue.lessEquals(minDecimal);
303 | }, [minDecimal, decimalValue]);
304 |
305 | // Cursor controller
306 | const [recordCursor, restoreCursor] = useCursor(inputRef.current, focus);
307 |
308 | // ============================= Data =============================
309 | /**
310 | * Find target value closet within range.
311 | * e.g. [11, 28]:
312 | * 3 => 11
313 | * 23 => 23
314 | * 99 => 28
315 | */
316 | const getRangeValue = (target: DecimalClass) => {
317 | // target > max
318 | if (maxDecimal && !target.lessEquals(maxDecimal)) {
319 | return maxDecimal;
320 | }
321 |
322 | // target < min
323 | if (minDecimal && !minDecimal.lessEquals(target)) {
324 | return minDecimal;
325 | }
326 |
327 | return null;
328 | };
329 |
330 | /**
331 | * Check value is in [min, max] range
332 | */
333 | const isInRange = (target: DecimalClass) => !getRangeValue(target);
334 |
335 | /**
336 | * Trigger `onChange` if value validated and not equals of origin.
337 | * Return the value that re-align in range.
338 | */
339 | const triggerValueUpdate = (newValue: DecimalClass, userTyping: boolean): DecimalClass => {
340 | let updateValue = newValue;
341 |
342 | let isRangeValidate = isInRange(updateValue) || updateValue.isEmpty();
343 |
344 | // Skip align value when trigger value is empty.
345 | // We just trigger onChange(null)
346 | // This should not block user typing
347 | if (!updateValue.isEmpty() && !userTyping) {
348 | // Revert value in range if needed
349 | updateValue = getRangeValue(updateValue) || updateValue;
350 | isRangeValidate = true;
351 | }
352 |
353 | if (!readOnly && !disabled && isRangeValidate) {
354 | const numStr = updateValue.toString();
355 | const mergedPrecision = getPrecision(numStr, userTyping);
356 | if (mergedPrecision >= 0) {
357 | updateValue = getMiniDecimal(toFixed(numStr, '.', mergedPrecision));
358 |
359 | // When to fixed. The value may out of min & max range.
360 | // 4 in [0, 3.8] => 3.8 => 4 (toFixed)
361 | if (!isInRange(updateValue)) {
362 | updateValue = getMiniDecimal(toFixed(numStr, '.', mergedPrecision, true));
363 | }
364 | }
365 |
366 | // Trigger event
367 | if (!updateValue.equals(decimalValue)) {
368 | setUncontrolledDecimalValue(updateValue);
369 | onChange?.(updateValue.isEmpty() ? null : getDecimalValue(stringMode, updateValue));
370 |
371 | // Reformat input if value is not controlled
372 | if (value === undefined) {
373 | setInputValue(updateValue, userTyping);
374 | }
375 | }
376 |
377 | return updateValue;
378 | }
379 |
380 | return decimalValue;
381 | };
382 |
383 | // ========================== User Input ==========================
384 | const onNextPromise = useFrame();
385 |
386 | // >>> Collect input value
387 | const collectInputValue = (inputStr: string) => {
388 | recordCursor();
389 |
390 | // Update inputValue in case input can not parse as number
391 | // Refresh ref value immediately since it may used by formatter
392 | inputValueRef.current = inputStr;
393 | setInternalInputValue(inputStr);
394 |
395 | // Parse number
396 | if (!compositionRef.current) {
397 | const finalValue = mergedParser(inputStr);
398 | const finalDecimal = getMiniDecimal(finalValue);
399 | if (!finalDecimal.isNaN()) {
400 | triggerValueUpdate(finalDecimal, true);
401 | }
402 | }
403 |
404 | // Trigger onInput later to let user customize value if they want to handle something after onChange
405 | onInput?.(inputStr);
406 |
407 | // optimize for chinese input experience
408 | // https://github.com/ant-design/ant-design/issues/8196
409 | onNextPromise(() => {
410 | let nextInputStr = inputStr;
411 | if (!parser) {
412 | nextInputStr = inputStr.replace(/。/g, '.');
413 | }
414 |
415 | if (nextInputStr !== inputStr) {
416 | collectInputValue(nextInputStr);
417 | }
418 | });
419 | };
420 |
421 | // >>> Composition
422 | const onCompositionStart = () => {
423 | compositionRef.current = true;
424 | };
425 |
426 | const onCompositionEnd = () => {
427 | compositionRef.current = false;
428 |
429 | collectInputValue(inputRef.current.value);
430 | };
431 |
432 | // >>> Input
433 | const onInternalInput: React.ChangeEventHandler = (e) => {
434 | collectInputValue(e.target.value);
435 | };
436 |
437 | // ============================= Step =============================
438 | const onInternalStep = (up: boolean, emitter: 'handler' | 'keyboard' | 'wheel') => {
439 | // Ignore step since out of range
440 | if ((up && upDisabled) || (!up && downDisabled)) {
441 | return;
442 | }
443 |
444 | // Clear typing status since it may be caused by up & down key.
445 | // We should sync with input value.
446 | userTypingRef.current = false;
447 |
448 | let stepDecimal = getMiniDecimal(shiftKeyRef.current ? getDecupleSteps(step) : step);
449 | if (!up) {
450 | stepDecimal = stepDecimal.negate();
451 | }
452 |
453 | const target = (decimalValue || getMiniDecimal(0)).add(stepDecimal.toString());
454 |
455 | const updatedValue = triggerValueUpdate(target, false);
456 |
457 | onStep?.(getDecimalValue(stringMode, updatedValue), {
458 | offset: shiftKeyRef.current ? getDecupleSteps(step) : step,
459 | type: up ? 'up' : 'down',
460 | emitter,
461 | });
462 |
463 | inputRef.current?.focus();
464 | };
465 |
466 | // ============================ Flush =============================
467 | /**
468 | * Flush current input content to trigger value change & re-formatter input if needed.
469 | * This will always flush input value for update.
470 | * If it's invalidate, will fallback to last validate value.
471 | */
472 | const flushInputValue = (userTyping: boolean) => {
473 | const parsedValue = getMiniDecimal(mergedParser(inputValue));
474 | let formatValue: DecimalClass;
475 |
476 | if (!parsedValue.isNaN()) {
477 | // Only validate value or empty value can be re-fill to inputValue
478 | // Reassign the formatValue within ranged of trigger control
479 | formatValue = triggerValueUpdate(parsedValue, userTyping);
480 | } else {
481 | formatValue = triggerValueUpdate(decimalValue, userTyping);
482 | }
483 |
484 | if (value !== undefined) {
485 | // Reset back with controlled value first
486 | setInputValue(decimalValue, false);
487 | } else if (!formatValue.isNaN()) {
488 | // Reset input back since no validate value
489 | setInputValue(formatValue, false);
490 | }
491 | };
492 |
493 | // Solve the issue of the event triggering sequence when entering numbers in chinese input (Safari)
494 | const onBeforeInput = () => {
495 | userTypingRef.current = true;
496 | };
497 |
498 | const onKeyDown: React.KeyboardEventHandler = (event) => {
499 | const { key, shiftKey } = event;
500 | userTypingRef.current = true;
501 |
502 | shiftKeyRef.current = shiftKey;
503 |
504 | if (key === 'Enter') {
505 | if (!compositionRef.current) {
506 | userTypingRef.current = false;
507 | }
508 | flushInputValue(false);
509 | onPressEnter?.(event);
510 | }
511 |
512 | if (keyboard === false) {
513 | return;
514 | }
515 |
516 | // Do step
517 | if (!compositionRef.current && ['Up', 'ArrowUp', 'Down', 'ArrowDown'].includes(key)) {
518 | onInternalStep(key === 'Up' || key === 'ArrowUp', 'keyboard');
519 | event.preventDefault();
520 | }
521 | };
522 |
523 | const onKeyUp = () => {
524 | userTypingRef.current = false;
525 | shiftKeyRef.current = false;
526 | };
527 |
528 | React.useEffect(() => {
529 | if (changeOnWheel && focus) {
530 | const onWheel = (event) => {
531 | // moving mouse wheel rises wheel event with deltaY < 0
532 | // scroll value grows from top to bottom, as screen Y coordinate
533 | onInternalStep(event.deltaY < 0, 'wheel');
534 | event.preventDefault();
535 | };
536 | const input = inputRef.current;
537 | if (input) {
538 | // React onWheel is passive and we can't preventDefault() in it.
539 | // That's why we should subscribe with DOM listener
540 | // https://stackoverflow.com/questions/63663025/react-onwheel-handler-cant-preventdefault-because-its-a-passive-event-listenev
541 | input.addEventListener('wheel', onWheel, { passive: false });
542 | return () => input.removeEventListener('wheel', onWheel);
543 | }
544 | }
545 | });
546 |
547 | // >>> Focus & Blur
548 | const onBlur = () => {
549 | if (changeOnBlur) {
550 | flushInputValue(false);
551 | }
552 |
553 | setFocus(false);
554 |
555 | userTypingRef.current = false;
556 | };
557 |
558 | // ========================== Controlled ==========================
559 | // Input by precision & formatter
560 | useLayoutUpdateEffect(() => {
561 | if (!decimalValue.isInvalidate()) {
562 | setInputValue(decimalValue, false);
563 | }
564 | }, [precision, formatter]);
565 |
566 | // Input by value
567 | useLayoutUpdateEffect(() => {
568 | const newValue = getMiniDecimal(value);
569 | setDecimalValue(newValue);
570 |
571 | const currentParsedValue = getMiniDecimal(mergedParser(inputValue));
572 |
573 | // When user typing from `1.2` to `1.`, we should not convert to `1` immediately.
574 | // But let it go if user set `formatter`
575 | if (!newValue.equals(currentParsedValue) || !userTypingRef.current || formatter) {
576 | // Update value as effect
577 | setInputValue(newValue, userTypingRef.current);
578 | }
579 | }, [value]);
580 |
581 | // ============================ Cursor ============================
582 | useLayoutUpdateEffect(() => {
583 | if (formatter) {
584 | restoreCursor();
585 | }
586 | }, [inputValue]);
587 |
588 | // ============================ Render ============================
589 | return (
590 | {
601 | setFocus(true);
602 | }}
603 | onBlur={onBlur}
604 | onKeyDown={onKeyDown}
605 | onKeyUp={onKeyUp}
606 | onCompositionStart={onCompositionStart}
607 | onCompositionEnd={onCompositionEnd}
608 | onBeforeInput={onBeforeInput}
609 | >
610 | {controls && (
611 |
619 | )}
620 |
621 |
636 |
637 |
638 | );
639 | },
640 | );
641 |
642 | const InputNumber = React.forwardRef((props, ref) => {
643 | const {
644 | disabled,
645 | style,
646 | prefixCls = 'rc-input-number',
647 | value,
648 | prefix,
649 | suffix,
650 | addonBefore,
651 | addonAfter,
652 | className,
653 | classNames,
654 | styles,
655 | ...rest
656 | } = props;
657 |
658 | const holderRef = React.useRef(null);
659 | const inputNumberDomRef = React.useRef(null);
660 | const inputFocusRef = React.useRef(null);
661 |
662 | const focus = (option?: InputFocusOptions) => {
663 | if (inputFocusRef.current) {
664 | triggerFocus(inputFocusRef.current, option);
665 | }
666 | };
667 |
668 | React.useImperativeHandle(ref, () =>
669 | proxyObject(inputFocusRef.current, {
670 | focus,
671 | nativeElement: holderRef.current.nativeElement || inputNumberDomRef.current,
672 | }),
673 | );
674 | const memoizedValue = React.useMemo(() => ({ classNames, styles }), [classNames, styles]);
675 | return (
676 |
677 |
698 |
707 |
708 |
709 | );
710 | }) as ((
711 | props: React.PropsWithChildren> & {
712 | ref?: React.Ref;
713 | },
714 | ) => React.ReactElement) & { displayName?: string };
715 |
716 | if (process.env.NODE_ENV !== 'production') {
717 | InputNumber.displayName = 'InputNumber';
718 | }
719 |
720 | export default InputNumber;
721 |
--------------------------------------------------------------------------------
/src/SemanticContext.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { InputNumberProps } from './InputNumber';
3 |
4 | interface SemanticContextProps {
5 | classNames?: InputNumberProps['classNames'];
6 | styles?: InputNumberProps['styles'];
7 | }
8 |
9 | const SemanticContext = React.createContext(undefined);
10 |
11 | export default SemanticContext;
12 |
--------------------------------------------------------------------------------
/src/StepHandler.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-unknown-property */
2 | import * as React from 'react';
3 | import cls from 'classnames';
4 | import useMobile from '@rc-component/util/lib/hooks/useMobile';
5 | import raf from '@rc-component/util/lib/raf';
6 | import SemanticContext from './SemanticContext';
7 |
8 | /**
9 | * When click and hold on a button - the speed of auto changing the value.
10 | */
11 | const STEP_INTERVAL = 200;
12 |
13 | /**
14 | * When click and hold on a button - the delay before auto changing the value.
15 | */
16 | const STEP_DELAY = 600;
17 |
18 | export interface StepHandlerProps {
19 | prefixCls: string;
20 | upNode?: React.ReactNode;
21 | downNode?: React.ReactNode;
22 | upDisabled?: boolean;
23 | downDisabled?: boolean;
24 | onStep: (up: boolean, emitter: 'handler' | 'keyboard' | 'wheel') => void;
25 | }
26 | export default function StepHandler({
27 | prefixCls,
28 | upNode,
29 | downNode,
30 | upDisabled,
31 | downDisabled,
32 | onStep,
33 | }: StepHandlerProps) {
34 | // ======================== Step ========================
35 | const stepTimeoutRef = React.useRef();
36 | const frameIds = React.useRef([]);
37 |
38 | const onStepRef = React.useRef();
39 | onStepRef.current = onStep;
40 |
41 | const { classNames, styles } = React.useContext(SemanticContext) || {};
42 |
43 | const onStopStep = () => {
44 | clearTimeout(stepTimeoutRef.current);
45 | };
46 |
47 | // We will interval update step when hold mouse down
48 | const onStepMouseDown = (e: React.MouseEvent, up: boolean) => {
49 | e.preventDefault();
50 | onStopStep();
51 |
52 | onStepRef.current(up, 'handler');
53 |
54 | // Loop step for interval
55 | function loopStep() {
56 | onStepRef.current(up, 'handler');
57 |
58 | stepTimeoutRef.current = setTimeout(loopStep, STEP_INTERVAL);
59 | }
60 |
61 | // First time press will wait some time to trigger loop step update
62 | stepTimeoutRef.current = setTimeout(loopStep, STEP_DELAY);
63 | };
64 |
65 | React.useEffect(
66 | () => () => {
67 | onStopStep();
68 | frameIds.current.forEach((id) => raf.cancel(id));
69 | },
70 | [],
71 | );
72 |
73 | // ======================= Render =======================
74 | const isMobile = useMobile();
75 | if (isMobile) {
76 | return null;
77 | }
78 |
79 | const handlerClassName = `${prefixCls}-handler`;
80 |
81 | const upClassName = cls(handlerClassName, `${handlerClassName}-up`, {
82 | [`${handlerClassName}-up-disabled`]: upDisabled,
83 | });
84 | const downClassName = cls(handlerClassName, `${handlerClassName}-down`, {
85 | [`${handlerClassName}-down-disabled`]: downDisabled,
86 | });
87 |
88 | // fix: https://github.com/ant-design/ant-design/issues/43088
89 | // In Safari, When we fire onmousedown and onmouseup events in quick succession,
90 | // there may be a problem that the onmouseup events are executed first,
91 | // resulting in a disordered program execution.
92 | // So, we need to use requestAnimationFrame to ensure that the onmouseup event is executed after the onmousedown event.
93 | const safeOnStopStep = () => frameIds.current.push(raf(onStopStep));
94 |
95 | const sharedHandlerProps = {
96 | unselectable: 'on' as const,
97 | role: 'button',
98 | onMouseUp: safeOnStopStep,
99 | onMouseLeave: safeOnStopStep,
100 | };
101 |
102 | return (
103 |
104 | {
107 | onStepMouseDown(e, true);
108 | }}
109 | aria-label="Increase Value"
110 | aria-disabled={upDisabled}
111 | className={upClassName}
112 | >
113 | {upNode || }
114 |
115 | {
118 | onStepMouseDown(e, false);
119 | }}
120 | aria-label="Decrease Value"
121 | aria-disabled={downDisabled}
122 | className={downClassName}
123 | >
124 | {downNode || }
125 |
126 |
127 | );
128 | }
129 |
--------------------------------------------------------------------------------
/src/hooks/useCursor.ts:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react';
2 | import warning from '@rc-component/util/lib/warning';
3 | /**
4 | * Keep input cursor in the correct position if possible.
5 | * Is this necessary since we have `formatter` which may mass the content?
6 | */
7 | export default function useCursor(
8 | input: HTMLInputElement,
9 | focused: boolean,
10 | ): [() => void, () => void] {
11 | const selectionRef = useRef<{
12 | start?: number;
13 | end?: number;
14 | value?: string;
15 | beforeTxt?: string;
16 | afterTxt?: string;
17 | }>(null);
18 |
19 | function recordCursor() {
20 | // Record position
21 | try {
22 | const { selectionStart: start, selectionEnd: end, value } = input;
23 | const beforeTxt = value.substring(0, start);
24 | const afterTxt = value.substring(end);
25 |
26 | selectionRef.current = {
27 | start,
28 | end,
29 | value,
30 | beforeTxt,
31 | afterTxt,
32 | };
33 | } catch (e) {
34 | // Fix error in Chrome:
35 | // Failed to read the 'selectionStart' property from 'HTMLInputElement'
36 | // http://stackoverflow.com/q/21177489/3040605
37 | }
38 | }
39 |
40 | /**
41 | * Restore logic:
42 | * 1. back string same
43 | * 2. start string same
44 | */
45 | function restoreCursor() {
46 | if (input && selectionRef.current && focused) {
47 | try {
48 | const { value } = input;
49 | const { beforeTxt, afterTxt, start } = selectionRef.current;
50 |
51 | let startPos = value.length;
52 |
53 | if (value.startsWith(beforeTxt)) {
54 | startPos = beforeTxt.length;
55 | } else if (value.endsWith(afterTxt)) {
56 | startPos = value.length - selectionRef.current.afterTxt.length;
57 | } else {
58 | const beforeLastChar = beforeTxt[start - 1];
59 | const newIndex = value.indexOf(beforeLastChar, start - 1);
60 | if (newIndex !== -1) {
61 | startPos = newIndex + 1;
62 | }
63 | }
64 |
65 | input.setSelectionRange(startPos, startPos);
66 | } catch (e) {
67 | warning(
68 | false,
69 | `Something warning of cursor restore. Please fire issue about this: ${e.message}`,
70 | );
71 | }
72 | }
73 | }
74 |
75 | return [recordCursor, restoreCursor];
76 | }
77 |
--------------------------------------------------------------------------------
/src/hooks/useFrame.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect } from 'react';
2 | import raf from '@rc-component/util/lib/raf';
3 |
4 | /**
5 | * Always trigger latest once when call multiple time
6 | */
7 | export default () => {
8 | const idRef = useRef(0);
9 |
10 | const cleanUp = () => {
11 | raf.cancel(idRef.current);
12 | };
13 |
14 | useEffect(() => cleanUp, []);
15 |
16 | return (callback: () => void) => {
17 | cleanUp();
18 |
19 | idRef.current = raf(() => {
20 | callback();
21 | });
22 | };
23 | };
24 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { InputNumberProps, ValueType, InputNumberRef } from './InputNumber';
2 | import InputNumber from './InputNumber';
3 |
4 | export type { InputNumberProps, ValueType, InputNumberRef };
5 |
6 | export default InputNumber;
7 |
--------------------------------------------------------------------------------
/src/utils/numberUtil.ts:
--------------------------------------------------------------------------------
1 | import { trimNumber, num2str } from '@rc-component/mini-decimal';
2 |
3 | export function getDecupleSteps(step: string | number) {
4 | const stepStr = typeof step === 'number' ? num2str(step) : trimNumber(step).fullStr;
5 | const hasPoint = stepStr.includes('.');
6 | if (!hasPoint) {
7 | return step + '0';
8 | }
9 | return trimNumber(stepStr.replace(/(\d)\.(\d)/g, '$1$2.')).fullStr;
10 | }
11 |
--------------------------------------------------------------------------------
/tests/__snapshots__/baseInput.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`baseInput addon should render properly 1`] = `
4 |
126 | `;
127 |
128 | exports[`baseInput prefix should render properly 1`] = `
129 |
185 | `;
186 |
--------------------------------------------------------------------------------
/tests/baseInput.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import InputNumber from '../src';
3 |
4 | describe('baseInput', () => {
5 | it('prefix should render properly', () => {
6 | const prefix = Prefix ;
7 |
8 | const { container } = render( );
9 | expect(container).toMatchSnapshot();
10 | });
11 |
12 | it('addon should render properly', () => {
13 | const addonBefore = Addon Before ;
14 | const addonAfter = Addon After ;
15 |
16 | const { container } = render(
17 |
18 |
19 |
20 |
21 |
22 |
,
23 | );
24 | expect(container).toMatchSnapshot();
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/tests/click.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, fireEvent, act } from '@testing-library/react';
3 | import InputNumber, { InputNumberProps } from '../src';
4 | import KeyCode from '@rc-component/util/lib/KeyCode';
5 |
6 | jest.mock('@rc-component/mini-decimal/lib/supportUtil');
7 | const { supportBigInt } = require('@rc-component/mini-decimal/lib/supportUtil');
8 | // jest.mock('../src/utils/supportUtil');
9 | // const { supportBigInt } = require('../src/utils/supportUtil');
10 |
11 | describe('InputNumber.Click', () => {
12 | beforeEach(() => {
13 | supportBigInt.mockImplementation(() => true);
14 | });
15 |
16 | afterEach(() => {
17 | supportBigInt.mockRestore();
18 | });
19 |
20 | function testInputNumber(
21 | name: string,
22 | props: Partial,
23 | selector: string,
24 | changedValue: string | number,
25 | stepType: 'up' | 'down',
26 | emitter: 'handler' | 'keyboard' | 'wheel',
27 | ) {
28 | it(name, () => {
29 | const onChange = jest.fn();
30 | const onStep = jest.fn();
31 | const { container, unmount } = render(
32 | ,
33 | );
34 | fireEvent.focus(container.querySelector('input'));
35 | fireEvent.mouseDown(container.querySelector(selector));
36 | fireEvent.mouseUp(container.querySelector(selector));
37 | fireEvent.click(container.querySelector(selector));
38 | expect(onChange).toHaveBeenCalledTimes(1);
39 | expect(onChange).toHaveBeenCalledWith(changedValue);
40 | expect(onStep).toHaveBeenCalledWith(changedValue, { offset: 1, type: stepType, emitter });
41 | unmount();
42 | });
43 | }
44 |
45 | describe('basic work', () => {
46 | testInputNumber('up button', { defaultValue: 10 }, '.rc-input-number-handler-up', 11, 'up', 'handler');
47 |
48 | testInputNumber('down button', { value: 10 }, '.rc-input-number-handler-down', 9, 'down', 'handler');
49 | });
50 |
51 | describe('empty input', () => {
52 | testInputNumber('up button', {}, '.rc-input-number-handler-up', 1, 'up', 'handler');
53 |
54 | testInputNumber('down button', {}, '.rc-input-number-handler-down', -1, 'down', 'handler');
55 | });
56 |
57 | describe('empty with min & max', () => {
58 | testInputNumber('up button', { min: 6, max: 10 }, '.rc-input-number-handler-up', 6, 'up', 'handler');
59 |
60 | testInputNumber('down button', { min: 6, max: 10 }, '.rc-input-number-handler-down', 6, 'down', 'handler');
61 | });
62 |
63 | describe('null with min & max', () => {
64 | testInputNumber(
65 | 'up button',
66 | { value: null, min: 6, max: 10 },
67 | '.rc-input-number-handler-up',
68 | 6,
69 | 'up',
70 | 'handler',
71 | );
72 |
73 | testInputNumber(
74 | 'down button',
75 | { value: null, min: 6, max: 10 },
76 | '.rc-input-number-handler-down',
77 | 6,
78 | 'down',
79 | 'handler',
80 | );
81 | });
82 |
83 | describe('disabled', () => {
84 | it('none', () => {
85 | const { container } = render( );
86 | expect(container.querySelector('.rc-input-number-handler-up-disabled')).toBeFalsy();
87 | expect(container.querySelector('.rc-input-number-handler-down-disabled')).toBeFalsy();
88 | });
89 |
90 | it('min', () => {
91 | const { container } = render( );
92 | expect(container.querySelector('.rc-input-number-handler-down-disabled')).toBeTruthy();
93 | });
94 |
95 | it('max', () => {
96 | const { container } = render( );
97 | expect(container.querySelector('.rc-input-number-handler-up-disabled')).toBeTruthy();
98 | });
99 | });
100 |
101 | describe('safe integer', () => {
102 | it('back to max safe when BigInt not support', () => {
103 | supportBigInt.mockImplementation(() => false);
104 |
105 | const onChange = jest.fn();
106 | const { container } = render( );
107 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
108 | expect(onChange).toHaveBeenCalledWith(Number.MAX_SAFE_INTEGER);
109 |
110 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down'));
111 | expect(onChange).toHaveBeenCalledWith(Number.MAX_SAFE_INTEGER - 1);
112 |
113 | supportBigInt.mockRestore();
114 | });
115 |
116 | it('back to min safe when BigInt not support', () => {
117 | supportBigInt.mockImplementation(() => false);
118 |
119 | const onChange = jest.fn();
120 | const { container } = render( );
121 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down'));
122 | expect(onChange).toHaveBeenCalledWith(Number.MIN_SAFE_INTEGER);
123 |
124 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
125 | expect(onChange).toHaveBeenCalledWith(Number.MIN_SAFE_INTEGER + 1);
126 |
127 | supportBigInt.mockRestore();
128 | });
129 |
130 | it('no limit max safe when BigInt support', () => {
131 | const onChange = jest.fn();
132 | const { container } = render(
133 | ,
134 | );
135 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
136 | expect(onChange).toHaveBeenCalledWith('999999999999999983222785');
137 | });
138 |
139 | it('no limit min safe when BigInt support', () => {
140 | const onChange = jest.fn();
141 | const { container } = render(
142 | ,
143 | );
144 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down'));
145 | expect(onChange).toHaveBeenCalledWith('-10000000000000000905969665');
146 | });
147 | });
148 |
149 | it('focus input when click up/down button', async () => {
150 | jest.useFakeTimers();
151 |
152 | const onFocus = jest.fn();
153 | const onBlur = jest.fn();
154 | const { container } = render( );
155 |
156 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
157 | act(() => {
158 | jest.advanceTimersByTime(100);
159 | });
160 | expect(document.activeElement).toBe(container.querySelector('input'));
161 |
162 | // jsdom not trigger onFocus with `.focus()`, let's trigger it manually
163 | fireEvent.focus(document.querySelector('input'));
164 | expect(container.querySelector('.rc-input-number-focused')).toBeTruthy();
165 | expect(onFocus).toHaveBeenCalled();
166 |
167 | fireEvent.blur(container.querySelector('input'));
168 | expect(onBlur).toHaveBeenCalled();
169 | expect(container.querySelector('.rc-input-number-focused')).toBeFalsy();
170 |
171 | jest.useRealTimers();
172 | });
173 |
174 | it('click down button with pressing shift key', () => {
175 | const onChange = jest.fn();
176 | const onStep = jest.fn();
177 | const { container } = render(
178 | ,
179 | );
180 | fireEvent.keyDown(container.querySelector('input'), {
181 | shiftKey: true,
182 | which: KeyCode.DOWN,
183 | key: 'ArrowDown',
184 | code: 'ArrowDown',
185 | keyCode: KeyCode.DOWN,
186 | });
187 |
188 | expect(onChange).toHaveBeenCalledWith(1.1);
189 | expect(onStep).toHaveBeenCalledWith(1.1, { offset: '0.1', type: 'down', emitter: 'keyboard' });
190 | });
191 |
192 | it('click up button with pressing shift key', () => {
193 | const onChange = jest.fn();
194 | const onStep = jest.fn();
195 | const { container } = render(
196 | ,
197 | );
198 |
199 | fireEvent.keyDown(container.querySelector('input'), {
200 | shiftKey: true,
201 | which: KeyCode.UP,
202 | key: 'ArrowUp',
203 | code: 'ArrowUp',
204 | keyCode: KeyCode.UP,
205 | });
206 | expect(onChange).toHaveBeenCalledWith(1.3);
207 | expect(onStep).toHaveBeenCalledWith(1.3, { offset: '0.1', type: 'up', emitter: 'keyboard' });
208 | });
209 | });
210 |
--------------------------------------------------------------------------------
/tests/cursor.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import KeyCode from '@rc-component/util/lib/KeyCode';
3 | import { render, fireEvent } from './util/wrapper';
4 | import InputNumber from '../src';
5 |
6 | describe('InputNumber.Cursor', () => {
7 | function cursorInput(input: HTMLInputElement, pos?: number) {
8 |
9 | if (pos !== undefined) {
10 | input.setSelectionRange(pos, pos);
11 | }
12 | return input.selectionStart;
13 | }
14 |
15 | function changeOnPos(
16 | input: HTMLInputElement,
17 | changeValue: string,
18 | cursorPos: number,
19 | which?: number,
20 | key?: number|string,
21 | ) {
22 | fireEvent.focus(input)
23 | fireEvent.keyDown(input,{which,keyCode:which,key})
24 | fireEvent.change(input,{ target: { value: changeValue, selectionStart: 1 }})
25 | fireEvent.keyUp(input,{which,keyCode:which,key})
26 | }
27 |
28 | // https://github.com/react-component/input-number/issues/235
29 | // We use post update position that not record before keyDown.
30 | // Origin test suite:
31 | // https://github.com/react-component/input-number/blob/e72ee088bdc8a8df32383b8fc0de562574e8616c/tests/index.test.js#L1490
32 | it('DELETE (not backspace)', () => {
33 | const { container } = render( );
34 | const input = container.querySelector('input');
35 | changeOnPos(input, '12', 1, KeyCode.DELETE);
36 | expect(cursorInput(input)).toEqual(1);
37 | });
38 |
39 | // https://github.com/ant-design/ant-design/issues/28366
40 | // Origin test suite:
41 | // https://github.com/react-component/input-number/blob/e72ee088bdc8a8df32383b8fc0de562574e8616c/tests/index.test.js#L1584
42 | describe('pre-pend string', () => {
43 | it('quick typing', () => {
44 | // `$ ` => `9$ ` => `$ 9`
45 | const { container } = render( `$ ${val}`} />);
46 | const input = container.querySelector('input');
47 | fireEvent.focus(input)
48 | cursorInput(input, 0);
49 | changeOnPos(input, '9$ ', 1, KeyCode.NUM_ONE,'1');
50 | expect(cursorInput(input,3)).toEqual(3);
51 | });
52 |
53 | describe('[LEGACY]', () => {
54 | const setUpCursorTest = (initValue: string, prependValue: string) => {
55 | const Demo = () => {
56 | const [value, setValue] = React.useState(initValue);
57 |
58 | return (
59 |
60 | stringMode
61 | value={value}
62 | onChange={(newValue) => {
63 | setValue(newValue);
64 | }}
65 | formatter={(val) => `$ ${val}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
66 | parser={(val) => val.replace(/\$\s?|(,*)/g, '')}
67 | />
68 | );
69 | };
70 |
71 | const { container } = render( );
72 | const input = container.querySelector('input');
73 | fireEvent.focus(input)
74 | for (let i = 0; i < prependValue.length; i += 1) {
75 | fireEvent.keyDown(input,{which: KeyCode.ONE,keyCode: KeyCode.ONE})
76 | }
77 |
78 | const finalValue = prependValue + initValue;
79 | cursorInput(input, prependValue.length);
80 | fireEvent.change(input,{ target: { value: finalValue } });
81 |
82 | return input;
83 | };
84 |
85 | it('should fix caret position on case 1', () => {
86 | // '$ 1'
87 | const input = setUpCursorTest('', '1');
88 | expect(cursorInput(input,3)).toEqual(3);
89 | });
90 |
91 | it('should fix caret position on case 2', () => {
92 | // '$ 111'
93 | const input = setUpCursorTest('', '111');
94 | expect(cursorInput(input,5)).toEqual(5);
95 | });
96 |
97 | it('should fix caret position on case 3', () => {
98 | // '$ 111'
99 | const input = setUpCursorTest('1', '11');
100 | expect(cursorInput(input,4)).toEqual(4);
101 | });
102 |
103 | it('should fix caret position on case 4', () => {
104 | // '$ 123,456'
105 | const input = setUpCursorTest('456', '123');
106 | expect(cursorInput(input,6)).toEqual(6);
107 | });
108 | });
109 | });
110 |
111 | describe('append string', () => {
112 | it('position caret before appended characters', () => {
113 | const { container } = render( `${value}%`} parser={(value) => value.replace('%', '')} />);
114 | const input = container.querySelector('input');
115 | fireEvent.focus(input);
116 | fireEvent.change(input,{ target: { value: '5' } });
117 | expect(cursorInput(input)).toEqual(1);
118 | });
119 | });
120 | });
121 |
--------------------------------------------------------------------------------
/tests/decimal.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, fireEvent } from './util/wrapper';
3 | import InputNumber from '../src';
4 |
5 | describe('InputNumber.Decimal', () => {
6 | it('decimal value', () => {
7 | const { container } = render( );
8 | expect(container.querySelector('input').value).toEqual('2.1');
9 | });
10 |
11 | it('decimal defaultValue', () => {
12 | const { container } = render( );
13 | expect(container.querySelector('input').value).toEqual('2.1');
14 | });
15 |
16 | it('increase and decrease decimal InputNumber by integer step', () => {
17 | const { container } = render( );
18 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
19 | expect(container.querySelector('input').value).toEqual('3.1');
20 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down'));
21 | expect(container.querySelector('input').value).toEqual('2.1');
22 | });
23 |
24 | it('small value and step', () => {
25 | const Demo = () => {
26 | const [value, setValue] = React.useState(0.000000001);
27 |
28 | return (
29 | {
35 | setValue(newValue);
36 | }}
37 | />
38 | );
39 | };
40 |
41 | const { container } = render( );
42 | const input = container.querySelector('input');
43 | expect(input.value).toEqual('0.000000001');
44 |
45 | for (let i = 0; i < 10; i += 1) {
46 | // plus until change precision
47 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
48 | }
49 |
50 | fireEvent.blur(input);
51 | expect(input.value).toEqual('0.000000011');
52 | });
53 |
54 | it('small step with integer value', () => {
55 | const { container } = render( );
56 | expect(container.querySelector('input').value).toEqual('1.000000000');
57 | });
58 |
59 | it('small step with empty value', () => {
60 | const { container } = render( );
61 | expect(container.querySelector('input').value).toEqual('');
62 |
63 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
64 | expect(container.querySelector('input').value).toEqual('0.1');
65 | });
66 |
67 | it('custom decimal separator', () => {
68 | const onChange = jest.fn();
69 | const { container } = render( );
70 |
71 | const input = container.querySelector('input');
72 | fireEvent.focus(input);
73 | fireEvent.change(input, { target: { value: '1,1' } });
74 | fireEvent.blur(input);
75 |
76 | expect(container.querySelector('input').value).toEqual('1,1');
77 | expect(onChange).toHaveBeenCalledWith(1.1);
78 | });
79 |
80 | describe('precision', () => {
81 | it('decimal step should not display complete precision', () => {
82 | const { container } = render( );
83 | expect(container.querySelector('input').value).toEqual('2.10');
84 | });
85 |
86 | it('string step should display complete precision', () => {
87 | const { container } = render( );
88 | expect(container.querySelector('input').value).toEqual('2.100');
89 | });
90 |
91 | it('prop precision is specified', () => {
92 | const onChange = jest.fn();
93 | const { container } = render(
94 | ,
95 | );
96 | const input = container.querySelector('input');
97 | expect(input.value).toEqual('2.00');
98 |
99 | fireEvent.change(input, { target: { value: '3.456' } });
100 | fireEvent.blur(input);
101 | expect(onChange).toHaveBeenCalledWith(3.46);
102 | expect(container.querySelector('input').value).toEqual('3.46');
103 |
104 | onChange.mockReset();
105 | fireEvent.change(input, { target: { value: '3.465' } });
106 | fireEvent.blur(input);
107 | expect(onChange).toHaveBeenCalledWith(3.47);
108 | expect(container.querySelector('input').value).toEqual('3.47');
109 |
110 | onChange.mockReset();
111 | fireEvent.change(input, { target: { value: '3.455' } });
112 | fireEvent.blur(input);
113 | expect(onChange).toHaveBeenCalledWith(3.46);
114 | expect(container.querySelector('input').value).toEqual('3.46');
115 |
116 | onChange.mockReset();
117 | fireEvent.change(input, { target: { value: '1' } });
118 | fireEvent.blur(input);
119 | expect(onChange).toHaveBeenCalledWith(1);
120 | expect(container.querySelector('input').value).toEqual('1.00');
121 | });
122 |
123 | it('zero precision should work', () => {
124 | const onChange = jest.fn();
125 | const { container } = render( );
126 | const input = container.querySelector('input');
127 | fireEvent.change(input, { target: { value: '1.44' } });
128 | fireEvent.blur(input);
129 | expect(onChange).toHaveBeenCalledWith(1);
130 | expect(container.querySelector('input').value).toEqual('1');
131 | });
132 |
133 | it('should not trigger onChange when blur InputNumber with precision', () => {
134 | const onChange = jest.fn();
135 | const { container } = render(
136 | ,
137 | );
138 | const input = container.querySelector('input');
139 | fireEvent.focus(input);
140 | fireEvent.blur(input);
141 |
142 | expect(onChange).toHaveBeenCalledTimes(0);
143 | });
144 |
145 | it('uncontrolled precision should not format immediately', () => {
146 | const { container } = render( );
147 | const input = container.querySelector('input');
148 | fireEvent.focus(input);
149 | fireEvent.change(input, { target: { value: '3' } });
150 |
151 | expect(container.querySelector('input').value).toEqual('3');
152 | });
153 |
154 | it('should empty value after removing value', () => {
155 | const onChange = jest.fn();
156 | const { container } = render( );
157 | const input = container.querySelector('input');
158 | fireEvent.focus(input);
159 | fireEvent.change(input, { target: { value: '3' } });
160 | fireEvent.change(input, { target: { value: '' } });
161 |
162 | expect(container.querySelector('input').value).toEqual('');
163 |
164 | fireEvent.blur(input);
165 | expect(onChange).toHaveBeenCalledWith(null);
166 | expect(container.querySelector('input').value).toEqual('');
167 | });
168 |
169 | it('should trigger onChange when removing value', () => {
170 | const onChange = jest.fn();
171 | const { container, rerender } = render( );
172 | const input = container.querySelector('input');
173 | fireEvent.focus(input);
174 | fireEvent.change(input, { target: { value: '1' } });
175 | expect(container.querySelector('input').value).toEqual('1');
176 | expect(onChange).toHaveBeenCalledWith(1);
177 |
178 | fireEvent.change(input, { target: { value: '' } });
179 | expect(container.querySelector('input').value).toEqual('');
180 | expect(onChange).toHaveBeenCalledWith(null);
181 |
182 | rerender( );
183 | fireEvent.change(input, { target: { value: '2' } });
184 | expect(container.querySelector('input').value).toEqual('2');
185 | expect(onChange).toHaveBeenCalledWith(2);
186 |
187 | fireEvent.change(input, { target: { value: '' } });
188 | expect(container.querySelector('input').value).toEqual('');
189 | expect(onChange).toHaveBeenCalledWith(null);
190 | });
191 | });
192 | });
193 |
--------------------------------------------------------------------------------
/tests/focus.test.tsx:
--------------------------------------------------------------------------------
1 | import { fireEvent, render } from '@testing-library/react';
2 | import InputNumber, { InputNumberRef } from '../src';
3 | import { spyElementPrototypes } from '@rc-component/util/lib/test/domHook';
4 | import React from 'react';
5 |
6 | const getInputRef = () => {
7 | const ref = React.createRef();
8 | render( );
9 | return ref;
10 | };
11 |
12 | describe('InputNumber.Focus', () => {
13 | let inputSpy: ReturnType;
14 | let focus: ReturnType;
15 | let setSelectionRange: ReturnType;
16 |
17 | beforeEach(() => {
18 | focus = jest.fn();
19 | setSelectionRange = jest.fn();
20 | inputSpy = spyElementPrototypes(HTMLInputElement, {
21 | focus,
22 | setSelectionRange,
23 | });
24 | });
25 |
26 | afterEach(() => {
27 | inputSpy.mockRestore();
28 | });
29 |
30 | it('start', () => {
31 | const input = getInputRef();
32 | input.current?.focus({ cursor: 'start' });
33 |
34 | expect(focus).toHaveBeenCalled();
35 | expect(setSelectionRange).toHaveBeenCalledWith(expect.anything(), 0, 0);
36 | });
37 |
38 | it('end', () => {
39 | const input = getInputRef();
40 | input.current?.focus({ cursor: 'end' });
41 |
42 | expect(focus).toHaveBeenCalled();
43 | expect(setSelectionRange).toHaveBeenCalledWith(expect.anything(), 5, 5);
44 | });
45 |
46 | it('all', () => {
47 | const input = getInputRef();
48 | input.current?.focus({ cursor: 'all' });
49 |
50 | expect(focus).toHaveBeenCalled();
51 | expect(setSelectionRange).toHaveBeenCalledWith(expect.anything(), 0, 5);
52 | });
53 |
54 | it('disabled should reset focus', () => {
55 | const { container, rerender } = render( );
56 | const input = container.querySelector('input')!;
57 |
58 | fireEvent.focus(input);
59 | expect(container.querySelector('.rc-input-number-focused')).toBeTruthy();
60 |
61 | rerender( );
62 | fireEvent.blur(input);
63 |
64 | expect(container.querySelector('.rc-input-number-focused')).toBeFalsy();
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/tests/formatter.test.tsx:
--------------------------------------------------------------------------------
1 | import KeyCode from '@rc-component/util/lib/KeyCode';
2 | import React from 'react';
3 | import InputNumber from '../src';
4 | import { fireEvent, render } from './util/wrapper';
5 |
6 | describe('InputNumber.Formatter', () => {
7 | it('formatter on default', () => {
8 | const { container } = render(
9 | `$ ${num}`} />,
10 | );
11 | const input = container.querySelector('input');
12 | expect(input.value).toEqual('$ 5');
13 | });
14 |
15 | it('formatter on mousedown', () => {
16 | const { container } = render( `$ ${num}`} />);
17 | const input = container.querySelector('input');
18 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
19 | expect(input.value).toEqual('$ 6');
20 |
21 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down'));
22 | expect(input.value).toEqual('$ 5');
23 | });
24 |
25 | it('formatter on keydown', () => {
26 | const onChange = jest.fn();
27 | const { container } = render(
28 | `$ ${num} ¥`} />,
29 | );
30 |
31 | const input = container.querySelector('input');
32 | fireEvent.focus(input);
33 | fireEvent.keyDown(input, {
34 | which: KeyCode.UP,
35 | key: 'ArrowUp',
36 | code: 'ArrowUp',
37 | keyCode: KeyCode.UP,
38 | });
39 |
40 | expect(input.value).toEqual('$ 6 ¥');
41 | expect(onChange).toHaveBeenCalledWith(6);
42 |
43 | fireEvent.keyDown(input, {
44 | which: KeyCode.DOWN,
45 | key: 'ArrowDown',
46 | code: 'ArrowDown',
47 | keyCode: KeyCode.DOWN,
48 | });
49 | expect(input.value).toEqual('$ 5 ¥');
50 | expect(onChange).toHaveBeenCalledWith(5);
51 | });
52 |
53 | it('formatter on direct input', () => {
54 | const onChange = jest.fn();
55 | const { container } = render(
56 | `$ ${num}`} onChange={onChange} />,
57 | );
58 | const input = container.querySelector('input');
59 | fireEvent.focus(input);
60 |
61 | fireEvent.change(input, { target: { value: '100' } });
62 | expect(input.value).toEqual('$ 100');
63 | expect(onChange).toHaveBeenCalledWith(100);
64 | });
65 |
66 | it('formatter and parser', () => {
67 | const onChange = jest.fn();
68 | const { container } = render(
69 | `$ ${num} boeing 737`}
72 | parser={(num) => num.toString().split(' ')[1]}
73 | onChange={onChange}
74 | />,
75 | );
76 | const input = container.querySelector('input');
77 | fireEvent.focus(input);
78 | fireEvent.keyDown(input, {
79 | which: KeyCode.UP,
80 | key: 'ArrowUp',
81 | code: 'ArrowUp',
82 | keyCode: KeyCode.UP,
83 | });
84 | expect(input.value).toEqual('$ 6 boeing 737');
85 | expect(onChange).toHaveBeenLastCalledWith(6);
86 |
87 | fireEvent.keyDown(input, {
88 | which: KeyCode.DOWN,
89 | key: 'ArrowDown',
90 | code: 'ArrowDown',
91 | keyCode: KeyCode.DOWN,
92 | });
93 |
94 | expect(input.value).toEqual('$ 5 boeing 737');
95 | expect(onChange).toHaveBeenLastCalledWith(5);
96 |
97 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'), {
98 | which: KeyCode.DOWN,
99 | });
100 | expect(input.value).toEqual('$ 6 boeing 737');
101 | expect(onChange).toHaveBeenLastCalledWith(6);
102 | });
103 |
104 | it('control not block user input', async () => {
105 | let numValue;
106 | const Demo = () => {
107 | const [value, setValue] = React.useState(null);
108 |
109 | return (
110 |
111 | value={value}
112 | onChange={setValue}
113 | formatter={(num, info) => {
114 | if (info.userTyping) {
115 | return info.input;
116 | }
117 |
118 | return String(num);
119 | }}
120 | parser={(num) => {
121 | numValue = num;
122 | return Number(num);
123 | }}
124 | />
125 | );
126 | };
127 |
128 | const { container } = render( );
129 | const input = container.querySelector('input');
130 | fireEvent.focus(input);
131 | fireEvent.change(input, { target: { value: '-' } });
132 | fireEvent.change(input, { target: { value: '-0' } });
133 |
134 | expect(numValue).toEqual('-0');
135 |
136 | fireEvent.blur(input);
137 | expect(input.value).toEqual('0');
138 | });
139 |
140 | it('in strictMode render correct defaultValue ', () => {
141 | const Demo = () => {
142 | return (
143 |
144 |
145 | `$ ${num}`} />
146 |
147 |
148 | );
149 | };
150 | const { container } = render( );
151 | const input = container.querySelector('input');
152 | expect(input.value).toEqual('$ 5');
153 |
154 | fireEvent.change(input, { target: { value: 3 } });
155 | expect(input.value).toEqual('$ 3');
156 | });
157 |
158 | it('formatter info should be correct', () => {
159 | const formatter = jest.fn();
160 | const { container } = render( );
161 |
162 | formatter.mockReset();
163 |
164 | fireEvent.change(container.querySelector('input'), { target: { value: '1' } });
165 | expect(formatter).toHaveBeenCalledTimes(1);
166 | expect(formatter).toHaveBeenCalledWith('1', { userTyping: true, input: '1' });
167 | });
168 |
169 | describe('dynamic formatter', () => {
170 | it('uncontrolled', () => {
171 | const { container, rerender } = render(
172 | `$${val}`} />,
173 | );
174 |
175 | expect(container.querySelector('.rc-input-number-input').value).toEqual(
176 | '$93',
177 | );
178 |
179 | rerender( `*${val}`} />);
180 | expect(container.querySelector('.rc-input-number-input').value).toEqual(
181 | '*93',
182 | );
183 | });
184 |
185 | it('controlled', () => {
186 | const { container, rerender } = render(
187 | `$${val}`} />,
188 | );
189 |
190 | expect(container.querySelector('.rc-input-number-input').value).toEqual(
191 | '$510',
192 | );
193 |
194 | rerender( `*${val}`} />);
195 | expect(container.querySelector('.rc-input-number-input').value).toEqual(
196 | '*510',
197 | );
198 | });
199 | });
200 | });
201 |
--------------------------------------------------------------------------------
/tests/github.test.tsx:
--------------------------------------------------------------------------------
1 | import KeyCode from '@rc-component/util/lib/KeyCode';
2 | import React from 'react';
3 | import InputNumber from '../src';
4 | import { act, fireEvent, render, screen, waitFor } from './util/wrapper';
5 |
6 | // Github issues
7 | describe('InputNumber.Github', () => {
8 | beforeEach(() => {
9 | jest.useFakeTimers();
10 | });
11 |
12 | afterEach(() => {
13 | jest.clearAllTimers();
14 | jest.useRealTimers();
15 | });
16 |
17 | // https://github.com/react-component/input-number/issues/32
18 | it('issue 32', () => {
19 | const { container } = render( );
20 | const input = container.querySelector('input');
21 | fireEvent.focus(input);
22 | fireEvent.change(input, { target: { value: '2' } });
23 | expect(input.value).toEqual('2');
24 |
25 | fireEvent.blur(input);
26 | expect(input.value).toEqual('2.0');
27 | });
28 |
29 | // https://github.com/react-component/input-number/issues/197
30 | it('issue 197', () => {
31 | const Demo = () => {
32 | const [value, setValue] = React.useState(NaN);
33 |
34 | return (
35 | {
39 | setValue(newValue);
40 | }}
41 | />
42 | );
43 | };
44 | const { container } = render( );
45 | const input = container.querySelector('input');
46 | fireEvent.focus(input);
47 | fireEvent.change(input, { target: { value: 'foo' } });
48 | });
49 |
50 | // https://github.com/react-component/input-number/issues/222
51 | it('issue 222', () => {
52 | const Demo = () => {
53 | const [value, setValue] = React.useState(1);
54 |
55 | return (
56 | {
61 | setValue(newValue);
62 | }}
63 | />
64 | );
65 | };
66 | const { container } = render( );
67 | const input = container.querySelector('input');
68 | fireEvent.focus(input);
69 |
70 | fireEvent.change(input, { target: { value: 'foo' } });
71 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
72 |
73 | expect(input.value).toEqual('2');
74 | });
75 |
76 | // https://github.com/react-component/input-number/issues/35
77 | it('issue 35', () => {
78 | let num: string | number;
79 |
80 | const { container } = render(
81 | {
85 | num = value;
86 | }}
87 | />,
88 | );
89 |
90 | for (let i = 1; i <= 400; i += 1) {
91 | fireEvent.keyDown(container.querySelector('input'), {
92 | key: 'ArrowDown',
93 | keyCode: KeyCode.DOWN,
94 | which: KeyCode.DOWN,
95 | });
96 | const input = container.querySelector('input');
97 | // no number like 1.5499999999999999
98 | expect((num.toString().split('.')[1] || '').length < 3).toBeTruthy();
99 | const expectedValue = Number(((200 - i) / 100).toFixed(2));
100 | expect(input.value).toEqual(String(expectedValue.toFixed(2)));
101 | expect(num).toEqual(expectedValue);
102 | }
103 |
104 | for (let i = 1; i <= 300; i += 1) {
105 | fireEvent.keyDown(container.querySelector('input'), {
106 | key: 'ArrowUp',
107 | keyCode: KeyCode.UP,
108 | which: KeyCode.UP,
109 | code: 'ArrowUp',
110 | });
111 | const input = container.querySelector('input');
112 | // no number like 1.5499999999999999
113 | expect((num.toString().split('.')[1] || '').length < 3).toBeTruthy();
114 | const expectedValue = Number(((i - 200) / 100).toFixed(2));
115 | expect(input.value).toEqual(String(expectedValue.toFixed(2)));
116 | expect(num).toEqual(expectedValue);
117 | }
118 | });
119 |
120 | // https://github.com/ant-design/ant-design/issues/4229
121 | it('long press not trigger onChange in uncontrolled component', () => {
122 | const onChange = jest.fn();
123 | const { container } = render( );
124 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
125 |
126 | act(() => {
127 | jest.advanceTimersByTime(500);
128 | });
129 | expect(onChange).toHaveBeenCalledWith(1);
130 |
131 | act(() => {
132 | jest.advanceTimersByTime(200);
133 | });
134 | expect(onChange).toHaveBeenCalledWith(2);
135 | });
136 |
137 | // https://github.com/ant-design/ant-design/issues/4757
138 | it('should allow to input text like "1."', () => {
139 | const Demo = () => {
140 | const [value, setValue] = React.useState(1.1);
141 | return (
142 | {
145 | setValue(newValue);
146 | }}
147 | />
148 | );
149 | };
150 |
151 | const { container } = render( );
152 |
153 | const input = container.querySelector('input');
154 | fireEvent.focus(input);
155 | fireEvent.keyDown(input, { which: KeyCode.ONE });
156 | fireEvent.keyDown(input, { which: KeyCode.PERIOD });
157 | fireEvent.change(input, { target: { value: '1.' } });
158 | expect(input.value).toEqual('1.');
159 |
160 | fireEvent.blur(input);
161 | expect(input.value).toEqual('1');
162 | });
163 |
164 | // https://github.com/ant-design/ant-design/issues/5012
165 | // https://github.com/react-component/input-number/issues/64
166 | it('controller InputNumber should be able to input number like 1.00* and 1.10*', () => {
167 | let num;
168 |
169 | const Demo = () => {
170 | const [value, setValue] = React.useState(2);
171 |
172 | return (
173 | {
176 | num = newValue;
177 | setValue(newValue);
178 | }}
179 | />
180 | );
181 | };
182 |
183 | const { container, rerender } = render( );
184 |
185 | const input = container.querySelector('input');
186 | fireEvent.focus(input);
187 | // keydown => 6.0
188 | fireEvent.keyDown(input, { keyCode: KeyCode.SIX });
189 | fireEvent.keyDown(input, { which: KeyCode.PERIOD });
190 | fireEvent.keyDown(input, { which: KeyCode.ZERO });
191 | fireEvent.change(input, { target: { value: '6.0' } });
192 | expect(input.value).toEqual('6.0');
193 | expect(num).toEqual(6);
194 |
195 | fireEvent.blur(input);
196 | expect(input.value).toEqual('6');
197 | expect(num).toEqual(6);
198 |
199 | rerender( );
200 | fireEvent.focus(input);
201 | fireEvent.keyDown(input, { which: KeyCode.SIX });
202 | fireEvent.keyDown(input, { which: KeyCode.PERIOD });
203 | fireEvent.keyDown(input, { which: KeyCode.ONE });
204 | fireEvent.keyDown(input, { which: KeyCode.ZERO });
205 | fireEvent.change(input, { target: { value: '6.10' } });
206 | expect(input.value).toEqual('6.10');
207 | expect(num).toEqual(6.1);
208 |
209 | fireEvent.blur(input);
210 | expect(input.value).toEqual('6.1');
211 | expect(num).toEqual(6.1);
212 | });
213 |
214 | it('onChange should not be called when input is not changed', () => {
215 | const onChange = jest.fn();
216 | const onInput = jest.fn();
217 |
218 | const { container } = render( );
219 |
220 | const input = container.querySelector('input');
221 | fireEvent.focus(input);
222 | fireEvent.change(input, { target: { value: '1' } });
223 | expect(onChange).toHaveBeenCalledTimes(1);
224 | expect(onChange).toHaveBeenCalledWith(1);
225 | expect(onInput).toHaveBeenCalledTimes(1);
226 | expect(onInput).toHaveBeenCalledWith('1');
227 |
228 | fireEvent.blur(input);
229 | expect(onChange).toHaveBeenCalledTimes(1);
230 | expect(onInput).toHaveBeenCalledTimes(1);
231 |
232 | fireEvent.focus(input);
233 | fireEvent.change(input, { target: { value: '' } });
234 | expect(onChange).toHaveBeenCalledTimes(2);
235 | expect(onInput).toHaveBeenCalledTimes(2);
236 | expect(onInput).toHaveBeenCalledWith('');
237 |
238 | fireEvent.blur(input);
239 | expect(onChange).toHaveBeenCalledTimes(2);
240 | expect(onInput).toHaveBeenCalledTimes(2);
241 | expect(onChange).toHaveBeenLastCalledWith(null);
242 |
243 | fireEvent.focus(input);
244 | fireEvent.blur(input);
245 | expect(onChange).toHaveBeenCalledTimes(2);
246 | expect(onInput).toHaveBeenCalledTimes(2);
247 | });
248 |
249 | // https://github.com/ant-design/ant-design/issues/5235
250 | it('input long number', () => {
251 | const { container } = render( );
252 | const input = container.querySelector('input');
253 | fireEvent.focus(input);
254 | fireEvent.change(input, { target: { value: '111111111111111111111' } });
255 | expect(input.value).toEqual('111111111111111111111');
256 | fireEvent.change(input, { target: { value: '11111111111111111111111111111' } });
257 | expect(input.value).toEqual('11111111111111111111111111111');
258 | });
259 |
260 | // https://github.com/ant-design/ant-design/issues/7363
261 | it('uncontrolled input should trigger onChange always when blur it', () => {
262 | const onChange = jest.fn();
263 | const onInput = jest.fn();
264 | const { container } = render(
265 | ,
266 | );
267 |
268 | const input = container.querySelector('input');
269 | fireEvent.focus(input);
270 | fireEvent.change(input, { target: { value: '123' } });
271 | expect(onChange).toHaveBeenCalledTimes(0);
272 | expect(onInput).toHaveBeenCalledTimes(1);
273 | expect(onInput).toHaveBeenCalledWith('123');
274 |
275 | fireEvent.blur(input);
276 | expect(onChange).toHaveBeenCalledTimes(1);
277 | expect(onChange).toHaveBeenCalledWith(10);
278 | expect(onInput).toHaveBeenCalledTimes(1);
279 |
280 | // repeat it, it should works in same way
281 | fireEvent.focus(input);
282 | fireEvent.change(input, { target: { value: '123' } });
283 | expect(onChange).toHaveBeenCalledTimes(1);
284 | expect(onInput).toHaveBeenCalledTimes(2);
285 | expect(onInput).toHaveBeenCalledWith('123');
286 |
287 | fireEvent.blur(input);
288 | expect(onChange).toHaveBeenCalledTimes(1);
289 | expect(onInput).toHaveBeenCalledTimes(2);
290 | });
291 |
292 | // https://github.com/ant-design/ant-design/issues/30465
293 | it('not block user input with min & max', () => {
294 | const onChange = jest.fn();
295 | const { container } = render( );
296 |
297 | const input = container.querySelector('input');
298 | fireEvent.focus(input);
299 |
300 | fireEvent.change(input, { target: { value: '2' } });
301 | expect(onChange).not.toHaveBeenCalled();
302 |
303 | fireEvent.change(input, { target: { value: '20' } });
304 | expect(onChange).not.toHaveBeenCalled();
305 |
306 | fireEvent.change(input, { target: { value: '200' } });
307 | expect(onChange).not.toHaveBeenCalled();
308 |
309 | fireEvent.change(input, { target: { value: '2000' } });
310 | expect(onChange).toHaveBeenCalledWith(2000);
311 | onChange.mockRestore();
312 |
313 | fireEvent.change(input, { target: { value: '1' } });
314 | expect(onChange).not.toHaveBeenCalled();
315 |
316 | fireEvent.blur(input);
317 | expect(onChange).toHaveBeenCalledWith(1900);
318 | });
319 |
320 | // https://github.com/ant-design/ant-design/issues/7867
321 | it('focus should not cut precision of input value', () => {
322 | const Demo = () => {
323 | const [value, setValue] = React.useState(2);
324 | return (
325 | {
329 | setValue(2);
330 | }}
331 | />
332 | );
333 | };
334 |
335 | const { container } = render( );
336 |
337 | const input = container.querySelector('input');
338 | fireEvent.focus(input);
339 | fireEvent.blur(input);
340 |
341 | expect(input.value).toEqual('2.0');
342 |
343 | fireEvent.focus(input);
344 | expect(input.value).toEqual('2.0');
345 | });
346 |
347 | // https://github.com/ant-design/ant-design/issues/7940
348 | it('should not format during input', () => {
349 | let num;
350 | const Demo = () => {
351 | const [value, setValue] = React.useState('');
352 | return (
353 | {
357 | setValue(newValue);
358 | num = newValue;
359 | }}
360 | />
361 | );
362 | };
363 |
364 | const { container } = render( );
365 |
366 | const input = container.querySelector('input');
367 | fireEvent.focus(input);
368 | fireEvent.change(input, { target: { value: '1' } });
369 |
370 | fireEvent.blur(input);
371 | expect(input.value).toEqual('1.0');
372 | expect(num).toEqual(1);
373 | });
374 |
375 | // https://github.com/ant-design/ant-design/issues/8196
376 | it('Allow input 。', async () => {
377 | const onChange = jest.fn();
378 | const { container } = render( );
379 | const input = container.querySelector('input');
380 | fireEvent.change(input, { target: { value: '8。1' } });
381 | fireEvent.blur(input);
382 |
383 | await waitFor(() => expect(input.value).toEqual('8.1'));
384 | await waitFor(() => expect(onChange).toHaveBeenCalledWith(8.1));
385 | });
386 |
387 | // https://github.com/ant-design/ant-design/issues/25614
388 | it("focus value should be '' when clear the input", () => {
389 | let targetValue: string;
390 |
391 | const { container } = render(
392 | {
396 | targetValue = e.target.value;
397 | }}
398 | value={1}
399 | />,
400 | );
401 | const input = container.querySelector('input');
402 | fireEvent.focus(input);
403 | fireEvent.change(input, { target: { value: '' } });
404 | fireEvent.blur(input);
405 | expect(targetValue).toEqual('');
406 | });
407 |
408 | it('should set input value as formatted when blur', () => {
409 | let valueOnBlur: string;
410 |
411 | const { container } = render(
412 | {
414 | valueOnBlur = e.target.value;
415 | }}
416 | formatter={(value) => `${Number(value) * 100}%`}
417 | value={1}
418 | />,
419 | );
420 | const input = container.querySelector('input');
421 | fireEvent.blur(input);
422 | expect(input.value).toEqual('100%');
423 | expect(valueOnBlur).toEqual('100%');
424 | });
425 |
426 | // https://github.com/ant-design/ant-design/issues/11574
427 | // Origin: should trigger onChange when max or min change
428 | it('warning UI when max or min change', () => {
429 | const onChange = jest.fn();
430 | const { container, rerender } = render(
431 | ,
432 | );
433 | const input = container.querySelector('input');
434 | expect(container.querySelector('.rc-input-number-out-of-range')).toBe(null);
435 | rerender( );
436 | expect(input.value).toEqual('10');
437 | expect(container.querySelector('.rc-input-number-out-of-range')).toBeTruthy();
438 | expect(onChange).toHaveBeenCalledTimes(0);
439 |
440 | rerender( );
441 | // wrapper.update();
442 |
443 | expect(input.value).toEqual('15');
444 | expect(container.querySelector('.rc-input-number-out-of-range')).toBeTruthy();
445 | expect(onChange).toHaveBeenCalledTimes(0);
446 | });
447 |
448 | // https://github.com/react-component/input-number/issues/120
449 | it('should not reset value when parent re-render with the same `value` prop', () => {
450 | const Demo = () => {
451 | const [, forceUpdate] = React.useState({});
452 |
453 | return (
454 | {
457 | forceUpdate({});
458 | }}
459 | />
460 | );
461 | };
462 |
463 | const { container } = render( );
464 | const input = container.querySelector('input');
465 | fireEvent.focus(input);
466 | fireEvent.change(input, { target: { value: '401' } });
467 |
468 | // Demo re-render and the `value` prop is still 40, but the user input should be retained
469 | expect(input.value).toEqual('401');
470 | });
471 |
472 | // https://github.com/ant-design/ant-design/issues/16710
473 | it('should use correct precision when change it to 0', () => {
474 | const Demo = () => {
475 | const [precision, setPrecision] = React.useState(2);
476 |
477 | return (
478 |
479 | {
481 | setPrecision(newPrecision);
482 | }}
483 | data-testid="first"
484 | />
485 |
486 |
487 | );
488 | };
489 |
490 | render( );
491 | fireEvent.change(screen.getByTestId('last'), { target: { value: '1.23' } });
492 | fireEvent.change(screen.getByTestId('first'), { target: { value: '0' } });
493 |
494 | expect(screen.getByTestId('last').value).toEqual('1');
495 | });
496 |
497 | // https://github.com/ant-design/ant-design/issues/30478
498 | it('-0 should input able', () => {
499 | const { container } = render( );
500 | const input = container.querySelector('input');
501 | fireEvent.change(input, { target: { value: '-' } });
502 | fireEvent.change(input, { target: { value: '-0' } });
503 | expect(input.value).toEqual('-0');
504 | });
505 |
506 | // https://github.com/ant-design/ant-design/issues/32274
507 | it('global modify when typing', () => {
508 | const Demo = ({ value }: { value?: number }) => {
509 | const [val, setVal] = React.useState(7);
510 |
511 | React.useEffect(() => {
512 | if (value) {
513 | setVal(value);
514 | }
515 | }, [value]);
516 |
517 | return ;
518 | };
519 | const { container, rerender } = render( );
520 | const input = container.querySelector('input');
521 | // Click
522 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
523 | expect(input.value).toEqual('8');
524 |
525 | // Keyboard change
526 | rerender( );
527 | expect(input.value).toEqual('3');
528 | });
529 |
530 | // https://github.com/ant-design/ant-design/issues/36641
531 | it('min value should be worked as expect', () => {
532 | const onChange = jest.fn();
533 | const { container } = render(
534 | ,
535 | );
536 |
537 | expect(container.querySelector('input').value).toEqual('0.00');
538 |
539 | fireEvent.change(container.querySelector('input'), {
540 | target: {
541 | value: '0',
542 | },
543 | });
544 | fireEvent.blur(container.querySelector('input'));
545 |
546 | expect(onChange).toHaveBeenCalledWith(0);
547 | });
548 | });
549 |
--------------------------------------------------------------------------------
/tests/input.test.tsx:
--------------------------------------------------------------------------------
1 | import KeyCode from '@rc-component/util/lib/KeyCode';
2 | import React from 'react';
3 | import InputNumber, { InputNumberProps, InputNumberRef } from '../src';
4 | import { fireEvent, render } from './util/wrapper';
5 |
6 | describe('InputNumber.Input', () => {
7 | function loopInput(input: HTMLElement, text: string) {
8 | for (let i = 0; i < text.length; i += 1) {
9 | const inputTxt = text.slice(0, i + 1);
10 | fireEvent.change(input, { target: { value: inputTxt } });
11 | }
12 | }
13 |
14 | function prepareWrapper(text: string, props?: Partial, skipInputCheck = false) {
15 | const { container } = render( );
16 | const input = container.querySelector('input');
17 | fireEvent.focus(input);
18 | for (let i = 0; i < text.length; i += 1) {
19 | const inputTxt = text.slice(0, i + 1);
20 | fireEvent.change(input, { target: { value: inputTxt } });
21 | }
22 |
23 | if (!skipInputCheck) {
24 | expect(input.value).toEqual(text);
25 | }
26 | fireEvent.blur(input);
27 | return input;
28 | }
29 |
30 | it('input valid number', () => {
31 | const wrapper = prepareWrapper('6');
32 |
33 | expect(wrapper.value).toEqual('6');
34 | });
35 |
36 | it('input invalid number', () => {
37 | const wrapper = prepareWrapper('xx');
38 |
39 | expect(wrapper.value).toEqual('');
40 | });
41 |
42 | it('input invalid string with number', () => {
43 | const wrapper = prepareWrapper('2x');
44 |
45 | expect(wrapper.value).toEqual('2');
46 | });
47 |
48 | it('input invalid decimal point with max number', () => {
49 | const wrapper = prepareWrapper('15.', { max: 10 });
50 | expect(wrapper.value).toEqual('10');
51 | });
52 |
53 | it('input invalid decimal point with min number', () => {
54 | const wrapper = prepareWrapper('3.', { min: 5 });
55 | expect(wrapper.value).toEqual('5');
56 | });
57 |
58 | it('input negative symbol', () => {
59 | const wrapper = prepareWrapper('-');
60 | expect(wrapper.value).toEqual('');
61 | });
62 |
63 | it('input negative number', () => {
64 | const wrapper = prepareWrapper('-98');
65 | expect(wrapper.value).toEqual('-98');
66 | });
67 |
68 | it('negative min with higher precision', () => {
69 | const wrapper = prepareWrapper('-4', { min: -3.5, precision: 0 });
70 | expect(wrapper.value).toEqual('-3');
71 | });
72 |
73 | it('positive min with higher precision', () => {
74 | const wrapper = prepareWrapper('4', { min: 3.5, precision: 0 });
75 | expect(wrapper.value).toEqual('4');
76 | });
77 |
78 | it('negative max with higher precision', () => {
79 | const wrapper = prepareWrapper('-4', { max: -3.5, precision: 0 });
80 | expect(wrapper.value).toEqual('-4');
81 | });
82 |
83 | it('positive max with higher precision', () => {
84 | const wrapper = prepareWrapper('4', { max: 3.5, precision: 0 });
85 | expect(wrapper.value).toEqual('3');
86 | });
87 |
88 | // https://github.com/ant-design/ant-design/issues/9439
89 | it('input negative zero', async () => {
90 | const wrapper = await prepareWrapper('-0', {}, true);
91 | expect(wrapper.value).toEqual('0');
92 | });
93 |
94 | it('input decimal number with integer step', () => {
95 | const wrapper = prepareWrapper('1.2', { step: 1.2 });
96 | expect(wrapper.value).toEqual('1.2');
97 | });
98 |
99 | it('input decimal number with decimal step', () => {
100 | const wrapper = prepareWrapper('1.2', { step: 0.1 });
101 | expect(wrapper.value).toEqual('1.2');
102 | });
103 |
104 | it('input empty text and blur', () => {
105 | const wrapper = prepareWrapper('');
106 | expect(wrapper.value).toEqual('');
107 | });
108 |
109 | it('blur on default input', () => {
110 | const onChange = jest.fn();
111 | const { container } = render( );
112 | fireEvent.blur(container.querySelector('input'));
113 | expect(onChange).not.toHaveBeenCalled();
114 | });
115 |
116 | it('pressEnter works', () => {
117 | const onPressEnter = jest.fn();
118 | const { container } = render( );
119 | fireEvent.keyDown(container.querySelector('.rc-input-number'), {
120 | key: 'Enter',
121 | keyCode: KeyCode.ENTER,
122 | which: KeyCode.ENTER,
123 | });
124 | expect(onPressEnter).toHaveBeenCalled();
125 | expect(onPressEnter).toHaveBeenCalledTimes(1);
126 | });
127 |
128 | it('pressEnter value should be ok', () => {
129 | const Demo = () => {
130 | const [value, setValue] = React.useState(1);
131 | const inputRef = React.useRef(null);
132 | return (
133 | {
137 | setValue(Number(inputRef.current.value));
138 | }}
139 | />
140 | );
141 | };
142 |
143 | const { container } = render( );
144 | const input = container.querySelector('input');
145 | fireEvent.focus(input);
146 | fireEvent.change(input, { target: { value: '3' } });
147 | fireEvent.keyDown(input, { which: KeyCode.ENTER });
148 | expect(input.value).toEqual('3');
149 | fireEvent.change(input, { target: { value: '5' } });
150 | fireEvent.keyDown(input, { which: KeyCode.ENTER });
151 | expect(input.value).toEqual('5');
152 | });
153 |
154 | it('keydown Tab, after change value should be ok', () => {
155 | let outSetValue;
156 |
157 | const Demo = () => {
158 | const [value, setValue] = React.useState(1);
159 | outSetValue = setValue;
160 | return setValue(val)} />;
161 | };
162 |
163 | const { container } = render( );
164 | const input = container.querySelector('input');
165 | fireEvent.keyDown(input, { which: KeyCode.TAB });
166 | fireEvent.blur(input);
167 | expect(input.value).toEqual('1');
168 | outSetValue(5);
169 | fireEvent.focus(input);
170 | expect(input.value).toEqual('5');
171 | });
172 |
173 | // https://github.com/ant-design/ant-design/issues/40733
174 | it('input combo should be correct', () => {
175 | const onChange = jest.fn();
176 | const input = prepareWrapper('', {
177 | onChange,
178 | precision: 0,
179 | });
180 |
181 | onChange.mockReset();
182 |
183 | fireEvent.focus(input);
184 | loopInput(input, '1.55.55');
185 | expect(onChange).not.toHaveBeenCalledWith(2);
186 |
187 | fireEvent.blur(input);
188 | expect(input.value).toEqual('2');
189 | expect(onChange).toHaveBeenCalledWith(2);
190 | });
191 |
192 | describe('empty on blur should trigger null', () => {
193 | it('basic', () => {
194 | const onChange = jest.fn();
195 | const { container } = render( );
196 | const input = container.querySelector('input');
197 | fireEvent.change(input, { target: { value: '' } });
198 | expect(onChange).toHaveBeenCalledWith(null);
199 |
200 | fireEvent.blur(input);
201 | expect(onChange).toHaveBeenLastCalledWith(null);
202 | });
203 |
204 | it('min range', () => {
205 | const onChange = jest.fn();
206 | const { container } = render( );
207 | const input = container.querySelector('input');
208 | fireEvent.change(input, { target: { value: '' } });
209 | expect(onChange).toHaveBeenCalled();
210 |
211 | fireEvent.blur(input);
212 | expect(onChange).toHaveBeenLastCalledWith(null);
213 | });
214 | });
215 |
216 | it('!changeOnBlur', () => {
217 | const onChange = jest.fn();
218 |
219 | const { container } = render(
220 | ,
221 | );
222 |
223 | fireEvent.blur(container.querySelector('input'));
224 | expect(onChange).not.toHaveBeenCalled();
225 | });
226 |
227 | describe('nativeElement', () => {
228 | it('basic', () => {
229 | const ref = React.createRef();
230 | const { container } = render( );
231 | expect(ref.current.nativeElement).toBe(container.querySelector('.rc-input-number'));
232 | });
233 |
234 | it('wrapper', () => {
235 | const ref = React.createRef();
236 | const { container } = render( );
237 | expect(ref.current.nativeElement).toBe(
238 | container.querySelector('.rc-input-number-affix-wrapper'),
239 | );
240 | });
241 | });
242 | });
243 |
--------------------------------------------------------------------------------
/tests/keyboard.test.tsx:
--------------------------------------------------------------------------------
1 | import KeyCode from '@rc-component/util/lib/KeyCode';
2 | import InputNumber from '../src';
3 | import { fireEvent, render } from './util/wrapper';
4 |
5 | describe('InputNumber.Keyboard', () => {
6 | it('up', () => {
7 | const onChange = jest.fn();
8 | const { container } = render( );
9 | fireEvent.keyDown(container.querySelector('input'), {
10 | which: KeyCode.UP,
11 | key: 'ArrowUp',
12 | keyCode: KeyCode.UP,
13 | });
14 | expect(onChange).toHaveBeenCalledWith(1);
15 | });
16 |
17 | it('up with pressing shift key', () => {
18 | const onChange = jest.fn();
19 | const { container } = render( );
20 | fireEvent.keyDown(container.querySelector('input'), {
21 | which: KeyCode.UP,
22 | key: 'ArrowUp',
23 | keyCode: KeyCode.UP,
24 | shiftKey: true,
25 | });
26 | expect(onChange).toHaveBeenCalledWith(1.3);
27 | });
28 |
29 | it('down', () => {
30 | const onChange = jest.fn();
31 | const { container } = render( );
32 | fireEvent.keyDown(container.querySelector('input'), {
33 | which: KeyCode.DOWN,
34 | key: 'ArrowDown',
35 | keyCode: KeyCode.DOWN,
36 | });
37 | expect(onChange).toHaveBeenCalledWith(-1);
38 | });
39 |
40 | it('down with pressing shift key', () => {
41 | const onChange = jest.fn();
42 | const { container } = render( );
43 | fireEvent.keyDown(container.querySelector('input'), {
44 | which: KeyCode.DOWN,
45 | key: 'ArrowDown',
46 | keyCode: KeyCode.DOWN,
47 | shiftKey: true,
48 | });
49 | expect(onChange).toHaveBeenCalledWith(1.1);
50 | });
51 |
52 | // shift + 10, ctrl + 0.1 test case removed
53 |
54 | it('disabled keyboard', () => {
55 | const onChange = jest.fn();
56 | const { container } = render( );
57 |
58 | fireEvent.keyDown(container.querySelector('input'), { which: KeyCode.UP, key: 'ArrowUp' });
59 | expect(onChange).not.toHaveBeenCalled();
60 |
61 | fireEvent.keyDown(container.querySelector('input'), { which: KeyCode.DOWN, key: 'ArrowDown' });
62 | expect(onChange).not.toHaveBeenCalled();
63 | });
64 |
65 | it('enter to trigger onChange with precision', () => {
66 | const onChange = jest.fn();
67 | const { container } = render( );
68 | const input = container.querySelector('input');
69 | fireEvent.change(input, { target: { value: '2.3333' } });
70 | expect(onChange).toHaveBeenCalledWith(2.3333);
71 | onChange.mockReset();
72 |
73 | fireEvent.keyDown(input, { which: KeyCode.ENTER, key: 'Enter', keyCode: KeyCode.ENTER });
74 | expect(onChange).toHaveBeenCalledWith(2);
75 | });
76 | });
77 |
--------------------------------------------------------------------------------
/tests/longPress.test.tsx:
--------------------------------------------------------------------------------
1 | import InputNumber from '../src';
2 | import { act, fireEvent, render, waitFor } from './util/wrapper';
3 |
4 | // Jest will mass of advanceTimersByTime if other test case not use fakeTimer.
5 | // Let's create a pure file here for test.
6 |
7 | describe('InputNumber.LongPress', () => {
8 | beforeAll(() => {
9 | jest.useFakeTimers();
10 | });
11 |
12 | afterAll(() => {
13 | jest.useRealTimers();
14 | });
15 |
16 | it('up button works', async () => {
17 | const onChange = jest.fn();
18 | const { container } = render( );
19 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
20 | act(() => {
21 | jest.advanceTimersByTime(600 + 200 * 5 + 100);
22 | });
23 | await waitFor(() => expect(onChange).toHaveBeenCalledWith(26));
24 | });
25 |
26 | it('down button works', async () => {
27 | const onChange = jest.fn();
28 | const { container } = render( );
29 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down'));
30 |
31 | act(() => {
32 | jest.advanceTimersByTime(600 + 200 * 5 + 100);
33 | });
34 | await waitFor(() => expect(onChange).toHaveBeenCalledWith(14));
35 | });
36 |
37 | it('Simulates event calls out of order in Safari', async () => {
38 | const onChange = jest.fn();
39 | const { container } = render( );
40 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
41 | act(() => {
42 | jest.advanceTimersByTime(10);
43 | });
44 | fireEvent.mouseUp(container.querySelector('.rc-input-number-handler-up'));
45 | act(() => {
46 | jest.advanceTimersByTime(10);
47 | });
48 | fireEvent.mouseUp(container.querySelector('.rc-input-number-handler-up'));
49 | act(() => {
50 | jest.advanceTimersByTime(10);
51 | });
52 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
53 | act(() => {
54 | jest.advanceTimersByTime(10);
55 | });
56 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
57 | act(() => {
58 | jest.advanceTimersByTime(10);
59 | });
60 | fireEvent.mouseUp(container.querySelector('.rc-input-number-handler-up'));
61 |
62 | act(() => {
63 | jest.advanceTimersByTime(600 + 200 * 5 + 100);
64 | });
65 |
66 | await waitFor(() => expect(onChange).toBeCalledTimes(3));
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/tests/mobile.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from './util/wrapper';
3 | import InputNumber from '../src';
4 | import { renderToString } from 'react-dom/server';
5 |
6 | jest.mock('@rc-component/util/lib/isMobile', () => () => true);
7 |
8 | // Mobile touch experience is not user-friendly which not apply in antd.
9 | // Let's hide operator instead.
10 |
11 | describe('InputNumber.Mobile', () => {
12 | it('not show steps when mobile', () => {
13 | const {container} = render( );
14 | expect(container.querySelector('.rc-input-number-handler-wrap')).toBeFalsy();
15 | });
16 |
17 | it('should render in server side', () => {
18 | const serverHTML = renderToString( );
19 | expect(serverHTML).toContain('rc-input-number-handler-wrap');
20 | })
21 | });
22 |
--------------------------------------------------------------------------------
/tests/precision.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom';
3 | import { render, fireEvent } from '@testing-library/react';
4 | import KeyCode from '@rc-component/util/lib/KeyCode';
5 | import InputNumber from '../src';
6 |
7 | describe('InputNumber.Precision', () => {
8 | // https://github.com/react-component/input-number/issues/506
9 | it('Safari bug of input', async () => {
10 | const Demo = () => {
11 | const [value, setValue] = React.useState(null);
12 |
13 | return ;
14 | };
15 |
16 | const { container } = render( );
17 | const input = container.querySelector('input');
18 |
19 | // React use SyntheticEvent to handle `onBeforeInput`, let's mock this
20 | fireEvent.keyPress(input, {
21 | which: KeyCode.TWO,
22 | keyCode: KeyCode.TWO,
23 | char: '2',
24 | });
25 |
26 | fireEvent.change(input, {
27 | target: {
28 | value: '2',
29 | },
30 | });
31 |
32 | expect(input.value).toEqual('2');
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/tests/props.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '@testing-library/jest-dom';
3 | import { render, fireEvent } from '@testing-library/react';
4 | import KeyCode from '@rc-component/util/lib/KeyCode';
5 | import type { ValueType } from '../src'
6 | import InputNumber from '../src';
7 |
8 | describe('InputNumber.Props', () => {
9 |
10 | it('max', () => {
11 | const onChange = jest.fn();
12 | const { container } = render( );
13 | for (let i = 0; i < 100; i += 1) {
14 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
15 | }
16 |
17 | expect(onChange.mock.calls[onChange.mock.calls.length - 1][0]).toEqual(10);
18 |
19 | expect(container.querySelector('input')).toHaveAttribute('aria-valuemax', '10');
20 | expect(container.querySelector('input')).toHaveAttribute('aria-valuenow', '10');
21 | });
22 |
23 | it('min', () => {
24 | const onChange = jest.fn();
25 | const { container } = render( );
26 | for (let i = 0; i < 100; i += 1) {
27 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down'));
28 | }
29 |
30 | expect(onChange.mock.calls[onChange.mock.calls.length - 1][0]).toEqual(-10);
31 |
32 | expect(container.querySelector('input')).toHaveAttribute('aria-valuemin', '-10');
33 | expect(container.querySelector('input')).toHaveAttribute('aria-valuenow', '-10');
34 | });
35 |
36 | it('disabled', () => {
37 | const onChange = jest.fn();
38 | const { container } = render( );
39 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
40 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down'));
41 | expect(container.querySelector('.rc-input-number-disabled')).toBeTruthy();
42 | expect(onChange).not.toHaveBeenCalled();
43 | });
44 |
45 | it('readOnly', () => {
46 | const onChange = jest.fn();
47 | const { container } = render( );
48 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
49 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down'));
50 | fireEvent.keyDown(container.querySelector('input'), { which: KeyCode.UP });
51 | fireEvent.keyDown(container.querySelector('input'), { which: KeyCode.DOWN });
52 | expect(container.querySelector('.rc-input-number-readonly')).toBeTruthy();
53 | expect(onChange).not.toHaveBeenCalled();
54 | });
55 |
56 | it('autofocus', (done) => {
57 | const onFocus = jest.fn();
58 | const { container } = render( );
59 | const input = container.querySelector('input');
60 | setTimeout(() => {
61 | expect(input).toHaveFocus();
62 | done();
63 | }, 500);
64 |
65 | });
66 |
67 | describe('step', () => {
68 | it('basic', () => {
69 | const onChange = jest.fn();
70 |
71 | const { container } = render( );
72 | for (let i = 0; i < 3; i += 1) {
73 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down'));
74 | expect(onChange).toHaveBeenCalledWith(-5 * (i + 1));
75 | }
76 | expect(container.querySelector('input')).toHaveAttribute('step', '5');
77 | });
78 |
79 | it('basic with pressing shift key', () => {
80 | const onChange = jest.fn();
81 | const { container } = render( );
82 |
83 | for (let i = 0; i < 3; i += 1) {
84 | fireEvent.keyDown(container.querySelector('.rc-input-number-handler-down'), {
85 | shiftKey: true,
86 | });
87 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down'));
88 |
89 | expect(onChange).toHaveBeenCalledWith(-5 * (i + 1) * 10);
90 | }
91 | });
92 |
93 | it('stringMode', () => {
94 | const onChange = jest.fn();
95 | const { container } = render(
96 | ,
102 | );
103 |
104 | for (let i = 0; i < 11; i += 1) {
105 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down'));
106 | }
107 |
108 | expect(onChange).toHaveBeenCalledWith('-0.00000001');
109 | });
110 |
111 | it('stringMode with pressing shift key', () => {
112 | const onChange = jest.fn();
113 | const { container } = render(
114 | ,
120 | );
121 |
122 | for (let i = 0; i < 11; i += 1) {
123 | fireEvent.keyDown(container.querySelector('.rc-input-number-handler-down'), {
124 | shiftKey: true,
125 | });
126 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down'));
127 | }
128 |
129 | expect(onChange).toHaveBeenCalledWith('-0.00000001'); // -1e-8
130 | });
131 |
132 | it('decimal', () => {
133 | const onChange = jest.fn();
134 | const { container } = render(
135 | ,
136 | );
137 | for (let i = 0; i < 3; i += 1) {
138 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
139 | }
140 | expect(onChange).toHaveBeenCalledWith(1.2);
141 | });
142 |
143 | it('decimal with pressing shift key', () => {
144 | const onChange = jest.fn();
145 | const { container } = render(
146 | ,
147 | );
148 | for (let i = 0; i < 3; i += 1) {
149 | fireEvent.keyDown(container.querySelector('input'), {
150 | shiftKey: true,
151 | which: KeyCode.UP,
152 | key: 'ArrowUp',
153 | code: 'ArrowUp',
154 | keyCode: KeyCode.UP,
155 | });
156 | }
157 | expect(onChange).toHaveBeenCalledWith(3.9);
158 | });
159 | });
160 |
161 | describe('controlled', () => {
162 | it('restore when blur input', () => {
163 | const { container } = render( );
164 | const input = container.querySelector('input');
165 | fireEvent.focus(input);
166 | fireEvent.change(input, { target: { value: '3' } });
167 | expect(input.value).toEqual('3');
168 |
169 | fireEvent.blur(input);
170 | expect(input.value).toEqual('9');
171 | });
172 |
173 | it('dynamic change value', () => {
174 | const { container, rerender } = render( );
175 | const input = container.querySelector('input');
176 | rerender( );
177 | expect(input.value).toEqual('3');
178 | });
179 |
180 | // Origin https://github.com/ant-design/ant-design/issues/7334
181 | // zombieJ: We should error this instead of auto change back to a normal value since it makes un-controlled
182 | it('show limited value when input is not focused', () => {
183 | const Demo = () => {
184 | const [value, setValue] = React.useState(2);
185 |
186 | return (
187 |
188 | {
191 | setValue('103aa');
192 | }}
193 | >
194 | change value
195 |
196 |
197 |
198 | );
199 | };
200 |
201 | const { container } = render( );
202 | const input = container.querySelector('input');
203 | expect(input.value).toEqual('2');
204 |
205 | fireEvent.click(container.querySelector('button'));
206 | expect(input.value).toEqual('103aa');
207 | expect(container.querySelector('.rc-input-number-not-a-number')).toBeTruthy();
208 | });
209 |
210 | // https://github.com/ant-design/ant-design/issues/7358
211 | it('controlled component should accept undefined value', () => {
212 | const Demo = () => {
213 | const [value, setValue] = React.useState(2);
214 |
215 | return (
216 |
217 | {
220 | setValue(undefined);
221 | }}
222 | >
223 | change value
224 |
225 |
226 |
227 | );
228 | };
229 |
230 | const { container } = render( );
231 | const input = container.querySelector('input');
232 | expect(input.value).toEqual('2');
233 |
234 | fireEvent.click(container.querySelector('button'));
235 | expect(input.value).toEqual('');
236 | });
237 | });
238 |
239 | describe('defaultValue', () => {
240 | it('default value should be empty', () => {
241 | const { container } = render( );
242 | const input = container.querySelector('input');
243 | expect(input.value).toEqual('');
244 | });
245 |
246 | it('default value should be empty when step is decimal', () => {
247 | const { container } = render( );
248 | const input = container.querySelector('input');
249 | expect(input.value).toEqual('');
250 | });
251 |
252 | it('default value should be 1', () => {
253 | const { container } = render( );
254 | const input = container.querySelector('input');
255 | expect(input.value).toEqual('1');
256 | });
257 |
258 | it('default value could be null', () => {
259 | const { container } = render( );
260 | const input = container.querySelector('input');
261 | expect(input.value).toEqual('');
262 | });
263 |
264 | it('warning when defaultValue higher than max', () => {
265 | const { container } = render( );
266 | const input = container.querySelector('input');
267 | expect(input.value).toEqual('13');
268 | expect(container.querySelector('.rc-input-number-out-of-range')).toBeTruthy();
269 | });
270 |
271 | it('warning when defaultValue lower than min', () => {
272 | const { container } = render( );
273 | const input = container.querySelector('input');
274 | expect(input.value).toEqual('-1');
275 | expect(container.querySelector('.rc-input-number-out-of-range')).toBeTruthy();
276 | });
277 |
278 | it('default value can be a string greater than 16 characters', () => {
279 | const { container } = render( max={10} defaultValue='-3.637978807091713e-12' />);
280 | const input = container.querySelector('input');
281 | expect(input.value).toEqual('-0.000000000003637978807091713');
282 | });
283 |
284 | it('invalidate defaultValue', () => {
285 | const { container } = render( );
286 | const input = container.querySelector('input');
287 | expect(input.value).toEqual('light');
288 | });
289 | });
290 |
291 | describe('value', () => {
292 | it('value shouldn\'t higher than max', () => {
293 | const { container } = render( );
294 | const input = container.querySelector('input');
295 | expect(input.value).toEqual('13');
296 | expect(container.querySelector('.rc-input-number-out-of-range')).toBeTruthy();
297 | });
298 |
299 | it('value shouldn\'t lower than min', () => {
300 | const { container } = render( );
301 | const input = container.querySelector('input');
302 | expect(input.value).toEqual('-1');
303 | expect(container.querySelector('.rc-input-number-out-of-range')).toBeTruthy();
304 | });
305 |
306 | it('value can be a string greater than 16 characters', () => {
307 | const { container } = render( max={10} value='-3.637978807091713e-12' />);
308 | const input = container.querySelector('input');
309 | expect(input.value).toEqual('-0.000000000003637978807091713');
310 | });
311 |
312 | it('value decimal over six decimal not be scientific notation', () => {
313 | const onChange = jest.fn();
314 | const { container } = render(
315 | ,
316 | );
317 | const input = container.querySelector('input');
318 | for (let i = 1; i <= 9; i += 1) {
319 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-up'));
320 | expect(input.value).toEqual(`0.000000${i}`);
321 | expect(onChange).toHaveBeenCalledWith(0.0000001 * i);
322 | }
323 |
324 | for (let i = 8; i >= 1; i -= 1) {
325 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down'));
326 | expect(input.value).toEqual(`0.000000${i}`);
327 | expect(onChange).toHaveBeenCalledWith(0.0000001 * i);
328 | }
329 |
330 | fireEvent.mouseDown(container.querySelector('.rc-input-number-handler-down'));
331 | expect(input.value).toEqual(`0.0000000`);
332 | expect(onChange).toHaveBeenCalledWith(0);
333 | });
334 |
335 | it('value can be changed when dynamic setting max', () => {
336 | const { container, rerender } = render( );
337 | const input = container.querySelector('input');
338 |
339 | // Origin logic shows `10` as `max`. But it breaks form logic.
340 | expect(input.value).toEqual('11');
341 | expect(container.querySelector('.rc-input-number-out-of-range')).toBeTruthy();
342 |
343 | rerender( );
344 | expect(input.value).toEqual('11');
345 | expect(container.querySelector('.rc-input-number-out-of-range')).toBeFalsy();
346 | });
347 |
348 | it('value can be changed when dynamic setting min', () => {
349 | const { container, rerender } = render( );
350 | const input = container.querySelector('input');
351 |
352 | // Origin logic shows `10` as `max`. But it breaks form logic.
353 | expect(input.value).toEqual('9');
354 | expect(container.querySelector('.rc-input-number-out-of-range')).toBeTruthy();
355 |
356 | rerender( );
357 | expect(input.value).toEqual('9');
358 | expect(container.querySelector('.rc-input-number-out-of-range')).toBeFalsy();
359 | });
360 |
361 | it('value can override given defaultValue', () => {
362 | const { container } = render( );
363 | const input = container.querySelector('input');
364 | expect(input.value).toEqual('2');
365 | });
366 | });
367 |
368 | describe(`required prop`, () => {
369 | it(`should add required attr to the input tag when get passed as true`, () => {
370 | const { container } = render( );
371 | expect(container.querySelector('input')).toHaveAttribute('required');
372 | });
373 |
374 | it(`should not add required attr to the input as default props when not being supplied`, () => {
375 | const { container } = render( );
376 | expect(container.querySelector('input')).not.toHaveAttribute('required');
377 | });
378 |
379 | it(`should not add required attr to the input tag when get passed as false`, () => {
380 | const { container } = render( );
381 | expect(container.querySelector('input')).not.toHaveAttribute('required');
382 | });
383 | });
384 |
385 | describe('Pattern prop', () => {
386 | it(`should render with a pattern attribute if the pattern prop is supplied`, () => {
387 | const { container } = render( );
388 | expect(container.querySelector('input')).toHaveAttribute('pattern', '\\d*');
389 | });
390 |
391 | it(`should render with no pattern attribute if the pattern prop is not supplied`, () => {
392 | const { container } = render( );
393 | expect(container.querySelector('input')).not.toHaveAttribute('pattern', '\\d*');
394 |
395 | });
396 | });
397 |
398 | describe('onPaste props', () => {
399 | it('passes onPaste event handler', () => {
400 | const onPaste = jest.fn();
401 | const { container } = render( );
402 | const input = container.querySelector('input');
403 | fireEvent.paste(input);
404 | // wrapper.findInput().simulate('paste');
405 | expect(onPaste).toHaveBeenCalled();
406 | });
407 | });
408 |
409 | describe('aria and data props', () => {
410 | it('passes data-* attributes', () => {
411 | const { container } = render( );
412 | const input = container.querySelector('input');
413 |
414 | expect(input).toHaveAttribute('data-test', 'test-id');
415 | expect(input).toHaveAttribute('data-id', '12345');
416 | });
417 |
418 | it('passes aria-* attributes', () => {
419 | const { container } = render(
420 | ,
421 | );
422 | const input = container.querySelector('input');
423 | expect(input).toHaveAttribute('aria-labelledby', 'test-id');
424 | expect(input).toHaveAttribute('aria-label', 'some-label');
425 |
426 | });
427 |
428 | it('passes role attribute', () => {
429 | const { container } = render( );
430 | expect(container.querySelector('input')).toHaveAttribute('role', 'searchbox');
431 |
432 | });
433 | });
434 | });
435 |
--------------------------------------------------------------------------------
/tests/semantic.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import InputNumber from '../src';
3 | import React from 'react';
4 |
5 | describe('InputNumber.Semantic', () => {
6 | it('support classNames and styles', () => {
7 | const testClassNames = {
8 | prefix: 'test-prefix',
9 | input: 'test-input',
10 | suffix: 'test-suffix',
11 | actions: 'test-handle',
12 | };
13 | const testStyles = {
14 | prefix: { color: 'red' },
15 | input: { color: 'blue' },
16 | suffix: { color: 'green' },
17 | actions: { color: 'yellow' },
18 | };
19 | const { container } = render(
20 | suffix}
24 | styles={testStyles}
25 | classNames={testClassNames}
26 | />,
27 | );
28 |
29 | const input = container.querySelector('.rc-input-number')!;
30 | const prefix = container.querySelector('.rc-input-number-prefix')!;
31 | const suffix = container.querySelector('.rc-input-number-suffix')!;
32 | const actions = container.querySelector('.rc-input-number-handler-wrap')!;
33 | expect(input.className).toContain(testClassNames.input);
34 | expect(prefix.className).toContain(testClassNames.prefix);
35 | expect(suffix.className).toContain(testClassNames.suffix);
36 | expect(actions.className).toContain(testClassNames.actions);
37 | expect(prefix).toHaveStyle(testStyles.prefix);
38 | expect(input).toHaveStyle(testStyles.input);
39 | expect(suffix).toHaveStyle(testStyles.suffix);
40 | expect(actions).toHaveStyle(testStyles.actions);
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/tests/setup.js:
--------------------------------------------------------------------------------
1 | global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
2 | require('regenerator-runtime');
3 |
--------------------------------------------------------------------------------
/tests/util/wrapper.ts:
--------------------------------------------------------------------------------
1 | import type { RenderOptions } from '@testing-library/react';
2 | import { act, render } from '@testing-library/react';
3 | import type { ReactElement } from 'react';
4 |
5 | const globalTimeout = global.setTimeout;
6 |
7 | export const sleep = async (timeout = 0) => {
8 | await act(async () => {
9 | await new Promise((resolve) => {
10 | globalTimeout(resolve, timeout);
11 | });
12 | });
13 | };
14 |
15 | const customRender = (ui: ReactElement, options?: Omit) =>
16 | render(ui, { ...options });
17 |
18 | export * from '@testing-library/react';
19 | export { customRender as render };
20 |
--------------------------------------------------------------------------------
/tests/wheel.test.tsx:
--------------------------------------------------------------------------------
1 | import KeyCode from '@rc-component/util/lib/KeyCode';
2 | import InputNumber from '../src';
3 | import { fireEvent, render } from './util/wrapper';
4 |
5 | describe('InputNumber.Wheel', () => {
6 | it('wheel up', () => {
7 | const onChange = jest.fn();
8 | const { container } = render( );
9 | fireEvent.focus(container.firstChild);
10 | fireEvent.wheel(container.querySelector('input'), {deltaY: -1});
11 | expect(onChange).toHaveBeenCalledWith(1);
12 | });
13 |
14 | it('wheel up with pressing shift key', () => {
15 | const onChange = jest.fn();
16 | const { container } = render( );
17 | fireEvent.focus(container.firstChild);
18 | fireEvent.keyDown(container.querySelector('input'), {
19 | which: KeyCode.SHIFT,
20 | key: 'Shift',
21 | keyCode: KeyCode.SHIFT,
22 | shiftKey: true,
23 | });
24 | fireEvent.wheel(container.querySelector('input'), {deltaY: -1});
25 | expect(onChange).toHaveBeenCalledWith(1.3);
26 | });
27 |
28 | it('wheel down', () => {
29 | const onChange = jest.fn();
30 | const { container } = render( );
31 | fireEvent.focus(container.firstChild);
32 | fireEvent.wheel(container.querySelector('input'), {deltaY: 1});
33 | expect(onChange).toHaveBeenCalledWith(-1);
34 | });
35 |
36 | it('wheel down with pressing shift key', () => {
37 | const onChange = jest.fn();
38 | const { container } = render( );
39 | fireEvent.focus(container.firstChild);
40 | fireEvent.keyDown(container.querySelector('input'), {
41 | which: KeyCode.SHIFT,
42 | key: 'Shift',
43 | keyCode: KeyCode.SHIFT,
44 | shiftKey: true,
45 | });
46 | fireEvent.wheel(container.querySelector('input'), {deltaY: 1});
47 | expect(onChange).toHaveBeenCalledWith(1.1);
48 | });
49 |
50 | it('disabled wheel', () => {
51 | const onChange = jest.fn();
52 | const { container, rerender } = render( );
53 | fireEvent.focus(container.firstChild);
54 |
55 | fireEvent.wheel(container.querySelector('input'), {deltaY: -1});
56 | expect(onChange).not.toHaveBeenCalled();
57 |
58 | fireEvent.wheel(container.querySelector('input'), {deltaY: 1});
59 | expect(onChange).not.toHaveBeenCalled();
60 |
61 | rerender( );
62 | fireEvent.focus(container.firstChild);
63 |
64 | fireEvent.wheel(container.querySelector('input'), {deltaY: 1});
65 | expect(onChange).toHaveBeenCalledWith(-1);
66 | });
67 |
68 | it('wheel is limited to range', () => {
69 | const onChange = jest.fn();
70 | const { container } = render( );
71 | fireEvent.focus(container.firstChild);
72 | fireEvent.keyDown(container.querySelector('input'), {
73 | which: KeyCode.SHIFT,
74 | key: 'Shift',
75 | keyCode: KeyCode.SHIFT,
76 | shiftKey: true,
77 | });
78 | fireEvent.wheel(container.querySelector('input'), {deltaY: -1});
79 | expect(onChange).toHaveBeenCalledWith(3);
80 | fireEvent.wheel(container.querySelector('input'), {deltaY: 1});
81 | expect(onChange).toHaveBeenCalledWith(-3);
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "moduleResolution": "node",
5 | "baseUrl": "./",
6 | "jsx": "preserve",
7 | "declaration": true,
8 | "skipLibCheck": true,
9 | "esModuleInterop": true,
10 | "paths": {
11 | "@/*": ["src/*"],
12 | "@@/*": ["src/.umi/*"],
13 | "@rc-component/input-number": ["src/index.ts"]
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/update-demo.js:
--------------------------------------------------------------------------------
1 | /*
2 | 用于 dumi 改造使用,
3 | 可用于将 examples 的文件批量修改为 demo 引入形式,
4 | 其他项目根据具体情况使用。
5 | */
6 |
7 | const fs = require('fs');
8 | const glob = require('glob');
9 |
10 | const paths = glob.sync('./docs/examples/*.tsx');
11 |
12 | paths.forEach(path => {
13 | const name = path.split('/').pop().split('.')[0];
14 | fs.writeFile(
15 | `./docs/demo/${name}.md`,
16 | `## ${name}
17 |
18 |
19 | `,
20 | 'utf8',
21 | function(error) {
22 | if(error){
23 | console.log(error);
24 | return false;
25 | }
26 | console.log(`${name} 更新成功~`);
27 | }
28 | )
29 | });
30 |
--------------------------------------------------------------------------------