Elementos
Teclado
env(keyboard-inset-height) es la altura del teclado de software sobre la superficie, y vale 0 mientras está oculto. Usado como padding o margin inferior de una vista, mantiene el contenido por encima del teclado: cuando el teclado aparece o se oculta, la vista hace espacio según su altura, y el cambio se anima de forma nativa junto con el teclado. Úsalo para contenido fuera de los scroll views, como una barra de mensajes en la parte inferior de una pantalla de chat; los inputs dentro de un scroll view o de un List se desplazan de forma nativa hasta quedar a la vista. Junto con él vienen Keyboard.dismiss(), que oculta el teclado, y la altura del teclado, para tus propios layouts.
import { mount, Scroll, signal, Text, View } from "nsh";
import { Input } from "nsh/base";
import { Button, Switch } from "nsh/base";
import { Keyboard, keyboardFrame } from "nsh";
import { button, buttonText, desktop, field, tint, ui } from "./palette";
function App() {
const draft = signal("");
const messages = signal(["Type below: the bar stays above the keyboard."]);
const behavior = signal<"padding" | "height">("padding");
const enabled = signal(true);
const send = () => {
if (!draft().trim()) return;
messages.update((list) => [...list, draft().trim()]);
draft.set("");
};
const chip = (on: () => boolean) => button("secondary", { on, size: "sm" });
const chipText = (on: () => boolean) => buttonText("secondary", { on });
const isPadding = () => behavior() === "padding";
const isHeight = () => behavior() === "height";
return (
<View
style={{
flex: 1,
backgroundColor: behavior() === "height" ? tint(ui().info, 0.12) : ui().page,
// Bottom padding (or margin) by the keyboard's height, while enabled.
...(!enabled() ? {} : behavior() === "padding" ? { paddingBottom: "env(keyboard-inset-height)" } : { marginBottom: "env(keyboard-inset-height)" }),
}}
>
<View style={{ padding: 20, paddingTop: 24, paddingBottom: 8, gap: 8 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>keyboard inset</Text>
<Text style={{ color: ui().muted }}>
{desktop
? "Desktops type with a hardware keyboard: its height stays 0 and the view keeps its layout. Open this on a phone to see it move."
: "Tap the message field: the bar moves up with the keyboard."}
</Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 6 }}>
<Button onPress={() => behavior.set("padding")} selected={isPadding()} {...chip(isPadding)}>
<Text style={chipText(isPadding)}>padding</Text>
</Button>
<Button onPress={() => behavior.set("height")} selected={isHeight()} {...chip(isHeight)}>
<Text style={chipText(isHeight)}>height</Text>
</Button>
<Switch
checked={enabled()}
onChange={({ checked }) => enabled.set(checked)}
class="flex-row items-center gap-2 px-1 active:opacity-70"
trackClass="w-9 rounded-full p-0.5 transition-colors motion-reduce:transition-none"
trackStyle={enabled() ? ui().accentFill : ui().inset}
thumbClass="size-4 rounded-full transition-transform motion-reduce:transition-none checked:translate-x-4"
thumbStyle={enabled() ? { backgroundColor: ui().onAccent } : ui().raised}
>
<Text style={{ color: ui().text, fontSize: 13, fontWeight: "600" }}>enabled {String(enabled())}</Text>
</Switch>
</View>
<Text style={{ color: ui().accent, fontSize: 13 }}>
keyboardFrame(): height {Math.round(keyboardFrame().height)} · duration {Math.round(keyboardFrame().duration)} ms
</Text>
</View>
<Scroll style={{ flex: 1 }} keyboardDismissMode="interactive">
<View style={{ padding: 20, paddingTop: 4, gap: 8 }}>
<Each of={messages()}>
{(text) => (
<View style={{ ...ui().row, alignSelf: "flex-start", maxWidth: 320, borderRadius: 14, paddingHorizontal: 12, paddingVertical: 8 }}>
<Text style={{ color: ui().text }}>{text()}</Text>
</View>
)}
</Each>
</View>
</Scroll>
<View style={{ flexDirection: "row", gap: 8, padding: 12, paddingHorizontal: desktop ? 20 : 12, borderTopWidth: 1, borderTopColor: ui().border, backgroundColor: ui().page }}>
<Input
value={draft()}
onChange={({ value }) => draft.set(value)}
onSubmit={send}
testID="message"
returnKeyType="send"
placeholder="Message"
{...field({ style: { flex: 1 } })}
/>
<Button onPress={send} {...button("primary")}>
<Text style={buttonText("primary")}>Send</Text>
</Button>
<Button onPress={() => Keyboard.dismiss()} {...button()}>
<Text style={buttonText()}>Done</Text>
</Button>
</View>
</View>
);
}
mount(App);use nsh_core::prelude::*;
fn chat() -> impl IntoElement {
let draft = signal(String::new());
view()
.flex_1()
.bg(white())
.pb_keyboard()
.child(scroll().flex_1().child(text("Messages…").p_4()))
.child(
view()
.flex_row()
.gap_2()
.p_2()
.border_t_1()
.border_color(rgb(0xe5e5e5))
.child(
input()
.value(draft)
.on_change(move |v: &str| draft.set(v.to_owned()))
.placeholder("Message")
.flex_1()
.p_2(),
),
)
}import { mount, Scroll, signal, Text, View } from "nsh";
import { Input } from "nsh/base";
function App() {
const draft = signal("");
return (
<View style={{ flex: 1, backgroundColor: "#ffffff", paddingBottom: "env(keyboard-inset-height)" }}>
<Scroll style={{ flex: 1 }} keyboardDismissMode="interactive">
<Text style={{ padding: 16, paddingTop: 48 }}>Messages…</Text>
</Scroll>
<View style={{ flexDirection: "row", gap: 8, padding: 8, borderTopWidth: 1, borderTopColor: "#e5e5e5" }}>
<Input
value={draft()}
onChange={({ value }) => draft.set(value)}
placeholder="Message"
style={{ flex: 1, fontSize: 16, padding: 8 }}
/>
</View>
</View>
);
}
mount(App);La altura completa del teclado es adecuada para una vista que llega hasta el borde inferior de la pantalla, como el contenido de una pantalla. Cuando hay algo por debajo, como una tab bar, réstalo con una longitud: calc(env(keyboard-inset-height) + -49px) mantiene una vista por encima de una tab bar de 49 pt, y calc(env(keyboard-inset-height) + 12px) añade espacio sobre el teclado. Un padding es como mínimo 0.
#Padding o margin
| Estilo | Cómo hace espacio |
|---|---|
paddingBottom: "env(keyboard-inset-height)" (Tailwind pb-[env(keyboard-inset-height)], Rust .pb_keyboard()) |
El contenido de arriba se reduce; el fondo sigue llegando hasta el borde inferior de la pantalla. |
marginBottom: "env(keyboard-inset-height)" (Tailwind mb-[env(keyboard-inset-height)], Rust .mb_keyboard()) |
La vista completa, incluido su fondo, termina en la parte superior del teclado. |
La altura del teclado se suma a la longitud solo dentro de la misma declaración, por lo que una vista la combina con su propio espaciado en calc(). En Rust, .pb_keyboard_plus(px(..)) y .mb_keyboard_plus(px(..)) añaden una longitud.
use nsh_core::prelude::*;
fn sign_in() -> impl IntoElement {
view()
.mb_keyboard_plus(px(-49.0))
.flex_1()
.p_4()
.gap_3()
.child(input().placeholder("E-mail").keyboard_type(KeyboardType::Email))
}<View class="flex-1 gap-3 p-4 mb-[calc(env(keyboard-inset-height)+-49px)]">
<Input placeholder="E-mail" keyboardType="email-address" class="rounded-lg border border-neutral-300 px-3 py-2" />
</View>Aquí, una tab bar de 49 pt debajo de la vista ya cubre parte de la altura del teclado. Para desactivar el espacio, deja la variable fuera del estilo, por ejemplo con una condición sobre un signal.
#Ocultar el teclado
Keyboard.dismiss(), de nsh, quita el foco del input enfocado, lo que oculta el teclado. Si no hay ningún input enfocado, lo deja todo como está. Los componentes de Rust llaman a dismiss_keyboard().
use nsh_base::button;
use nsh_core::prelude::*;
fn done() -> impl IntoElement {
button()
.on_press(|| dismiss_keyboard())
.rounded_lg()
.bg(rgb(0x2563eb))
.px_4()
.py_3()
.child(text("Done").text_color(white()))
}import { Text, Keyboard } from "nsh";
import { Button } from "nsh/base";
function Done() {
return (
<Button onPress={() => Keyboard.dismiss()} class="rounded-lg bg-blue-600 px-4 py-3 active:bg-blue-700">
<Text class="text-center font-semibold text-white">Done</Text>
</Button>
);
}#La altura del teclado
| TSX | Rust | Devuelve |
|---|---|---|
keyboardFrame() |
keyboard_height(), keyboard_duration() |
La altura del teclado de software sobre la superficie de NSH, en puntos (0 mientras está oculto), y la duración de su animación de aparición u ocultación, en milisegundos. |
Ambos son reactivos: un effect, una expresión JSX o un closure de estilo que los lea se vuelve a ejecutar cuando el teclado aparece, se oculta o cambia de altura.
use nsh_core::prelude::*;
fn message_bar() -> impl IntoElement {
view()
.flex_row()
.p_2()
.pb(move || {
// The home indicator's inset only matters while the keyboard is hidden.
let home = if keyboard_height() > Points::ZERO {
Points::ZERO
} else {
safe_area().bottom
};
px(8.0) + home
})
.child(input().placeholder("Message").flex_1())
}import { View, keyboardFrame, safeArea } from "nsh";
import { Input } from "nsh/base";
function MessageBar() {
const insets = safeArea();
return (
<View
style={{
flexDirection: "row",
padding: 8,
// The home indicator's inset only matters while the keyboard is hidden.
paddingBottom: 8 + (keyboardFrame().height > 0 ? 0 : insets().bottom),
}}
>
<Input placeholder="Message" style={{ flex: 1 }} />
</View>
);
}La altura es la parte del teclado que se superpone a la superficie de NSH. Cuando la propia ventana se reduce por el teclado (una activity de Android que se redimensiona), o cuando NSH está incrustado en una vista que termina por encima del teclado, la altura es 0.
#Notas por plataforma
- iOS: el espacio se anima con la duración propia del teclado y una curva ease-in-ease-out.
- Android: cuando la ventana se reduce por el teclado (una activity que se redimensiona), el contenido ya termina por encima del teclado y la variable vale 0. Cuando el teclado se superpone a la vista, hace espacio igual que en iOS.
- Escritorio y Mac Catalyst: se escribe con un teclado físico; la variable vale 0 y la vista mantiene su layout.
- Web: la variable es la propia del navegador (la de la VirtualKeyboard API).
Consulta Forms and keyboard para el orden de foco, las teclas de retorno y los accesorios de input, y Safe area para las demás variables de entorno.