Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions assets/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
78 changes: 78 additions & 0 deletions docs/demo/spinner.tsx
Original file line number Diff line number Diff line change
@@ -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<string | number>(93);

const onChange = (val: number) => {
console.warn('onChange:', val, typeof val);
setValue(val);
};

return (
<div style={{ margin: 10 }}>
<h3>Controlled</h3>
<InputNumber
type="spinner"
aria-label="Simple number input example"
min={-8}
max={10}
style={{ width: 100 }}
value={value}
onChange={onChange}
readOnly={readOnly}
disabled={disabled}
keyboard={keyboard}
changeOnWheel={wheel}
stringMode={stringMode}
/>
<p>
<button type="button" onClick={() => setDisabled(!disabled)}>
toggle Disabled ({String(disabled)})
</button>
<button type="button" onClick={() => setReadOnly(!readOnly)}>
toggle readOnly ({String(readOnly)})
</button>
<button type="button" onClick={() => setKeyboard(!keyboard)}>
toggle keyboard ({String(keyboard)})
</button>
<button type="button" onClick={() => setStringMode(!stringMode)}>
toggle stringMode ({String(stringMode)})
</button>
<button type="button" onClick={() => setWheel(!wheel)}>
toggle wheel ({String(wheel)})
</button>
</p>

<hr />
<h3>Uncontrolled</h3>
<InputNumber
type="spinner"
style={{ width: 100 }}
onChange={onChange}
min={-99}
max={99}
defaultValue={33}
/>

<hr />
<h3>!changeOnBlur</h3>
<InputNumber
type="spinner"
style={{ width: 100 }}
min={-9}
max={9}
defaultValue={10}
onChange={onChange}
changeOnBlur={false}
/>
</div>
);
};
4 changes: 4 additions & 0 deletions docs/example.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,7 @@ nav:
## focus

<code src="./demo/focus.tsx"></code>

## spinner

<code src="./demo/spinner.tsx"></code>
46 changes: 36 additions & 10 deletions src/InputNumber.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export interface InputNumberProps<T extends ValueType = ValueType>
/** value will show as string */
stringMode?: boolean;

type?: 'input' | 'spinner';

defaultValue?: T;
value?: T | null;

Expand Down Expand Up @@ -119,6 +121,7 @@ type InternalInputNumberProps = Omit<InputNumberProps, 'prefix' | 'suffix'> & {
const InternalInputNumber = React.forwardRef(
(props: InternalInputNumberProps, ref: React.Ref<HTMLInputElement>) => {
const {
type,
prefixCls,
className,
style,
Expand Down Expand Up @@ -586,6 +589,23 @@ const InternalInputNumber = React.forwardRef(
}, [inputValue]);

// ============================ Render ============================
const upNode = (
<StepHandler action="up" prefixCls={prefixCls} disabled={upDisabled} onStep={onInternalStep}>
{upHandler}
</StepHandler>
);

const downNode = (
<StepHandler
action="down"
prefixCls={prefixCls}
disabled={downDisabled}
onStep={onInternalStep}
>
{downHandler}
</StepHandler>
);

return (
<div
ref={domRef}
Expand All @@ -607,16 +627,18 @@ const InternalInputNumber = React.forwardRef(
onCompositionEnd={onCompositionEnd}
onBeforeInput={onBeforeInput}
>
{controls && (
<StepHandler
prefixCls={prefixCls}
upNode={upHandler}
downNode={downHandler}
upDisabled={upDisabled}
downDisabled={downDisabled}
onStep={onInternalStep}
/>
{type === 'input' && controls && (
<div
className={clsx(`${prefixCls}-handler-wrap`, props.classNames?.actions)}
style={props.styles?.actions}
>
{upNode}
{downNode}
</div>
)}

{type === 'spinner' && controls && downNode}

<div className={`${inputClassName}-wrap`}>
<input
autoComplete="off"
Expand All @@ -634,13 +656,16 @@ const InternalInputNumber = React.forwardRef(
readOnly={readOnly}
/>
</div>

{type === 'spinner' && controls && upNode}
</div>
);
},
);

const InputNumber = React.forwardRef<InputNumberRef, InputNumberProps>((props, ref) => {
const {
type = 'input',
disabled,
style,
prefixCls = 'rc-input-number',
Expand Down Expand Up @@ -675,7 +700,7 @@ const InputNumber = React.forwardRef<InputNumberRef, InputNumberProps>((props, r
return (
<SemanticContext.Provider value={memoizedValue}>
<BaseInput
className={className}
className={clsx(`${prefixCls}-type-${type}`, className)}
triggerFocus={focus}
prefixCls={prefixCls}
value={value}
Expand All @@ -696,6 +721,7 @@ const InputNumber = React.forwardRef<InputNumberRef, InputNumberProps>((props, r
ref={holderRef}
>
<InternalInputNumber
type={type}
prefixCls={prefixCls}
disabled={disabled}
ref={inputFocusRef}
Expand Down
64 changes: 26 additions & 38 deletions src/StepHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,17 @@ const STEP_DELAY = 600;

export interface StepHandlerProps {
prefixCls: string;
upNode?: React.ReactNode;
downNode?: React.ReactNode;
upDisabled?: boolean;
downDisabled?: boolean;
action: 'up' | 'down';
children?: React.ReactNode;
disabled?: boolean;
onStep: (up: boolean, emitter: 'handler' | 'keyboard' | 'wheel') => void;
}

export default function StepHandler({
prefixCls,
upNode,
downNode,
upDisabled,
downDisabled,
action,
children,
disabled,
onStep,
}: StepHandlerProps) {
// ======================== Step ========================
Expand All @@ -45,15 +43,15 @@ export default function StepHandler({
};

// 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');
onStepRef.current(action === 'up', 'handler');

// Loop step for interval
function loopStep() {
onStepRef.current(up, 'handler');
onStepRef.current(action === 'up', 'handler');

stepTimeoutRef.current = setTimeout(loopStep, STEP_INTERVAL);
}
Expand All @@ -75,11 +73,8 @@ 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 className = clsx(handlerClassName, `${handlerClassName}-up`, {
[`${handlerClassName}-${action}-disabled`]: disabled,
});

// fix: https://github.com/ant-design/ant-design/issues/43088
Expand All @@ -96,30 +91,23 @@ export default function StepHandler({
onMouseLeave: safeOnStopStep,
};

return (
<span
{...sharedHandlerProps}
onMouseDown={(e) => {
onStepMouseDown(e);
}}
Comment on lines +103 to +105
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

在交互处理器中检查 disabled 状态。

disabled=true 时,onMouseDown 处理器仍然会触发 onStepMouseDown,导致用户可以与禁用状态的按钮交互。这违反了 aria-disabled 的语义。

应用此 diff 添加 disabled 检查:

   <span
     {...sharedHandlerProps}
     onMouseDown={(e) => {
-      onStepMouseDown(e);
+      if (!disabled) {
+        onStepMouseDown(e);
+      }
     }}
     aria-label={isUpAction ? 'Increase Value' : 'Decrease Value'}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onMouseDown={(e) => {
onStepMouseDown(e);
}}
onMouseDown={(e) => {
if (!disabled) {
onStepMouseDown(e);
}
}}
🤖 Prompt for AI Agents
In src/StepHandler.tsx around lines 103 to 105, the onMouseDown handler always
calls onStepMouseDown even when the step is disabled; update the handler to
first check the component's disabled state (e.g., props.disabled or
aria-disabled) and bail out early when disabled is true so the click does
nothing and respects aria-disabled semantics; ensure the check uses the same
prop used elsewhere for disabled state and returns without calling
onStepMouseDown when disabled.

aria-label={action === 'up' ? 'Increase Value' : 'Decrease Value'}
aria-disabled={disabled}
className={className}
>
{children || <span unselectable="on" className={`${prefixCls}-handler-${action}-inner`} />}
</span>
);

return (
<div className={clsx(`${handlerClassName}-wrap`, classNames?.actions)} style={styles?.actions}>
<span
{...sharedHandlerProps}
onMouseDown={(e) => {
onStepMouseDown(e, true);
}}
aria-label="Increase Value"
aria-disabled={upDisabled}
className={upClassName}
>
{upNode || <span unselectable="on" className={`${prefixCls}-handler-up-inner`} />}
</span>
<span
{...sharedHandlerProps}
onMouseDown={(e) => {
onStepMouseDown(e, false);
}}
aria-label="Decrease Value"
aria-disabled={downDisabled}
className={downClassName}
>
{downNode || <span unselectable="on" className={`${prefixCls}-handler-down-inner`} />}
</span>
//
</div>
);
}
6 changes: 3 additions & 3 deletions tests/__snapshots__/baseInput.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ exports[`baseInput addon should render properly 1`] = `
<div>
<div>
<div
class="rc-input-group-wrapper"
class="rc-input-group-wrapper rc-input-type-input"
>
<div
class="rc-input-wrapper rc-input-group"
Expand Down Expand Up @@ -64,7 +64,7 @@ exports[`baseInput addon should render properly 1`] = `
<br />
<br />
<div
class="rc-input-group-wrapper"
class="rc-input-group-wrapper rc-input-type-input"
>
<div
class="rc-input-wrapper rc-input-group"
Expand Down Expand Up @@ -128,7 +128,7 @@ exports[`baseInput addon should render properly 1`] = `
exports[`baseInput prefix should render properly 1`] = `
<div>
<div
class="rc-input-affix-wrapper"
class="rc-input-affix-wrapper rc-input-type-input"
>
<span
class="rc-input-prefix"
Expand Down
Loading