Stepper
数值步进器,支持长按连续触发与变更拦截
步进器(Stepper)由「减 + 输入框 + 加」三段组成,用于小幅度调整数量。加减按钮复用 Button(size="icon" + variant="ghost"),中间是一个受组件托管的 TextInput。值走 useControllableState,受控与非受控共用一条路径。
import { Stepper } from '@skyroc/native-ui';基础用法
默认值为 1、最小值为 1、最大值为 Number.MAX_SAFE_INTEGER、步长为 1。传 value + onChange 即为受控。
import { Stepper, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
const StepperBasic = () => {
const [basic, setBasic] = useState(3);
return (
<View className="bg-background px-6">
<View className="mb-8 gap-2">
<Stepper
value={basic}
onChange={setBasic}
/>
<Text color="muted">当前值:{basic}</Text>
</View>
</View>
);
};
export { StepperBasic };注意默认 min 是 1 而不是 0 —— 组件按「购物车数量」这个最常见场景取的默认值,需要归零请显式传 min={0}。
何时使用
- 数量、份数这类小范围整数调整(购物车、点单、打印份数)。
- 范围大或不要求精确时用
Slider;需要任意数值输入时用Input配keyboardType。
尺寸
size 同时决定按钮边长、输入框宽高与字号:
| 尺寸 | 按钮 | 输入框(宽 × 高) | 数字字号 | 加减符号 |
|---|---|---|---|---|
sm | 28 × 28 | 40 × 28 | --text-xs | text-base |
md | 32 × 32 | 56 × 32 | --text-sm | text-lg |
lg | 40 × 40 | 64 × 40 | --text-base | text-xl |
import { Stepper, Text } from '@skyroc/native-ui';
import type { StepperSize as StepperSizeToken } from '@skyroc/native-ui';
import { View } from 'react-native';
const SIZES: StepperSizeToken[] = ['sm', 'md', 'lg'];
const StepperSize = () => {
return (
<View className="bg-background px-6">
<View className="mb-8 gap-3">
{SIZES.map(size => (
<View
key={size}
className="flex-row items-center gap-3"
>
<Stepper
defaultValue={2}
size={size}
/>
<Text color="muted">{size}</Text>
</View>
))}
</View>
</View>
);
};
export { StepperSize };数字字号用 text-(length:--text-*) 而不是 text-sm 简写:简写会连带输出 line-height,Uniwind 换算成绝对行高传给 RN 后,iOS 会把多出的空间全加在文字上方,表现为输入框里的数字偏下。
外观主题
| 主题 | 表现 |
|---|---|
default | 连体式:三段贴合成一条,两端圆角,整体 bg-muted |
round | 分离式:两枚圆钮(加号为 bg-primary)+ 无底色输入框 |
import { Stepper, Text } from '@skyroc/native-ui';
import type { StepperTheme as StepperThemeToken } from '@skyroc/native-ui';
import { View } from 'react-native';
const THEMES: StepperThemeToken[] = ['default', 'round'];
const StepperTheme = () => {
return (
<View className="bg-background px-6">
<View className="mb-8 gap-3">
{THEMES.map(theme => (
<View
key={theme}
className="flex-row items-center gap-3"
>
<Stepper
defaultValue={2}
theme={theme}
/>
<Text color="muted">{theme}</Text>
</View>
))}
</View>
</View>
);
};
export { StepperTheme };步长与范围
min / max 限定边界,step 决定每次加减的量。输入框失焦时同样会把值夹回范围。到达边界后对应按钮降到 50% 不透明度,但仍然可点 —— 点击时触发 onOverlimit 而不是继续步进。
import { Stepper, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
const StepperRange = () => {
const [ranged, setRanged] = useState(4);
return (
<View className="bg-background px-6">
<View className="mb-8 gap-2">
<Stepper
max={10}
min={2}
step={2}
value={ranged}
onChange={setRanged}
/>
<Text color="muted">当前值:{ranged}</Text>
</View>
</View>
);
};
export { StepperRange };数值精度
decimalLength 固定小数位(0 表示强制取整到个位),integer 只允许整数。未指定 decimalLength 时按数值自身的小数位收敛,既能抹掉浮点累加的尾数(0.1 + 0.2),又不会截掉用户输入的精度。归一化顺序是先定精度、再夹边界,保证 min / max 始终成立。
import { Stepper, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
const StepperDecimal = () => {
const [decimal, setDecimal] = useState(0.1);
return (
<View className="bg-background px-6">
<Text
className="mb-2"
color="muted"
>
step 0.1 不设 decimalLength,连加也不会出现 0.30000000000000004 这类浮点尾数
</Text>
<View className="mb-4 gap-2">
<Stepper
max={3}
min={0}
step={0.1}
value={decimal}
onChange={setDecimal}
/>
<Text color="muted">当前值:{String(decimal)}</Text>
</View>
<Text
className="mb-2"
color="muted"
>
decimalLength 固定小数位,0 表示归整到个位(输入 3.7 失焦后变 4)
</Text>
<View className="mb-4 flex-row items-center gap-3">
<Stepper
decimalLength={2}
defaultValue={1.5}
max={10}
min={0}
step={0.25}
/>
<Stepper
decimalLength={0}
defaultValue={3}
max={100}
min={0}
/>
</View>
<Text
className="mb-2"
color="muted"
>
integer 只允许整数,输入 4.6 失焦后取整为 5,键盘也切成纯数字
</Text>
<View className="mb-8">
<Stepper
integer
defaultValue={3}
max={10}
min={0}
/>
</View>
</View>
);
};
export { StepperDecimal };integer 还会把键盘类型从 decimal-pad 换成 number-pad。
按钮事件
onMinus / onPlus 在点击对应按钮且值真的会变化时触发;已经在边界上还继续点,只触发 onOverlimit(type)。
import { Stepper, Text } from '@skyroc/native-ui';
import type { StepperStepType } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
const StepperEvents = () => {
const [tip, setTip] = useState('等待操作');
function handleOverlimit(type: StepperStepType) {
setTip(type === 'minus' ? 'onOverlimit:已到最小值' : 'onOverlimit:已到最大值');
}
function handleMinus() {
setTip('onMinus:减少一步');
}
function handlePlus() {
setTip('onPlus:增加一步');
}
return (
<View className="bg-background px-6">
<View className="mb-8 gap-2">
<Stepper
max={3}
min={1}
onMinus={handleMinus}
onOverlimit={handleOverlimit}
onPlus={handlePlus}
/>
<Text color="muted">{tip}</Text>
</View>
</View>
);
};
export { StepperEvents };输入框事件与 ref
ref 指向底层 TextInput,可以 focus / blur / measure(showInput={false} 时它始终是 null)。编辑过程中 onChangeText 反馈的是原始文本,值只在失焦时提交并归一化 —— 这样用户输到一半的 1. 不会被立刻改写。
import { Button, Stepper, Text } from '@skyroc/native-ui';
import { useRef, useState } from 'react';
import type { TextInput } from 'react-native';
import { View } from 'react-native';
const StepperInputEvents = () => {
const inputRef = useRef<TextInput>(null);
const [lastEvent, setLastEvent] = useState('尚未触发');
function blurInput() {
inputRef.current?.blur();
}
function focusInput() {
inputRef.current?.focus();
}
return (
<View className="gap-3 bg-background px-6 pb-6">
<Stepper
ref={inputRef}
accessibilityLabel="可编辑数量"
defaultValue={2}
max={99}
min={0}
onBlur={() => setLastEvent('onBlur:输入已提交')}
onChangeText={text => setLastEvent(`onChangeText:${text || '(空)'}`)}
/>
<Text color="muted">最近事件:{lastEvent}</Text>
<View className="flex-row gap-2">
<Button
size="sm"
variant="outline"
onPress={focusInput}
>
聚焦输入框
</Button>
<Button
size="sm"
variant="outline"
onPress={blurInput}
>
失焦并提交
</Button>
</View>
</View>
);
};
export { StepperInputEvents };禁用
disabled 禁用整体;也可以用 disableInput / disableMinus / disablePlus 分别禁用三个部分。
import { Stepper, Text } from '@skyroc/native-ui';
import { View } from 'react-native';
const StepperDisabled = () => {
return (
<View className="bg-background px-6">
<View className="mb-8 gap-3">
<View className="flex-row items-center gap-3">
<Stepper
disabled
defaultValue={2}
/>
<Text color="muted">disabled</Text>
</View>
<View className="flex-row items-center gap-3">
<Stepper
disableInput
defaultValue={2}
/>
<Text color="muted">disableInput</Text>
</View>
<View className="flex-row items-center gap-3">
<Stepper
disableMinus
defaultValue={2}
/>
<Text color="muted">disableMinus</Text>
</View>
<View className="flex-row items-center gap-3">
<Stepper
disablePlus
defaultValue={2}
/>
<Text color="muted">disablePlus</Text>
</View>
</View>
</View>
);
};
export { StepperDisabled };按需显示
showInput / showMinus / showPlus 控制三个部分是否渲染,可以拼出「只读数量 + 加号」这类形态。
import { Stepper, Text } from '@skyroc/native-ui';
import { View } from 'react-native';
const StepperVisibility = () => {
return (
<View className="bg-background px-6">
<View className="mb-8 gap-3">
<View className="flex-row items-center gap-3">
<Stepper
defaultValue={2}
showInput={false}
/>
<Text color="muted">showInput=false</Text>
</View>
<View className="flex-row items-center gap-3">
<Stepper
defaultValue={2}
showMinus={false}
/>
<Text color="muted">showMinus=false</Text>
</View>
<View className="flex-row items-center gap-3">
<Stepper
defaultValue={2}
showPlus={false}
/>
<Text color="muted">showPlus=false</Text>
</View>
</View>
</View>
);
};
export { StepperVisibility };长按连续触发
longPress 默认开启:按住 600ms 后进入连续触发,之后每 150ms 步进一次,到边界自动停止。长按结束时系统仍会补发一次 press,组件会把这一次吞掉,不会多走一步。
import { Stepper, Text } from '@skyroc/native-ui';
import { View } from 'react-native';
const StepperLongPress = () => {
return (
<View className="bg-background px-6">
<View className="mb-8 gap-3">
<View className="flex-row items-center gap-3">
<Stepper
defaultValue={10}
max={999}
/>
<Text color="muted">默认开启</Text>
</View>
<View className="flex-row items-center gap-3">
<Stepper
defaultValue={10}
longPress={false}
max={999}
/>
<Text color="muted">longPress=false</Text>
</View>
</View>
</View>
);
};
export { StepperLongPress };变化前拦截
beforeChange 在值提交前调用,返回 false(或 resolve 为 false)即拒绝本次变化。它可以是异步的:长按期间如果上一次校验还没落定,本次触发会被跳过,避免读到同一个基准值连跳。
import { Stepper, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
/** BeforeChange 模拟异步校验的耗时 */
const GUARD_DELAY = 600;
/** 异步校验放行的上限 */
const GUARD_MAX = 5;
const StepperBeforeChange = () => {
const [guarded, setGuarded] = useState(3);
const [guardPending, setGuardPending] = useState(false);
async function handleBeforeChange(next: number) {
setGuardPending(true);
await new Promise(resolve => {
setTimeout(resolve, GUARD_DELAY);
});
setGuardPending(false);
return next <= GUARD_MAX;
}
return (
<View className="bg-background px-6">
<Text
className="mb-2"
color="muted"
>
beforeChange 异步校验 {GUARD_DELAY}ms,超过 {GUARD_MAX} 一律拒绝;校验期间长按不会连跳
</Text>
<View className="mb-8 gap-2">
<Stepper
beforeChange={handleBeforeChange}
max={10}
min={0}
value={guarded}
onChange={setGuarded}
/>
<Text color="muted">{guardPending ? '校验中…' : `当前值:${guarded}`}</Text>
</View>
</View>
);
};
export { StepperBeforeChange };空值与自动修正
allowEmpty:失焦时允许输入框保持为空,否则空串会回滚成当前值。autoFixed(默认true):失焦时把超范围的值修正回边界;设为false则保留用户输入的原始文本,既不修正也不提交。
非法输入(NaN)在任何配置下都会回滚。
import { Stepper, Text } from '@skyroc/native-ui';
import { View } from 'react-native';
const StepperEmpty = () => {
return (
<View className="bg-background px-6">
<View className="mb-8 gap-3">
<View className="flex-row items-center gap-3">
<Stepper
allowEmpty
defaultValue={2}
min={0}
/>
<Text color="muted">allowEmpty</Text>
</View>
<View className="flex-row items-center gap-3">
<Stepper
autoFixed={false}
defaultValue={2}
max={9}
min={1}
/>
<Text color="muted">autoFixed=false</Text>
</View>
</View>
</View>
);
};
export { StepperEmpty };样式覆盖
className 追加到根容器上,classNames 按 slot 细粒度覆盖:
| slot | 作用位置 |
|---|---|
root | 根容器 View(横向排布与三段之间的间距) |
minus | 减少按钮容器 |
minusIcon | 减号文字(通过 Button 的 classNames.text) |
plus | 增加按钮容器 |
plusIcon | 加号文字 |
input | 中间的 TextInput |
import { Stepper } from '@skyroc/native-ui';
import { View } from 'react-native';
const StepperStyles = () => {
return (
<View className="bg-background px-6">
<View className="mb-8 gap-3">
<Stepper
className="self-start rounded-lg bg-secondary p-2"
defaultValue={2}
/>
<Stepper
classNames={{
input: 'bg-transparent text-primary',
minus: 'rounded-full bg-primary-100',
minusIcon: 'text-primary',
plus: 'rounded-full bg-primary',
plusIcon: 'text-primary-foreground',
root: 'gap-2'
}}
defaultValue={2}
/>
</View>
</View>
);
};
export { StepperStyles };className 排在 classNames.root 之后参与合并,冲突时 className 优先。
外部控制
受控模式下父级拒绝更新时,组件不会自行改变显示值 —— 提交只写 value,显示态等 value 回流后才派生,界面与值不会脱节。
import { Button, Stepper } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
const StepperControlled = () => {
const [controlled, setControlled] = useState(2);
return (
<View className="bg-background px-6">
<View className="mb-8 gap-3">
<Stepper
max={20}
min={0}
value={controlled}
onChange={setControlled}
/>
<View className="flex-row gap-2">
<Button
color="primary"
variant="outline"
onPress={() => setControlled(Math.max(0, controlled - 5))}
>
-5
</Button>
<Button
color="primary"
variant="outline"
onPress={() => setControlled(Math.min(20, controlled + 5))}
>
+5
</Button>
<Button
color="primary"
variant="ghost"
onPress={() => setControlled(0)}
>
重置
</Button>
</View>
</View>
</View>
);
};
export { StepperControlled };API
Stepper
除下表外,Stepper 透传 TextInput 的属性(accessibilityLabel、placeholder、maxLength、testID 等),其中 defaultValue、value、onChange、editable、style 已被移除或由组件接管。透传属性展开在组件预置值之后,因此 keyboardType、textAlign、selectTextOnFocus、allowFontScaling 是可以覆盖的。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| value | 当前值(受控) | number | - |
| defaultValue | 非受控初始值 | number | 1 |
| onChange | 值变化回调 | (value: number) => void | - |
| min | 最小值 | number | 1 |
| max | 最大值 | number | Number.MAX_SAFE_INTEGER |
| step | 每次加减的数值 | number | 1 |
| size | 尺寸,决定按钮边长、输入框宽高与字号 | 'sm' | 'md' | 'lg' | 'md' |
| theme | 外观主题,default 为连体式,round 为分离圆钮 | 'default' | 'round' | 'default' |
| integer | 只允许整数,同时把键盘换成 number-pad | boolean | false |
| decimalLength | 固定小数位数,0 表示取整到个位 | number | - |
| longPress | 长按连续触发:600ms 后每 150ms 步进一次 | boolean | true |
| beforeChange | 值变化前的拦截器,返回 false 阻止变化;长按期间上一次未结束时跳过本次 | (value: number) => boolean | Promise<boolean> | - |
| allowEmpty | 允许输入框失焦后保持为空 | boolean | false |
| autoFixed | 失焦时自动修正超范围的值;关闭后保留原始文本,既不修正也不提交 | boolean | true |
| disabled | 禁用整个组件 | boolean | false |
| disableInput | 仅禁用输入框 | boolean | false |
| disableMinus | 仅禁用减少按钮 | boolean | false |
| disablePlus | 仅禁用增加按钮 | boolean | false |
| showInput | 是否渲染输入框 | boolean | true |
| showMinus | 是否渲染减少按钮 | boolean | true |
| showPlus | 是否渲染增加按钮 | boolean | true |
| onMinus | 点击减少按钮且值会变化时触发 | () => void | - |
| onPlus | 点击增加按钮且值会变化时触发 | () => void | - |
| onOverlimit | 已在边界仍点击(或长按越界)时触发,此时不触发 onMinus / onPlus | (type: 'minus' | 'plus') => void | - |
| className | 根容器类名,合并在 classNames.root 之后 | string | - |
| classNames | 各 slot 的类名覆盖,见「样式覆盖」一节 | SlotClassNames<StepperSlots> | - |
| ref | 底层 TextInput 的 ref;showInput 为 false 时始终为 null | Ref<TextInput> | - |
类型
import type { StepperProps, StepperSize, StepperSlots, StepperStepType, StepperTheme } from '@skyroc/native-ui';StepperTheme
外观主题,default 为连体式,round 为分离圆钮。
StepperSize
尺寸预设,决定按钮边长、输入框宽高与字号。
StepperStepType
步进方向,onOverlimit 用它区分是哪一侧越界。
StepperSlots
可通过 classNames 覆盖的 slot 名称。
SlotClassNames
classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 StepperSlots。
包内还导出了 stepperVariants 与 StepperVariantProps。