Organism
BottomSheet
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 bottom-sheetO instala el paquete completo e impórtalo
pnpm add orn-ui
import { BottomSheet } from 'orn-ui/bottom-sheet';Uso
- cuándo usarlo — Acciones o contenido secundario sobre la pantalla actual, sin salir de ella.
- visible / onClose — Controlado: cierra con el fondo y con el gesto, siempre a través de `onClose`.
- title / footer — Con título aparece el header, con su botón de cerrar. `footerPlacement` decide dónde vive el footer: fijo, al final del scroll, u oculto mientras el teclado está abierto.
- draggable / maxHeight — `draggable={false}` para una hoja que sólo cierra por botón o fondo. `maxHeight` como fracción 0-1 de la pantalla, o en píxeles.
Clip del demo todavía sin grabar — ver MEDIA.md
Variantes
bare — handle only
<>
<Button title="Open sheet" onPress={() => setBare(true)} />
<BottomSheet visible={bare} onClose={() => setBare(false)}>
<Title style={{ marginBottom: 8 }}>Bottom sheet</Title>
<Body>Drag the handle down, or tap outside, to dismiss.</Body>
</BottomSheet>
</>title + footer — a form in a sheet
<>
<Button title="Open form" onPress={() => setForm(true)} />
<BottomSheet
visible={form}
onClose={() => setForm(false)}
title="Add a note"
footer={
<FormActions
primaryLabel="Save"
onPrimaryPress={() => setForm(false)}
secondaryLabel="Cancel"
onSecondaryPress={() => setForm(false)}
/>
}
>
<Input label="Note" placeholder="Anything worth remembering" value={note} onChangeText={setNote} />
</BottomSheet>
</>long content — scrolls up to maxHeight
<>
<Button title="Open long sheet" onPress={() => setLong(true)} />
<BottomSheet visible={long} onClose={() => setLong(false)} title="Twelve rows">
<View style={{ gap: 12 }}>
{LONG.map((row) => (
<Body key={row}>{row}</Body>
))}
</View>
</BottomSheet>
</>footerPlacement="hide-with-keyboard" — the form keeps that row
<>
<Button title="Open long form" onPress={() => setHiding(true)} />
<BottomSheet
visible={hiding}
onClose={() => setHiding(false)}
title="Long form"
footerPlacement="hide-with-keyboard"
footer={
<FormActions
primaryLabel="Save"
onPrimaryPress={() => setHiding(false)}
secondaryLabel="Cancel"
onSecondaryPress={() => setHiding(false)}
/>
}
>
<View style={{ gap: 12 }}>
{['Name', 'Company', 'Email', 'Phone', 'Address'].map((label) => (
<Input key={label} label={label} value={note} onChangeText={setNote} />
))}
</View>
</BottomSheet>
</>draggable={false} + maxHeight={0.4}
<>
<Button title="Open fixed sheet" onPress={() => setFixed(true)} />
<BottomSheet
visible={fixed}
onClose={() => setFixed(false)}
title="No dragging"
draggable={false}
maxHeight={0.4}
>
<Body>Dismiss with the close button or by tapping outside.</Body>
</BottomSheet>
</>Código completo del demo
const variants: VariantDef[] = [
{
label: 'bare — handle only',
content: (
<>
<Button title="Open sheet" onPress={() => setBare(true)} />
<BottomSheet visible={bare} onClose={() => setBare(false)}>
<Title style={{ marginBottom: 8 }}>Bottom sheet</Title>
<Body>Drag the handle down, or tap outside, to dismiss.</Body>
</BottomSheet>
</>
),
},
{
label: 'title + footer — a form in a sheet',
content: (
<>
<Button title="Open form" onPress={() => setForm(true)} />
<BottomSheet
visible={form}
onClose={() => setForm(false)}
title="Add a note"
footer={
<FormActions
primaryLabel="Save"
onPrimaryPress={() => setForm(false)}
secondaryLabel="Cancel"
onSecondaryPress={() => setForm(false)}
/>
}
>
<Input label="Note" placeholder="Anything worth remembering" value={note} onChangeText={setNote} />
</BottomSheet>
</>
),
},
{
label: 'long content — scrolls up to maxHeight',
content: (
<>
<Button title="Open long sheet" onPress={() => setLong(true)} />
<BottomSheet visible={long} onClose={() => setLong(false)} title="Twelve rows">
<View style={{ gap: 12 }}>
{LONG.map((row) => (
<Body key={row}>{row}</Body>
))}
</View>
</BottomSheet>
</>
),
},
{
label: 'footerPlacement="hide-with-keyboard" — the form keeps that row',
content: (
<>
<Button title="Open long form" onPress={() => setHiding(true)} />
<BottomSheet
visible={hiding}
onClose={() => setHiding(false)}
title="Long form"
footerPlacement="hide-with-keyboard"
footer={
<FormActions
primaryLabel="Save"
onPrimaryPress={() => setHiding(false)}
secondaryLabel="Cancel"
onSecondaryPress={() => setHiding(false)}
/>
}
>
<View style={{ gap: 12 }}>
{['Name', 'Company', 'Email', 'Phone', 'Address'].map((label) => (
<Input key={label} label={label} value={note} onChangeText={setNote} />
))}
</View>
</BottomSheet>
</>
),
},
{
label: 'draggable={false} + maxHeight={0.4}',
content: (
<>
<Button title="Open fixed sheet" onPress={() => setFixed(true)} />
<BottomSheet
visible={fixed}
onClose={() => setFixed(false)}
title="No dragging"
draggable={false}
maxHeight={0.4}
>
<Body>Dismiss with the close button or by tapping outside.</Body>
</BottomSheet>
</>
),
},
];
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 |
|---|---|---|---|
visible | boolean | — | — |
onClose | () => void | — | — |
title? | string | — | Con título aparece el header, con su botón de cerrar. |
footer? | ReactNode | — | Acciones de la hoja. Dónde viven lo decide `footerPlacement`. |
footerPlacement? | 'fixed' | 'scroll' | 'hide-with-keyboard' | fixed | 'fixed' lo ancla al fondo de la hoja; 'scroll' lo deja al final del contenido; 'hide-with-keyboard' lo esconde mientras se tipea, que en un formulario largo le devuelve esa franja al campo enfocado. |
scrollable? | boolean | true | El contenido scrollea cuando no entra. |
draggable? | boolean | true | Arrastrar la hoja hacia abajo la cierra. |
maxHeight? | number | 0.9 | Fracción de la pantalla (0-1) o píxeles. |
closeAccessibilityLabel? | string | Close | — |
containerStyle? | ViewStyle | — | — |
contentStyle? | ViewStyle | — | — |