Elementi
Tastiera
env(keyboard-inset-height) è l’altezza della tastiera software sopra la superficie, 0 quando è nascosta. Usata come padding o margin inferiore di una view, mantiene il contenuto sopra la tastiera: quando la tastiera appare o scompare, la view fa spazio per la sua altezza e la modifica viene animata nativamente insieme alla tastiera. Usala per i contenuti fuori dalle scroll view, come una barra dei messaggi in fondo a una schermata di chat; gli input dentro una scroll view o una List vengono portati in vista nativamente. Insieme a essa ci sono Keyboard.dismiss(), che nasconde la tastiera, e l’altezza della tastiera, per i layout personalizzati.
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);L’altezza completa della tastiera è adatta a una view che raggiunge il fondo dello schermo, come il contenuto di una schermata. Quando sotto c’è qualcos’altro, come una tab bar, sottrailo con una lunghezza: calc(env(keyboard-inset-height) + -49px) mantiene una view sopra una tab bar da 49 pt, e calc(env(keyboard-inset-height) + 12px) aggiunge spazio sopra la tastiera. Un padding è almeno 0.
#Padding o margin
| Stile | Come fa spazio |
|---|---|
paddingBottom: "env(keyboard-inset-height)" (Tailwind pb-[env(keyboard-inset-height)], Rust .pb_keyboard()) | Il contenuto sopra si riduce; lo sfondo raggiunge comunque il fondo dello schermo. |
marginBottom: "env(keyboard-inset-height)" (Tailwind mb-[env(keyboard-inset-height)], Rust .mb_keyboard()) | L’intera view, sfondo compreso, termina in cima alla tastiera. |
L’altezza della tastiera viene aggiunta solo alla lunghezza nella stessa dichiarazione, quindi una view la combina con una spaziatura propria in calc(). In Rust, .pb_keyboard_plus(px(..)) e .mb_keyboard_plus(px(..)) aggiungono una lunghezza.
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>Qui una tab bar da 49 pt sotto la view copre già parte dell’altezza della tastiera. Per disattivare lo spazio, ometti la variabile dallo stile, ad esempio con una condizione su un signal.
#Nascondere la tastiera
Keyboard.dismiss(), da nsh, rimuove il focus dall’input attivo, e questo nasconde la tastiera. Se nessun input ha il focus, lascia tutto invariato. I componenti Rust chiamano 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>
);
}#L’altezza della tastiera
| TSX | Rust | Restituisce |
|---|---|---|
keyboardFrame() | keyboard_height(), keyboard_duration() | L’altezza della tastiera software sopra la superficie NSH, in punti (0 quando è nascosta), e la durata della sua animazione di comparsa o scomparsa, in millisecondi. |
Entrambi sono reattivi: un effect, un’espressione JSX o una closure di stile che li legge viene rieseguita quando la tastiera appare, scompare o cambia altezza.
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>
);
}L’altezza è la parte della tastiera che si sovrappone alla superficie NSH. Quando la finestra stessa si riduce per la tastiera (un’activity Android che si ridimensiona), o quando NSH è incorporato in una view che termina sopra la tastiera, l’altezza è 0.
#Note sulle piattaforme
- iOS: lo spazio si anima con la durata propria della tastiera e una curva ease-in-ease-out.
- Android: dove la finestra si riduce per la tastiera (un’activity che si ridimensiona), il contenuto termina già sopra la tastiera e la variabile è 0. Dove la tastiera si sovrappone alla view, fa spazio come su iOS.
- Desktop e Mac Catalyst usano una tastiera hardware: la variabile è 0 e la view mantiene il proprio layout.
- Web: la variabile è quella propria del browser (quella della VirtualKeyboard API).
Consulta Moduli e tastiera per ordine del focus, tasti di invio e accessori degli input, e Safe area per le altre variabili d’ambiente.