Skyroc Native UI

Stepper

数值步进器,支持长按连续触发与变更拦截

步进器(Stepper)由「减 + 输入框 + 加」三段组成,用于小幅度调整数量。加减按钮复用 Buttonsize="icon" + variant="ghost"),中间是一个受组件托管的 TextInput。值走 useControllableState,受控与非受控共用一条路径。

import { Stepper } from '@skyroc/native-ui';

基础用法

默认值为 1、最小值为 1、最大值为 Number.MAX_SAFE_INTEGER、步长为 1。传 value + onChange 即为受控。

StepperBasic.tsx
Loading…
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 };

注意默认 min1 而不是 0 —— 组件按「购物车数量」这个最常见场景取的默认值,需要归零请显式传 min={0}

何时使用

  • 数量、份数这类小范围整数调整(购物车、点单、打印份数)。
  • 范围大或不要求精确时用 Slider;需要任意数值输入时用 InputkeyboardType

尺寸

size 同时决定按钮边长、输入框宽高与字号:

尺寸按钮输入框(宽 × 高)数字字号加减符号
sm28 × 2840 × 28--text-xstext-base
md32 × 3256 × 32--text-smtext-lg
lg40 × 4064 × 40--text-basetext-xl
StepperSize.tsx
Loading…
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)+ 无底色输入框
StepperTheme.tsx
Loading…
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 而不是继续步进。

StepperRange.tsx
Loading…
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 始终成立。

StepperDecimal.tsx
Loading…
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)

StepperEvents.tsx
Loading…
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 / measureshowInput={false} 时它始终是 null)。编辑过程中 onChangeText 反馈的是原始文本,值只在失焦时提交并归一化 —— 这样用户输到一半的 1. 不会被立刻改写。

StepperInputEvents.tsx
Loading…
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 分别禁用三个部分。

StepperDisabled.tsx
Loading…
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 控制三个部分是否渲染,可以拼出「只读数量 + 加号」这类形态。

StepperVisibility.tsx
Loading…
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,组件会把这一次吞掉,不会多走一步。

StepperLongPress.tsx
Loading…
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)即拒绝本次变化。它可以是异步的:长按期间如果上一次校验还没落定,本次触发会被跳过,避免读到同一个基准值连跳。

StepperBeforeChange.tsx
Loading…
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)在任何配置下都会回滚。

StepperEmpty.tsx
Loading…
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
StepperStyles.tsx
Loading…
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 回流后才派生,界面与值不会脱节。

StepperControlled.tsx
Loading…
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 的属性(accessibilityLabelplaceholdermaxLengthtestID 等),其中 defaultValuevalueonChangeeditablestyle 已被移除或由组件接管。透传属性展开在组件预置值之后,因此 keyboardTypetextAlignselectTextOnFocusallowFontScaling 是可以覆盖的。

属性说明类型默认值
value当前值(受控)number-
defaultValue非受控初始值number1
onChange值变化回调(value: number) => void-
min最小值number1
max最大值numberNumber.MAX_SAFE_INTEGER
step每次加减的数值number1
size尺寸,决定按钮边长、输入框宽高与字号'sm' | 'md' | 'lg''md'
theme外观主题,default 为连体式,round 为分离圆钮'default' | 'round''default'
integer只允许整数,同时把键盘换成 number-padbooleanfalse
decimalLength固定小数位数,0 表示取整到个位number-
longPress长按连续触发:600ms 后每 150ms 步进一次booleantrue
beforeChange值变化前的拦截器,返回 false 阻止变化;长按期间上一次未结束时跳过本次(value: number) => boolean | Promise<boolean>-
allowEmpty允许输入框失焦后保持为空booleanfalse
autoFixed失焦时自动修正超范围的值;关闭后保留原始文本,既不修正也不提交booleantrue
disabled禁用整个组件booleanfalse
disableInput仅禁用输入框booleanfalse
disableMinus仅禁用减少按钮booleanfalse
disablePlus仅禁用增加按钮booleanfalse
showInput是否渲染输入框booleantrue
showMinus是否渲染减少按钮booleantrue
showPlus是否渲染增加按钮booleantrue
onMinus点击减少按钮且值会变化时触发() => void-
onPlus点击增加按钮且值会变化时触发() => void-
onOverlimit已在边界仍点击(或长按越界)时触发,此时不触发 onMinus / onPlus(type: 'minus' | 'plus') => void-
className根容器类名,合并在 classNames.root 之后string-
classNames各 slot 的类名覆盖,见「样式覆盖」一节SlotClassNames<StepperSlots>-
ref底层 TextInput 的 ref;showInput 为 false 时始终为 nullRef<TextInput>-

类型

import type { StepperProps, StepperSize, StepperSlots, StepperStepType, StepperTheme } from '@skyroc/native-ui';

StepperTheme

外观主题,default 为连体式,round 为分离圆钮。

'default' | 'round'

StepperSize

尺寸预设,决定按钮边长、输入框宽高与字号。

'sm' | 'md' | 'lg'

StepperStepType

步进方向,onOverlimit 用它区分是哪一侧越界。

'minus' | 'plus'

StepperSlots

可通过 classNames 覆盖的 slot 名称。

'input' | 'minus' | 'minusIcon' | 'plus' | 'plusIcon' | 'root'

SlotClassNames

classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 StepperSlots。

Partial<Record<Slots, string>>

包内还导出了 stepperVariantsStepperVariantProps