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-wheelO instala el paquete completo e impórtalo
pnpm add orn-ui
import { OptionWheel } from 'orn-ui/option-wheel';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.
| Nombre | Tipo | Por defecto | Descripción |
|---|---|---|---|
options | OptionWheelOption<T>[] | — | El rango, en orden. Cada opción lleva una etiqueta, su valor, y puede estar deshabilitada. |
selectedValue | string | number | — | La opción de la ventana del centro. |
onSelect | (value: T) => void | — | Avisa la opción en la que quedó la rueda. |
label? | string | — | Título arriba de la rueda. |
accessibilityLabel? | string | — | Obligatorio cuando no hay `label`: la rueda entera necesita un nombre. |
visibleCount? | number | 5 | Filas en pantalla. Se fuerza al impar siguiente para que una de ellas sea el centro. |
itemHeight? | number | 44 | Alto de cada fila. |
unit? | string | — | Texto fijo a la derecha de la fila del centro ('kg', 'min'). |
perspective? | boolean | true | Inclina 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? | string | theme.colors.text | Color 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? | number | — | Radio, 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' | left | De qué lado está el centro de la rueda. |
disabled? | boolean | false | Impide girar la rueda. |
style? | StyleProp<ViewStyle> | — | Estilo del contenedor. |