diff --git a/assets/index.less b/assets/index.less index a5e1abe5..e3e559e5 100644 --- a/assets/index.less +++ b/assets/index.less @@ -133,4 +133,24 @@ .handler-disabled(); } } + + &-type-spinner { + display: inline-flex; + align-items: center; + } + + &-type-spinner &-handler { + flex: 0 0 20px; + line-height: 26px; + height: 100%; + } + + &-type-spinner &-handler-up { + border-bottom: 0; + border-left: 1px solid #d9d9d9; + } + &-type-spinner &-handler-down { + border-top: 0; + border-right: 1px solid #d9d9d9; + } } diff --git a/docs/demo/spinner.tsx b/docs/demo/spinner.tsx new file mode 100644 index 00000000..085bc0b3 --- /dev/null +++ b/docs/demo/spinner.tsx @@ -0,0 +1,78 @@ +/* eslint no-console:0 */ +import InputNumber from '@rc-component/input-number'; +import React from 'react'; +import '../../assets/index.less'; + +export default () => { + const [disabled, setDisabled] = React.useState(false); + const [readOnly, setReadOnly] = React.useState(false); + const [keyboard, setKeyboard] = React.useState(true); + const [wheel, setWheel] = React.useState(true); + const [stringMode, setStringMode] = React.useState(false); + const [value, setValue] = React.useState(93); + + const onChange = (val: number) => { + console.warn('onChange:', val, typeof val); + setValue(val); + }; + + return ( +
+

Controlled

+ +

+ + + + + +

+ +
+

Uncontrolled

+ + +
+

!changeOnBlur

+ +
+ ); +}; diff --git a/docs/example.md b/docs/example.md index 7743a757..b88e7135 100644 --- a/docs/example.md +++ b/docs/example.md @@ -52,3 +52,7 @@ nav: ## focus + +## spinner + + diff --git a/src/InputNumber.tsx b/src/InputNumber.tsx index 8cb3d822..f38b96c9 100644 --- a/src/InputNumber.tsx +++ b/src/InputNumber.tsx @@ -1,3 +1,4 @@ +import { BaseInput } from '@rc-component/input'; import getMiniDecimal, { DecimalClass, getNumberPrecision, @@ -6,20 +7,20 @@ import getMiniDecimal, { validateNumber, ValueType, } from '@rc-component/mini-decimal'; -import { clsx } from 'clsx'; -import { BaseInput } from '@rc-component/input'; import { useLayoutUpdateEffect } from '@rc-component/util/lib/hooks/useLayoutEffect'; import proxyObject from '@rc-component/util/lib/proxyObject'; import { composeRef } from '@rc-component/util/lib/ref'; +import { clsx } from 'clsx'; import * as React from 'react'; import useCursor from './hooks/useCursor'; +import SemanticContext from './SemanticContext'; import StepHandler from './StepHandler'; import { getDecupleSteps } from './utils/numberUtil'; -import SemanticContext from './SemanticContext'; import type { HolderRef } from '@rc-component/input/lib/BaseInput'; import { BaseInputProps } from '@rc-component/input/lib/interface'; import { InputFocusOptions, triggerFocus } from '@rc-component/input/lib/utils/commonUtils'; +import { useEvent } from '@rc-component/util'; import useFrame from './hooks/useFrame'; export type { ValueType }; @@ -54,7 +55,7 @@ const getDecimalIfValidate = (value: ValueType) => { return decimal.isInvalidate() ? null : decimal; }; -type SemanticName = 'actions' | 'input'; +type SemanticName = 'actions' | 'input' | 'action'; export interface InputNumberProps extends Omit< React.InputHTMLAttributes, @@ -63,6 +64,8 @@ export interface InputNumberProps /** value will show as string */ stringMode?: boolean; + type?: 'input' | 'spinner'; + defaultValue?: T; value?: T | null; @@ -119,6 +122,7 @@ type InternalInputNumberProps = Omit & { const InternalInputNumber = React.forwardRef( (props: InternalInputNumberProps, ref: React.Ref) => { const { + type, prefixCls, className, style, @@ -154,6 +158,8 @@ const InternalInputNumber = React.forwardRef( ...inputProps } = props; + const { classNames, styles } = React.useContext(SemanticContext) || {}; + const inputClassName = `${prefixCls}-input`; const inputRef = React.useRef(null); @@ -435,7 +441,7 @@ const InternalInputNumber = React.forwardRef( }; // ============================= Step ============================= - const onInternalStep = (up: boolean, emitter: 'handler' | 'keyboard' | 'wheel') => { + const onInternalStep = useEvent((up: boolean, emitter: 'handler' | 'keyboard' | 'wheel') => { // Ignore step since out of range if ((up && upDisabled) || (!up && downDisabled)) { return; @@ -461,7 +467,7 @@ const InternalInputNumber = React.forwardRef( }); inputRef.current?.focus(); - }; + }); // ============================ Flush ============================= /** @@ -586,6 +592,35 @@ const InternalInputNumber = React.forwardRef( }, [inputValue]); // ============================ Render ============================ + // >>>>>> Handler + const sharedHandlerProps = { + prefixCls, + onStep: onInternalStep, + className: classNames?.action, + style: styles?.action, + }; + + const upNode = ( + + {upHandler} + + ); + + const downNode = ( + + {downHandler} + + ); + + // >>>>>> Render return (
- {controls && ( - + {type === 'input' && controls && ( +
+ {upNode} + {downNode} +
)} + + {type === 'spinner' && controls && downNode} +
+ + {type === 'spinner' && controls && upNode}
); }, @@ -641,6 +680,7 @@ const InternalInputNumber = React.forwardRef( const InputNumber = React.forwardRef((props, ref) => { const { + type = 'input', disabled, style, prefixCls = 'rc-input-number', @@ -675,7 +715,7 @@ const InputNumber = React.forwardRef((props, r return ( ((props, r ref={holderRef} > void; } export default function StepHandler({ prefixCls, - upNode, - downNode, - upDisabled, - downDisabled, + action, + children, + disabled, + className, + style, onStep, }: StepHandlerProps) { + // ======================== MISC ======================== + const isUpAction = action === 'up'; + // ======================== Step ======================== const stepTimeoutRef = React.useRef(); const frameIds = React.useRef([]); - const onStepRef = React.useRef(); - onStepRef.current = onStep; - - const { classNames, styles } = React.useContext(SemanticContext) || {}; - const onStopStep = () => { clearTimeout(stepTimeoutRef.current); }; // We will interval update step when hold mouse down - const onStepMouseDown = (e: React.MouseEvent, up: boolean) => { + const onStepMouseDown = (e: React.MouseEvent) => { e.preventDefault(); onStopStep(); - onStepRef.current(up, 'handler'); + onStep(isUpAction, 'handler'); // Loop step for interval function loopStep() { - onStepRef.current(up, 'handler'); + onStep(isUpAction, 'handler'); stepTimeoutRef.current = setTimeout(loopStep, STEP_INTERVAL); } @@ -75,12 +74,14 @@ export default function StepHandler({ // ======================= Render ======================= const handlerClassName = `${prefixCls}-handler`; - const upClassName = clsx(handlerClassName, `${handlerClassName}-up`, { - [`${handlerClassName}-up-disabled`]: upDisabled, - }); - const downClassName = clsx(handlerClassName, `${handlerClassName}-down`, { - [`${handlerClassName}-down-disabled`]: downDisabled, - }); + const mergedClassName = clsx( + handlerClassName, + `${handlerClassName}-${action}`, + { + [`${handlerClassName}-${action}-disabled`]: disabled, + }, + className + ); // fix: https://github.com/ant-design/ant-design/issues/43088 // In Safari, When we fire onmousedown and onmouseup events in quick succession, @@ -97,29 +98,17 @@ export default function StepHandler({ }; return ( -
- { - onStepMouseDown(e, true); - }} - aria-label="Increase Value" - aria-disabled={upDisabled} - className={upClassName} - > - {upNode || } - - { - onStepMouseDown(e, false); - }} - aria-label="Decrease Value" - aria-disabled={downDisabled} - className={downClassName} - > - {downNode || } - -
+ { + onStepMouseDown(e); + }} + aria-label={isUpAction ? 'Increase Value' : 'Decrease Value'} + aria-disabled={disabled} + className={mergedClassName} + style={style} + > + {children || } + ); } diff --git a/tests/__snapshots__/baseInput.test.tsx.snap b/tests/__snapshots__/baseInput.test.tsx.snap index a8e10153..dfc613ff 100644 --- a/tests/__snapshots__/baseInput.test.tsx.snap +++ b/tests/__snapshots__/baseInput.test.tsx.snap @@ -4,7 +4,7 @@ exports[`baseInput addon should render properly 1`] = `

{ - it('max', () => { const onChange = jest.fn(); const { container } = render(); @@ -61,7 +60,6 @@ describe('InputNumber.Props', () => { expect(input).toHaveFocus(); done(); }, 500); - }); describe('step', () => { @@ -96,8 +94,8 @@ describe('InputNumber.Props', () => { , ); @@ -114,8 +112,8 @@ describe('InputNumber.Props', () => { , ); @@ -186,7 +184,7 @@ describe('InputNumber.Props', () => { return (