orn-ui
Organism

OptionWheel

Requiere <UIProvider> — Primeros pasos →

Instalación

Instala solo este componente (copia el código fuente a tu proyecto, sin dependencia npm)

npx orn-ui add option-wheel

O instala el paquete completo e impórtalo

pnpm add orn-ui
import { OptionWheel } from 'orn-ui/option-wheel';
Depende de:core (theme + icons)transition

Uso

  • cuándo usarlo — Un rango largo y ordenado donde importan los vecinos: minutos, un peso, un talle, un año. La opción de la ventana del centro es la elegida — no hay paso de confirmar. Para una lista corta de opciones sin relación está Select, y para un puñado, SegmentedControl.
  • options / selectedValue / onSelect — Controlado y genérico, el mismo patrón que Select: el valor conserva su tipo, así que una rueda de números devuelve un número.
  • visibleCount / itemHeight — Cuánto del rango entra en pantalla. visibleCount se fuerza al impar siguiente para que haya una fila que sea el centro, y itemHeight es lo que hace que la rueda entre en una card o llene una hoja.
  • variant — 'window' marca la elección con una banda detrás de la fila del centro — la lectura de iOS. 'spotlight' saca la banda y en su lugar apaga las filas lejanas, que va mejor sobre un fondo oscuro o con imagen, donde la banda parece una costura.
  • curveRadius / curveFrom — Arquea las filas sobre una rueda cuyo centro está corrido hacia un lado, así se van curvando hacia adentro a medida que se alejan del medio. Es el adorno, no el mecanismo — sin eso la rueda funciona igual.
  • accessibilityLabel — Obligatorio cuando no hay label: la rueda entera necesita un nombre, porque sus filas son números y un lector de pantalla diciendo "15, 20, 25" no dice de qué son.
  • textColor — La rueda no pinta su propio fondo — lo pinta quien la compone. Éste es el modo de que el texto siga legible arriba de lo que sea que resulte.
Clip del demo todavía sin grabar — ver MEDIA.md

Variantes

default — five rows, the centre one is the pick

<SizeWheel label="Size" options={SIZES} />

disabled options — the wheel slides off them on its own

<SizeWheel label="In stock" options={STOCK} />

unit and visibleCount={3} — a compact wheel

<MinuteWheel />

perspective={false} — size and fade, no tilt

<FlatWheel />

disabled — the whole wheel is frozen

<LockedWheel />

inside a Card, named for screen readers

<InCard />

variant='spotlight' — no band, the sharp row is the pick

<PortfolioWheel />
Código completo del demo
function PortfolioWheel() {
  const [bucket, setBucket] = useState('stocks');
  const colors = useColors();
  const current = PORTFOLIO.find((option) => option.value === bucket);

  return (
    <View style={{ gap: 8 }}>
      <View
        style={{
          backgroundColor: colors.surface,
          borderRadius: 24,
          borderWidth: 1,
          borderColor: colors.border,
          overflow: 'hidden',
          paddingVertical: 12,
        }}
      >
        <OptionWheel
          accessibilityLabel="Portfolio bucket"
          options={PORTFOLIO}
          selectedValue={bucket}
          onSelect={setBucket}
          variant="spotlight"
          textColor={colors.text}
          curveRadius={CURVE}
          perspective={false}
          visibleCount={ROWS}
          itemHeight={ROW}
        />
      </View>
      <Body>Allocating to: {current?.label ?? '—'}</Body>
    </View>
  );
}

function SizeWheel({ label, options }: { label: string; options: OptionWheelOption<string>[] }) {
  const [size, setSize] = useState('m');
  const current = options.find((option) => option.value === size);

  return (
    <View style={{ gap: 8 }}>
      <OptionWheel label={label} options={options} selectedValue={size} onSelect={setSize} />
      <Body>Picked: {current?.label ?? '—'}</Body>
    </View>
  );
}

function MinuteWheel() {
  const [minutes, setMinutes] = useState(15);

  return (
    <View style={{ gap: 8 }}>
      <OptionWheel
        label="Remind me in"
        options={MINUTES}
        selectedValue={minutes}
        onSelect={setMinutes}
        unit="min"
        visibleCount={3}
      />
      <Body>Reminder in {minutes} minutes</Body>
    </View>
  );
}

function FlatWheel() {
  const [size, setSize] = useState('l');
  const current = SIZES.find((option) => option.value === size);

  return (
    <View style={{ gap: 8 }}>
      <OptionWheel
        label="No tilt"
        options={SIZES}
        selectedValue={size}
        onSelect={setSize}
        perspective={false}
      />
      <Body>Flat pick: {current?.label ?? '—'}</Body>
    </View>
  );
}

function LockedWheel() {
  const [size, setSize] = useState('m');
  const current = SIZES.find((option) => option.value === size);

  return (
    <View style={{ gap: 8 }}>
      <OptionWheel
        label="Locked"
        options={SIZES}
        selectedValue={size}
        onSelect={setSize}
        disabled
      />
      <Body>Locked on: {current?.label ?? '—'}</Body>
    </View>
  );
}

function InCard() {
  const [size, setSize] = useState('s');
  const current = SIZES.find((option) => option.value === size);

  return (
    <Card>
      <Subtitle>Bag size</Subtitle>
      <Body style={{ marginTop: 4, marginBottom: 12 }}>Ships free over 2 kg</Body>
      <OptionWheel
        accessibilityLabel="Bag size"
        options={SIZES}
        selectedValue={size}
        onSelect={setSize}
        visibleCount={3}
      />
      <Body style={{ marginTop: 8 }}>Bag: {current?.label ?? '—'}</Body>
    </Card>
  );
}

export function OptionWheelDemo() {
  const variants: VariantDef[] = [
    {
      label: 'default — five rows, the centre one is the pick',
      content: <SizeWheel label="Size" options={SIZES} />,
    },
    {
      label: 'disabled options — the wheel slides off them on its own',
      content: <SizeWheel label="In stock" options={STOCK} />,
    },
    {
      label: 'unit and visibleCount={3} — a compact wheel',
      content: <MinuteWheel />,
    },
    {
      label: 'perspective={false} — size and fade, no tilt',
      content: <FlatWheel />,
    },
    {
      label: 'disabled — the whole wheel is frozen',
      content: <LockedWheel />,
    },
    {
      label: 'inside a Card, named for screen readers',
      content: <InCard />,
    },
    {
      label: "variant='spotlight' — no band, the sharp row is the pick",
      content: <PortfolioWheel />,
    },
  ];
  return <VariantList variants={variants} />;

Props

Además acepta testID, que se reenvía al nodo raíz. Existe para los tests end-to-end (Maestro maneja la app por el árbol de accesibilidad) y no cambia en nada cómo se ve ni cómo se comporta el componente.

NombreTipoPor defectoDescripción
optionsOptionWheelOption<T>[]El rango, en orden. Cada opción lleva una etiqueta, su valor, y puede estar deshabilitada.
selectedValuestring | numberLa opción de la ventana del centro.
onSelect(value: T) => voidAvisa la opción en la que quedó la rueda.
label?stringTítulo arriba de la rueda.
accessibilityLabel?stringObligatorio cuando no hay `label`: la rueda entera necesita un nombre.
visibleCount?number5Filas en pantalla. Se fuerza al impar siguiente para que una de ellas sea el centro.
itemHeight?number44Alto de cada fila.
unit?stringTexto fijo a la derecha de la fila del centro ('kg', 'min').
perspective?booleantrueInclina las filas lejanas para sugerir un cilindro.
variant?'window' | 'spotlight'window'window' marca la elección con una banda detrás de la fila del centro. 'spotlight' saca la banda y deja que las filas lejanas se apaguen.
textColor?stringtheme.colors.textColor de las etiquetas. El fondo sobre el que se apoya la rueda lo pinta quien la compone — éste es el modo de que el texto siga legible.
curveRadius?numberRadio, en píxeles, de la rueda sobre la que se apoyan las filas. Su centro está corrido hacia un lado, así que las filas se arquean hacia adentro a medida que se alejan del medio.
curveFrom?'left' | 'right'leftDe qué lado está el centro de la rueda.
disabled?booleanfalseImpide girar la rueda.
style?StyleProp<ViewStyle>Estilo del contenedor.